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:
// build.gradle.kts
dependencies {
implementation("com.google.android.play:core:1.10.3")
}Then initialize the update manager in your main Activity or Application:
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:
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:
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:
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:
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:
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):
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:
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
startUpdateFlowForResultwithout verifyingupdateAvailability() == UPDATE_AVAILABLEcauses crashes or silent failures. - Not unregistering the update listener: The
InstallStateUpdatedListenercan cause memory leaks if not cleaned up inonPauseor 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
onResumeandonPauseto 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.






Comments
Loading comments…