Chapters

Hide chapters

Android App Distribution

First Edition · Android 12 · Kotlin 1.5 · Android Studio Bumblebee

13. Getting Top Ratings & Avoiding Negative Reviews: Gathering In-App Feedback
Written by Fuad Kamal

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

So now your app is live on the Play Store, after months of internal, alpha and beta testing. Your app is hardened and secure, and you’ve built automated toolchains to rapidly test and update your app. What happens once your software is in the customers’ hands?

How do you assess how they feel about the app? How do you collect and respond to feedback? How can you avoid most one-star ratings and ensure your users are happy and have the best possible experience, both with the app and your company? In this chapter, you’ll get an inside look at how two large companies accomplished just that and how with a little effort, you can, too.

Responding to Google Play comments in the dashboard

When your app is on Google Play in any form, your users can provide a rating and feedback. This feedback is visible to you in the Play Console under the Quality section.

Go to the Play Console and open Quality -> Ratings and reviews -> Ratings. Here you’ll find various statistics and data visualizations for your app’s ratings over time. Note that in this section, ratings from both beta and release versions of your app are combined.

Next, check the Reviews section. Here you’ll find any reviews for the release version of your app. You may also see a notice reminding you to use the Google Play In-App Review API. You’ll implement this in Podplay later in this chapter.

Feedback from beta versions of your app appears in the Testing Feedback section. You’ll find the star rating and user’s comments for each review, along with a text box where you can directly reply to the feedback. Your replies will then appear in the Play Store along with the user’s review. It’s a handy way for developers to provide some sort of response to reviews received in the Play Store.

Next, check out the Notifications page under the Play Console’s Setup section. Here you’ll find various options to get email notifications, such as a notification when a user updates their review after you’ve replied to it.

Ratings and reviews can significantly impact the perception of your app and the willingness of new users to download it. Most app developers seem to publish their app and then wait for reviews, good or bad. However, the typical user doesn’t make an effort to leave feedback or rate your app on the App Store unless they have a very negative experience. Then, they take the extra effort to go to the Play Store, leave a one-star rating and rant about your app.

Typically these users are frustrated after having a negative experience, and they don’t know a more effective way to get the developer’s attention to resolve the issue. For example, maybe 70% of your users are happy with your app, but 30% of users have some sort of frustrating experience. Most users with a good experience might not bother to leave a rating or review, while users with a negative experience will make an extra effort to leave a review. The typical net result can be a low one or two-star rating overall on the App Store and a long list of negative reviews, despite most users having a good experience.

However, you can be proactive and guide users to provide feedback directly to you from within your app when they have a negative experience, rather than leaving negative feedback publicly on the Play Store. Likewise, you can encourage users who had a good experience with your app to leave a high star rating and positive review on Google Play.

Case studies: in-app feedback

Earlier in this chapter, you learned the strategy for optimizing your app’s ratings and reviews in your favor. That strategy isn’t untested theory. More than one company has quickly changed overwhelmingly negative reviews and one-star ratings to positive reviews and four and five-star ratings.

Target

Target, the eighth largest American retail chain, has been in business since 1962. It has close to 2,000 stores spread across the country. However, their public-facing apps were plagued with one-star reviews and negative ratings. Erik Kerber, the principal iOS developer at Target, posted the following in the raywenderlich.com slack in July 2017:

Octo Telematics

Octo Telematics, based in Rome, Italy, is the world’s leading telematics provider. They produce hardware and apps used by auto insurance companies worldwide to monitor their customers’ driving behavior, enabling them to reward good driving behavior with incentives such as discounts on insurance premiums. One of their clients had an app with an overwhelming number of one-star reviews on both the iOS and Android app stores.

Building an in-app feedback mechanism

Now that you’ve seen how important it is to properly channel reviews and feedback for your app, you’ll learn how to implement it in Podplay. Sending your user out of the app to leave a review in the Play Store can be a disruptive experience. Thankfully, you don’t have to do that anymore because Google, and Apple for iOS, now provides in-app review APIs which let users leave feedback and reviews directly from within your app interface.

Creating an in-app review module

To start implementing the in-app review feature, you’ll create a new feature module. This module will let you decouple the relevant code from the rest of the code. Decoupling features helps you reuse them across your app or even between multiple projects. In fact, you can even publish the code as a library for other people or organizations to use.

implementation project(":inappreview")
// 1
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:$coreKtx_version"

//2 - UI
implementation "androidx.appcompat:appcompat:$appCompat_version"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayout_version"
implementation "androidx.cardview:cardview:$cardView_version"

// 3 - Play Core
api "com.google.android.play:core:$playCore_version"
api "com.google.android.play:core-ktx:$playCoreKtx_version"

Review prompt UI

Next, you need to create the UI for your review prompt.

dataBinding {
  enabled = true
}
buildFeatures {
  viewBinding true
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:id="@+id/inAppReviewPromptRoot"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_gravity="center"
  android:orientation="vertical"
  app:cardCornerRadius="4dp"
  app:cardElevation="0dp">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:orientation="vertical">

    <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      android:paddingTop="@dimen/largeMargin">

      <ImageView
        android:id="@+id/sadFace"
        android:layout_width="@dimen/ratePromptFaceSize"
        android:layout_height="@dimen/ratePromptFaceSize"
        android:layout_marginStart="@dimen/largeMargin"
        android:contentDescription="@string/sad_face"
        android:src="@drawable/sad_face" />

      <ImageView
        android:id="@+id/progressBar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/smallMargin"
        android:layout_marginStart="@dimen/promptProgressMargin"
        android:layout_marginEnd="@dimen/promptProgressMargin"
        android:layout_weight="1"
        android:contentDescription="@string/progress_bar"
        android:src="@drawable/progress_black" />

      <ImageView
        android:id="@+id/happyFace"
        android:layout_width="@dimen/ratePromptFaceSize"
        android:layout_height="@dimen/ratePromptFaceSize"
        android:layout_marginEnd="@dimen/largeMargin"
        android:contentDescription="@string/happy_face"
        android:src="@drawable/happy_face" />

    </LinearLayout>

    <TextView
      android:id="@+id/reviewPromptTitle"
      style="@style/TextAppearance.AppCompat.Headline"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_margin="@dimen/defaultMargin"
      android:gravity="center_horizontal"
      android:text="@string/inAppReviewTitle"
      android:textColor="@color/charcoal_black"
      android:textSize="@dimen/ratePromptTitleTextSize" />

    <TextView
      android:id="@+id/reviewPromptText"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginStart="@dimen/defaultMargin"
      android:layout_marginEnd="@dimen/defaultMargin"
      android:gravity="center"
      android:text="@string/inAppReviewMessage"
      android:textSize="@dimen/ratePromptMessageTextSize" />

    <TextView
      android:id="@+id/leaveReview"
      android:layout_width="match_parent"
      android:layout_height="@dimen/buttonSize"
      android:layout_marginHorizontal="@dimen/defaultMargin"
      android:layout_marginTop="@dimen/largeMargin"
      android:background="@color/dark_sea_green"
      android:gravity="center"
      android:text="@string/inAppReviewRateNow"
      android:textColor="@color/white"
      android:textSize="@dimen/promptButtonTextSize" />

    <TextView
      android:id="@+id/reviewLater"
      android:layout_width="match_parent"
      android:layout_height="@dimen/buttonSize"
      android:layout_marginHorizontal="@dimen/defaultMargin"
      android:layout_marginTop="@dimen/buttonMarginTop"
      android:layout_marginBottom="@dimen/defaultMargin"
      android:background="?attr/selectableItemBackground"
      android:gravity="center"
      android:text="@string/inAppReviewRateLater"
      android:textSize="@dimen/promptButtonTextSize" />
  </LinearLayout>
</androidx.cardview.widget.CardView>

import androidx.fragment.app.DialogFragment

class InAppReviewPromptDialog: DialogFragment() {

  private var binding: FragmentInAppReviewPromptBinding? = null

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    binding = FragmentInAppReviewPromptBinding.inflate(inflater, container, false)

    return binding?.root
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    initListeners()
    dialog?.setCanceledOnTouchOutside(false)
  }

  private fun initListeners() {
    val binding = binding ?: return

    binding.leaveReview.setOnClickListener { onLeaveReviewTapped() }
    binding.reviewLater.setOnClickListener { onRateLaterTapped() }
  }

  private fun onLeaveReviewTapped() {
    // TODO
    dismissAllowingStateLoss()
  }

  private fun onRateLaterTapped() {
    // TODO
    dismissAllowingStateLoss()
  }

  /**
   * Styles the dialog to have a transparent background and window insets.
   */
  override fun onStart() {
    super.onStart()
    initStyle()
  }

  private fun initStyle() {
    val back = ColorDrawable(Color.TRANSPARENT)
    dialog?.window?.setBackgroundDrawable(back)
    dialog?.window?.setLayout(
      resources.getDimensionPixelSize(R.dimen.ratePromptWidth),
      resources.getDimensionPixelSize(R.dimen.ratePromptHeight))
  }
}

In-app review feature

Now that you’ve set up the UI, it’s time to build out the in-app review feature. You want to show the UI from the previous section after using Podplay for some pre-defined amount of time. The dialog will offer two options: one to rate the app and one to rate it later.

// 1
fun hasUserRatedApp(): Boolean

fun setUserRatedApp(hasRated: Boolean)

// 2
fun hasUserChosenRateLater(): Boolean

fun setUserChosenRateLater(hasUserChosenRateLater: Boolean)

// 3
fun getRateLaterTime(): Long

fun setRateLater(time: Long)
class InAppReviewPreferencesImpl(
  private val sharedPreferences: SharedPreferences
// 1
) : InAppReviewPreferences {

  // 2
    companion object {
        const val KEY_IN_APP_REVIEW_PREFERENCES = "inAppReviewPreferences"

        private const val KEY_HAS_RATED_APP = "hasRatedApp"
        private const val KEY_CHOSEN_RATE_LATER = "chosenRateLater"
        private const val KEY_RATE_LATER_TIME = "rateLaterTime"
    }
}
override fun hasUserRatedApp(): Boolean =
	sharedPreferences.getBoolean(KEY_HAS_RATED_APP, false)

override fun setUserRatedApp(hasRated: Boolean) =
  sharedPreferences.edit { putBoolean(KEY_HAS_RATED_APP, hasRated) }

override fun hasUserChosenRateLater(): Boolean =
  sharedPreferences.getBoolean(KEY_CHOSEN_RATE_LATER, false)

override fun setUserChosenRateLater(hasUserChosenRateLater: Boolean) =
  sharedPreferences.edit { putBoolean(KEY_CHOSEN_RATE_LATER, hasUserChosenRateLater) }

override fun getRateLaterTime(): Long =
  sharedPreferences.getLong(KEY_RATE_LATER_TIME, System.currentTimeMillis())

override fun setRateLater(time: Long) =
  sharedPreferences.edit { putLong(KEY_RATE_LATER_TIME, time) }
import androidx.core.content.edit
private val preferences: InAppReviewPreferences
preferences.setUserRatedApp(true)
preferences.setUserChosenRateLater(true)
preferences.setRateLater(getLaterTime())
private fun getLaterTime(): Long {
  return System.currentTimeMillis() + TimeUnit.DAYS.toMillis(14)
}

override fun onCancel(dialog: DialogInterface) {
  preferences.setUserChosenRateLater(true)
  preferences.setRateLater(getLaterTime())
  super.onCancel(dialog)
}

Implementing the in-app review manager

Now that you have almost everything in place regarding project setup and data, it’s time to build the in-app review manager. Create a new interface in the inappreview module called InAppReviewManager and add the following to the body:

fun startReview(activity: Activity)

fun isEligibleForReview(): Boolean
class InAppReviewManagerImpl(
  private val reviewManager: ReviewManager,
  private val inAppReviewPreferences: InAppReviewPreferences
): InAppReviewManager {

  companion object {
    private const val KEY_REVIEW = "reviewFlow"
  }

  private var reviewInfo: ReviewInfo? = null

  init {
    if (isEligibleForReview()) {
      reviewManager.requestReviewFlow().addOnCompleteListener {
        if (it.isComplete && it.isSuccessful) {
          this.reviewInfo = it.result
        }
      }
    }
  }

}
override fun isEligibleForReview(): Boolean {
  // 1
  return (!inAppReviewPreferences.hasUserRatedApp() &&
          !inAppReviewPreferences.hasUserChosenRateLater()
          // 2
          || (inAppReviewPreferences.hasUserChosenRateLater() && enoughTimePassed()))
}

// 3
private fun enoughTimePassed(): Boolean {
  val rateLaterTimeStamp = inAppReviewPreferences.getRateLaterTime()

  return abs(rateLaterTimeStamp - System.currentTimeMillis()) >= TimeUnit.DAYS.toMillis(14)
}
override fun startReview(activity: Activity) {
  val myReviewInfo = reviewInfo
  if (myReviewInfo != null) {
    reviewManager.launchReviewFlow(activity, myReviewInfo)
      .addOnCompleteListener { reviewFlow ->
        onReviewFlowLaunchCompleted(reviewFlow)
      }
  }
}

private fun onReviewFlowLaunchCompleted(reviewFlow: Task<Void>) {
  if (reviewFlow.isSuccessful) {
    logSuccess()
  }
}

private fun logSuccess() {
  if (BuildConfig.DEBUG) {
    Log.d(KEY_REVIEW, "Review complete!")
  }
}

Connecting the manager to the UI

Finally, you need to connect the manager to the UI. Create an interface named InAppReviewView in the inappreview module. You’ll implement this interface in places you want to expose and use the in-app review feature. Add the following function to this interface:

fun showReviewFlow()
private val inAppReviewManager: InAppReviewManager
inAppReviewManager.startReview(requireActivity())
podcastViewModel.setInAppReviewView(this)

class PodcastActivity :
    AppCompatActivity(),
    PodcastListAdapterListener,
    OnPodcastDetailsListener,
    InAppReviewView {
override fun showReviewFlow() {
}
private lateinit var inAppReview: InAppReviewView
fun setInAppReviewView(podcastActivity: PodcastActivity) {
  this.inAppReview = podcastActivity
}
private fun checkIfNeedsReviewPrompt() {
  val value = podcastListAdapter.itemCount
  if (value > 3) {
    showReviewFlow()
  }
}
private lateinit var preferences: InAppReviewPreferences
private lateinit var inAppReviewManager: InAppReviewManager
private fun setupReviewManager() {
  val sharedPreferences = getSharedPreferences(InAppReviewPreferencesImpl.KEY_IN_APP_REVIEW_PREFERENCES, Context.MODE_PRIVATE)
  preferences = InAppReviewPreferencesImpl(sharedPreferences)
  inAppReviewManager = InAppReviewManagerImpl(
    ReviewManagerFactory.create(this),
    preferences
  )
}
if(inAppReviewManager.isEligibleForReview()) {
  val dialog = InAppReviewPromptDialog(preferences, inAppReviewManager)
  dialog.show(this.supportFragmentManager, null)
}

Testing the in-app review feature

In-App Review is tied to the Play Core API environment, so you need to do a few things to make it testable and make sure it works. It requires live data from the Play Store or the app you want to publish.

Testing limitations

Currently, you can only test In-App Review with builds from the Play Store. However, you can test your in-app review integration using an internal testing track or internal app sharing. For rapid iteration, internal app sharing is the way to go. However, note that you can’t submit the reviews to the Play Store when using when this method. For that, you need to use the internal test track instead.

Key points

As Rob Napier (@cocoaphony) famously said,

Where to go from here?

Check out the Android In-App Review video course on raywenderlich.com at https://www.raywenderlich.com/20065814-android-in-app-review/lessons/1

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now