Descriptive image for Page: Clean and user friendly Android app tracking with Firebase

Clean and user friendly Android app tracking with Firebase

Usually companies and developers care about the performance of their products or services and how the user interacts with them. The most basic things that come to one’s mind are screen/page views and crashes and interactions like clicking a link or button. To measure these values there are several solutions out there. We could use server side tracking and check how often a page is requested or client side tracking to send events to a server.

One of the solutions for client side tracking is Google Firebase Analytics/Crashlytics, which we will take a look at today. We will learn some basic steps to set it up for an Android application and after that look at how we can improve our most basic implementation.

Set up Analytics and Crashlytics

First step is to get everything up and running. You can skip this if you just came for the “clean and user friendly tracking” part of this post. Also, we got to keep in mind the restrictions for our projects:

  • com.android.tools.build:gradle v3.2.1+
  • compileSdkVersion 28+
  • Gradle 4.1+

I will summarize everything shortly. You can find a detailed guide for the setup here.

1. Add the dependencies to your app

Top level build.gradle:

buildscript {

  repositories {
    ...
    google()
    ...
  }

  dependencies {
    ...
    classpath 'com.google.gms:google-services:4.3.3'
    ...
  }
}

allprojects {
  ...
  repositories {
    ...
    google()
    ...
  }
  ...
}

App level build.gradle:

dependencies {
    implementation 'com.google.firebase:firebase-analytics-ktx:x.x.x'
    implementation 'com.google.firebase:firebase-crashlytics:x.x.x'
}

2. Register your project and add the google-services.json

To use Google Firebase you need to register your application and download the google-services.json.

Tracking interactions and crashes

Now that we added Firebase Crashlytics and Analytics to our project, we are able to get information about crashes and user interaction.

Crashes should now be tracked automatically and you can see an initialization message in logcat.

To track events you can now use analytics like so:

Firebase.analytics.logEvent(FirebaseAnalytics.Event.SELECT_ITEM)) {
    param(FirebaseAnalytics.Param.ITEM_ID, "product-1337")
    param(FirebaseAnalytics.Param.ITEM_NAME, "Cool product")
    param(FirebaseAnalytics.Param.CONTENT_TYPE, "product")
}

To not violate the law in some countries or just to show that we care about our users, we want to ensure that we only track events and crashes if the user allows us to. To do this we have to make some changes to our Analytics and Crashlytics implementations.

1. Set tracking disabled by default

Add following lines to your AndroidManifest.xml file to disable tracking by default.

<manifest ...>
    <application ... >

        <meta-data
            android:name="firebase_analytics_collection_enabled"
            android:value="false" />

        <meta-data
            android:name="firebase_crashlytics_collection_enabled"
            android:value="false" />

    </application>
</manifest>

To ask the user for consent, we can simply show them a dialog and take action on positive or negative button presses.

MaterialAlertDialogBuilder(this).apply {

    setTitle(R.string.title)
    setMessage(R.string.summary)

    setPositiveButton(android.R.string.yes) { dialogInterface: DialogInterface, _: Int ->

        //Enable tracking and crash collection
        Firebase.analytics.setAnalyticsCollectionEnabled(true)
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)

        dialogInterface.dismiss()
    }

    setNegativeButton(android.R.string.no) { dialogInterface: DialogInterface, _: Int ->

        //Disable tracking and crash collection
        Firebase.analytics.setAnalyticsCollectionEnabled(false)
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)

        dialogInterface.dismiss()
    }
}.create().show()

Wrap it up

Since we now know how to add, enable and disable tracking it is time for cleaning our code up and make things easier for us in the future. Since we might at one point want to change the tracking provider to get other features, better prices or better integrations with other systems, we should prepare for that case. We should not bloat our code with calls to Firebase at any point.

What we can do instead is to create a simple wrapper class, which hides the implementation details of Firebase. A simple implementation could look like this:

class AppTracker(
    private val resources: Resources,
    private val preferences: SharedPreferences,
    private val firebaseAnalytics: FirebaseAnalytics,
    private val crashlytics: FirebaseCrashlytics
) {

    private var isEnabled: Boolean = preferences.getBoolean(
        resources.getString(R.string.preference_tracking_enabled), false
    )

    fun setTrackingAndCrashReportsEnabled(enabled: Boolean) {
        isEnabled = enabled
        preferences.edit {
            putBoolean(resources.getString(R.string.preference_tracking_enabled), enabled)
        }
        firebaseAnalytics.setAnalyticsCollectionEnabled(enabled)
        crashlytics.setCrashlyticsCollectionEnabled(enabled)
    }

    fun setCurrentScreen(activity: Activity, screenName: String) {
        if (!isEnabled) return
        firebaseAnalytics.setCurrentScreen(activity, screenName, null)
    }

    fun trackEvent(eventType: String, eventName: String, eventParameter: Map<String, String>) {
        if (!isEnabled) return
        firebaseAnalytics.logEvent(eventType) {
            param(eventName, eventName)
            eventParameter.forEach { 
                param(it.key, it.value)
            }
        }
    }

    fun trackException(
        priority: Int,
        tag: String?,
        message: String,
        throwable: Throwable?
    ) {
        if (!isEnabled) return
        crashlytics.apply {
            setCustomKey(CRASHLYTICS_KEY_PRIORITY, priority)
            setCustomKey(CRASHLYTICS_KEY_TAG, tag ?: "")
            log(message)
            throwable?.let { recordException(it) }
        }
    }

    fun shouldAskForUserConsent(): Boolean {
        return !preferences.getBoolean(
            resources.getString(R.string.preference_tracking_enabled),
            false
        ) && !preferences.getBoolean(
            resources.getString(R.string.preference_tracking_enabled_do_not_show_dialog),
            false
        )
    }
}

Additional note

I use this pattern every time I need to have tracking or logging in an app, since it makes things easy as soon as you have to change the library you are using. Also, please take care of asking your users for consent before doing any tracking. Value their data as much as you value their money.

More posts