Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 074 / 100

In-app updates and reviews: patterns, pitfalls and fixes

Keep users current and gather ratings.

Expona Admin7 min read

In-app updates and reviews: patterns, pitfalls and fixes

Keeping your Android users on the latest version and collecting their feedback are two of the most critical tasks for maintaining app health. The Play Store gives us two powerful APIs—In-App Updates and In-App Review—but misusing them can frustrate users and tank your ratings. This article walks through the patterns, pitfalls and fixes that we've learned from managing these flows in production e-commerce, SaaS and ride-hailing apps.

Why updates and reviews matter in production

Users rarely visit the Play Store to update manually. Without in-app prompting, you'll end up with a fragmented install base running old code—making bug fixes, security patches and feature rollouts unpredictable. Similarly, most users never rate an app unless you ask at exactly the right moment. A 4.5-star rating with 50,000 reviews attracts new installs; a 3.2-star rating with the same volume drives them away.

The balance is delicate. Too aggressive with updates and reviews, and users uninstall. Too passive, and you lose visibility into problems and momentum. This is where understanding the APIs and their constraints becomes essential.

Play In-App Updates API: overview and setup

The Play In-App Updates API lets you prompt users to update without sending them to the Play Store. Google provides two update modes: flexible (user can defer) and immediate (blocks the app until update is done). First, add the dependency:

kotlin
// build.gradle.kts
dependencies {
    implementation("com.google.android.play:core:1.10.3")
}

Then initialize the update manager in your main Activity or Application:

kotlin
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory

class MainActivity : AppCompatActivity() {
    private lateinit var appUpdateManager: AppUpdateManager
    private val updateRequestCode = 100

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        appUpdateManager = AppUpdateManagerFactory.create(this)
        checkForUpdates()
    }

    private fun checkForUpdates() {
        val appUpdateInfoTask = appUpdateManager.appUpdateInfo

        appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
            // Handle update availability
        }
    }
}

The appUpdateInfo object tells you whether an update is available, its type (flexible or immediate), and how long the update has been queued.

Flexible vs immediate updates: when to use each

The choice between flexible and immediate updates defines the user experience and your release strategy.

Flexible updates let users continue using the app while the new version downloads in the background. This is appropriate for feature releases, minor bug fixes, and UI improvements. The user sees a notification when the download completes and can choose to install at their convenience. Use flexible updates for most production releases:

kotlin
private fun checkForUpdates() {
    val appUpdateInfoTask = appUpdateManager.appUpdateInfo

    appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
        if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
            appUpdateInfo.isFlexibleUpdateAllowed
        ) {
            startFlexibleUpdate(appUpdateInfo)
        }
    }
}

private fun startFlexibleUpdate(appUpdateInfo: AppUpdateInfo) {
    try {
        appUpdateManager.startUpdateFlowForResult(
            appUpdateInfo,
            AppUpdateType.FLEXIBLE,
            this,
            updateRequestCode
        )
    } catch (e: IntentSender.SendIntentException) {
        Log.e("UpdateError", e.toString())
    }
}

After the download completes, listen for the completion event and prompt the user to install:

kotlin
private val updateListener = InstallStateUpdatedListener { installState ->
    when (installState.installStatus()) {
        InstallStatus.DOWNLOADED -> {
            // Update downloaded; show snackbar prompting restart
            showRestartSnackbar()
        }
        InstallStatus.INSTALLED -> {
            // Installation complete; update was applied
            appUpdateManager.unregisterListener(updateListener)
        }
        else -> {}
    }
}

override fun onResume() {
    super.onResume()
    appUpdateManager.registerListener(updateListener)
}

override fun onPause() {
    super.onPause()
    appUpdateManager.unregisterListener(updateListener)
}

private fun showRestartSnackbar() {
    Snackbar.make(
        findViewById(android.R.id.content),
        "Update ready to install",
        Snackbar.LENGTH_INDEFINITE
    ).setAction("Restart") {
        appUpdateManager.completeUpdate()
    }.show()
}

Immediate updates pause the app and force the user to update. This should be reserved for critical security patches or blocking bugs that prevent core functionality. Google enforces strict rules on immediate updates—abuse causes Play Store rejection. Use this sparingly:

kotlin
private fun startImmediateUpdateIfCritical(appUpdateInfo: AppUpdateInfo) {
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
        appUpdateInfo.isImmediateUpdateAllowed &&
        isCriticalSecurityPatch()
    ) {
        try {
            appUpdateManager.startUpdateFlowForResult(
                appUpdateInfo,
                AppUpdateType.IMMEDIATE,
                this,
                updateRequestCode
            )
        } catch (e: IntentSender.SendIntentException) {
            Log.e("UpdateError", e.toString())
        }
    }
}

In production, we check a remote config flag to decide whether a pending update is critical. This decouples the release decision from the binary version.

In-App Review API: gathering feedback at the right moment

The In-App Review API opens a lightweight dialog prompting users to rate your app without leaving. Unlike older dialogs, Google manages the review flow—you can't force a user to submit, and Google rate-limits these prompts to protect the user experience.

Add the dependency:

kotlin
dependencies {
    implementation("com.google.android.play:core:1.10.3")
}

Request a review only after a meaningful user action—completing a purchase, finishing a task, or successfully using a key feature. In a ride-hailing app, we prompt after the user completes a ride. In e-commerce, we ask after checkout:

kotlin
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory

class OrderConfirmationActivity : AppCompatActivity() {
    private lateinit var reviewManager: ReviewManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_order_confirmation)

        reviewManager = ReviewManagerFactory.create(this)

        // After order confirmed
        findViewById(R.id.continueButton).setOnClickListener {
            requestReview()
            navigateHome()
        }
    }

    private fun requestReview() {
        val reviewFlow = reviewManager.requestReviewFlow()
        reviewFlow.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val reviewInfo = task.result
                val launchReviewFlow = reviewManager.launchReviewFlow(
                    this@OrderConfirmationActivity,
                    reviewInfo
                )
                launchReviewFlow.addOnCompleteListener { _ ->
                    // Review flow completed (user may or may not have submitted)
                }
            } else {
                // Handle error; review not available
                Log.e("Review", "Error requesting review", task.exception)
            }
        }
    }
}

The key insight: the review dialog may not appear at all. Google's rate-limiting ensures users aren't spammed. The flow completes regardless, so don't block navigation or functionality on it.

Do not prompt too often: throttling and user preferences

Aggressive prompting is one of the fastest ways to lose users. Google's API includes built-in rate-limiting, but you should add your own throttling to prevent re-prompting on every session.

Store a timestamp of the last review request and enforce a minimum interval (we use 90 days in production):

kotlin
import android.content.SharedPreferences

class ReviewManager(private val prefs: SharedPreferences) {
    private val lastReviewRequestKey = "last_review_request_time"
    private val minIntervalMillis = 90L * 24 * 60 * 60 * 1000

    fun shouldRequestReview(): Boolean {
        val lastTime = prefs.getLong(lastReviewRequestKey, 0L)
        val now = System.currentTimeMillis()
        return (now - lastTime) > minIntervalMillis
    }

    fun recordReviewRequested() {
        prefs.edit().putLong(lastReviewRequestKey, System.currentTimeMillis()).apply()
    }
}

Similarly, track whether the user has actively dismissed a review prompt and respect that preference:

kotlin
class ReviewManager(private val prefs: SharedPreferences) {
    private val reviewDismissedKey = "user_dismissed_review"

    fun userDismissedReview(): Boolean {
        return prefs.getBoolean(reviewDismissedKey, false)
    }

    fun recordReviewDismissed() {
        prefs.edit().putBoolean(reviewDismissedKey, true).apply()
    }

    fun shouldShowReview(): Boolean {
        return !userDismissedReview() && shouldRequestReview()
    }
}

In a real app, you might also respect a remote config setting that disables reviews during onboarding or if your app is in a known broken state.

Common mistakes

  • Prompting for review on app launch: Users haven't used the app yet. Request review only after meaningful interaction—a completed action, not just opening.
  • Using immediate updates for non-critical releases: Google's Play Store policies flag this. Reserve immediate for genuine security patches; use flexible for everything else.
  • Ignoring update availability checks: Calling startUpdateFlowForResult without verifying updateAvailability() == UPDATE_AVAILABLE causes crashes or silent failures.
  • Not unregistering the update listener: The InstallStateUpdatedListener can cause memory leaks if not cleaned up in onPause or when the Activity is destroyed.
  • Prompting on every session: Even though Google rate-limits reviews, your own throttling gives users a better experience. Prompting for updates more than once per session annoys users.
  • Blocking navigation on review completion: The review dialog may not appear or may close without the user rating. Never gate core functionality on it.

Key takeaways

  • Use flexible updates for most releases and immediate updates only for critical security patches. Validate that updates are actually available before starting the flow.
  • Request reviews after meaningful user actions, not on launch. Respect user dismissals and enforce a minimum 90-day interval between prompts.
  • Register and unregister the update listener in onResume and onPause to avoid memory leaks and handle app backgrounding gracefully.
  • Don't block app functionality on update or review flows; both can fail silently or be rate-limited by the Play Store.
  • Use remote config to control update criticality and review availability without requiring a new binary release.
  • Test both APIs on a real device with a staged release; emulator behavior doesn't match production rate-limiting.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles