How to ask users nicely to review your Android app using Kotlin

Last updated on: May 27, 2023

A common way for developers to ask their users for an app review is to show a pop-up dialog after a couple of starts of the app.

The problem is users will ignore it because you interrupt their experience.

To fix this problem, we will implement this thing inside a RecyclerView.

In this tutorial, we are using the library Amplify, which is heavily inspired by Circa’s article The right way to ask users to review your app.

First, go to the top-level build.gradle file and add the jcenter (the library is not available in the maven repo yet…)

buildscript {

    // ...

    repositories {
        google()
        mavenCentral()
        jcenter()
        
    }

    // ...

}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter()

    }
}

Then, go to the module-level build.gradle file and add the library:

dependencies {

    // ...

    implementation 'com.github.stkent:amplify:2.2.3'

}Code language: Kotlin (kotlin)

In the colors.xml file, inside the folder values, we’re adding the background and text color we want our rate view to have.

<color name="promptBackgroundColor">#bc214b</color>
<color name="promptTextColor">#FFFFFF</color>Code language: HTML, XML (xml)

Now, we need to add our RecyclerView row layout, which contains the view, by making a new XML file under the layout folder named review_row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

    <com.github.stkent.amplify.prompt.DefaultLayoutPromptView
            android:id="@+id/prompt_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

</LinearLayout>Code language: HTML, XML (xml)

Let’s go now and set up how frequently we’re going to ask our users to review our app by adding in our Application() file (or MultiDexApplication()) the following code:

class Application : Application() {

    override fun onCreate() {
        super.onCreate()
        Amplify.initSharedInstance(this)
            .setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
            .setCriticalFeedbackCollectors(object :
                BaseEmailFeedbackCollector("feedback@domain.com") {
                override fun getSubjectLine(
                    iApp: IApp,
                    iEnvironment: IEnvironment,
                    iDevice: IDevice
                ): String =
                    "Feedback for the Social App"

                override fun getBody(
                    iApp: IApp,
                    iEnvironment: IEnvironment,
                    iDevice: IDevice
                ): String =
                    ""
            })
            .addEnvironmentBasedRule(GooglePlayStoreRule()) // Prompt never shown if Google Play Store not installed.
            .setInstallTimeCooldownDays(5)   // Prompt not shown within 5 days of initial install.
            .setLastUpdateTimeCooldownDays(3) // Prompt not shown within 3 days of most recent update.
            .setLastCrashTimeCooldownDays(7) // Prompt not shown within one week of most recent crash.
            .addTotalEventCountRule(
                PromptInteractionEvent.USER_GAVE_POSITIVE_FEEDBACK,
                MaximumCountRule(1)
            ) // Never ask the user for feedback again if they already responded positively.
            .addTotalEventCountRule(
                PromptInteractionEvent.USER_GAVE_CRITICAL_FEEDBACK,
                MaximumCountRule(1)
            ) // Never ask the user for feedback again if they already responded critically.
            .addTotalEventCountRule(
                PromptInteractionEvent.USER_DECLINED_FEEDBACK,
                MaximumCountRule(1)
            ).setAlwaysShow(BuildConfig.DEBUG)
    }
}Code language: Kotlin (kotlin)

If you don’t have any Application() file on your project, create a new Kotlin file and paste the code above. After that, go to your AndroidManifest.xml and add android:name=".Application" inside the tag <application> like this:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.johncodeos.asktorate">

    <application
            android:name=".Application"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity
                android:name=".MainActivity"
                android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>Code language: HTML, XML (xml)

After we finish with all that, let’s add it to our RecyclerView.

Go to your RecyclerView Adapter, in our example, it’s called SocialRVAdapter, and add the following code:

class SocialRVAdapter(private val socialCells: List<SocialModel>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    inner class PostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    inner class RateViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    private lateinit var mContext: Context

    companion object {
        const val RATE_VIEW = 0
        const val POST_VIEW = 1
        const val RATE_VIEW_POSITION = 1
    }

    private lateinit var basePromptViewConfig: BasePromptViewConfig
    private lateinit var defaultLayoutPromptViewConfig: DefaultLayoutPromptViewConfig


    override fun getItemViewType(position: Int): Int {
        return if (position == RATE_VIEW_POSITION) {
            RATE_VIEW
        } else {
            POST_VIEW
        }
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        mContext = parent.context
        if (viewType == RATE_VIEW) {
            val rateView =
                LayoutInflater.from(parent.context).inflate(R.layout.review_row, parent, false)
            val rateViewHolder = RateViewHolder(rateView)
            return rateViewHolder
        } else if (viewType == POST_VIEW) {
            val postView =
                LayoutInflater.from(parent.context).inflate(R.layout.post_row, parent, false)
            val postViewHolder = PostViewHolder(postView)
            return postViewHolder
        }
        return null!!
    }


    override fun getItemCount(): Int {
        //We adding 1 more row in the size of the array for our Rate View
        return socialCells.size + 1
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder.itemViewType == RATE_VIEW) {
            val rateViewHolder = holder as RateViewHolder

            val promptView =
                rateViewHolder.itemView.findViewById<DefaultLayoutPromptView>(R.id.prompt_view)
            setupPromptViewConfig()
            setupPromptViewColors(R.color.promptBackgroundColor, R.color.promptTextColor)
            promptView.applyBaseConfig(basePromptViewConfig)
            promptView.applyConfig(defaultLayoutPromptViewConfig)
            Amplify.getSharedInstance().promptIfReady(promptView)
        } else if (holder.itemViewType == POST_VIEW) {
            val postViewHolder = holder as PostViewHolder
            val currentPosition: Int = if (position > RATE_VIEW_POSITION) {
                position - 1
            } else {
                position
            }

            val userProfilePicImageView =
                postViewHolder.itemView.findViewById<ImageView>(R.id.user_profile_pic_imageview)
            userProfilePicImageView.setImageResource(socialCells[currentPosition].userProfilePic)

            val userNameTextView =
                postViewHolder.itemView.findViewById<TextView>(R.id.username_textview)
            userNameTextView.text = socialCells[currentPosition].userName

            val userDateTextView =
                postViewHolder.itemView.findViewById<TextView>(R.id.user_date_textview)
            userDateTextView.text = socialCells[currentPosition].userDate

            val userDescTextview =
                postViewHolder.itemView.findViewById<TextView>(R.id.user_desc_textview)
            userDescTextview.text = socialCells[currentPosition].userDesc

            val userPhotoImageview =
                postViewHolder.itemView.findViewById<ImageView>(R.id.user_photo_imageview)
            userPhotoImageview.setImageResource(socialCells[currentPosition].userPhoto)
        }
    }

    private fun setupPromptViewConfig() {
        basePromptViewConfig = BasePromptViewConfig.Builder()
            .setUserOpinionQuestionTitle("Do you like Social App?")
            .setUserOpinionQuestionPositiveButtonLabel("Yeah!")
            .setUserOpinionQuestionNegativeButtonLabel("Nah")
            .setPositiveFeedbackQuestionTitle("Awesome! Would you like to rate it?")
            .setPositiveFeedbackQuestionSubtitle("It helps a lot :)")
            .setPositiveFeedbackQuestionPositiveButtonLabel("Sure!")
            .setPositiveFeedbackQuestionNegativeButtonLabel("Not right now")
            .setCriticalFeedbackQuestionTitle(":( Would you like to send feedback?")
            .setCriticalFeedbackQuestionPositiveButtonLabel("Sure!")
            .setCriticalFeedbackQuestionNegativeButtonLabel("Not right now")
            .setThanksTitle("Thank you!!!")
            .setThanksDisplayTimeMs(2000)
            .build()
    }

    private fun setupPromptViewColors(backgroundColor: Int, textColor: Int) {
        defaultLayoutPromptViewConfig = DefaultLayoutPromptViewConfig.Builder()
            .setForegroundColor(ContextCompat.getColor(mContext, textColor))
            .setBackgroundColor(ContextCompat.getColor(mContext, backgroundColor))
            .setTitleTextColor(ContextCompat.getColor(mContext, textColor))
            .setSubtitleTextColor(ContextCompat.getColor(mContext, textColor))
            .setPositiveButtonTextColor(ContextCompat.getColor(mContext, textColor))
            .setPositiveButtonBackgroundColor(ContextCompat.getColor(mContext, backgroundColor))
            .setPositiveButtonBorderColor(ContextCompat.getColor(mContext, textColor))
            .setNegativeButtonTextColor(ContextCompat.getColor(mContext, textColor))
            .setNegativeButtonBackgroundColor(ContextCompat.getColor(mContext, backgroundColor))
            .setNegativeButtonBorderColor(ContextCompat.getColor(mContext, textColor))
            .build()
    }

}Code language: Kotlin (kotlin)

Here’s what we’re doing:

  • Create a ViewHolder for our RateView as we have for our Posts (called PostViewHolder)
  • Give a number that we’ll use in the getItemViewType later to know in which row will return our Rate or Post view
  • Give the position we want to show the RateView in the RecyclerView
  • The getItemViewType method will return the RateView in our position that we put in RATE_VIEW_POSITION before
  • Set up the layouts for the Rate View and Post View
  • Add one more row to include our RateView in our array with our posts
  • Set up our RateView with our preferable prompt text and the colors
  • Calculate the position, so we don’t skip any Post because from the row that RateView is using

That’s it!!

You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments