Simple Runtime Feature Gating on Android

jameson

Jameson

Posted on October 29, 2022

Simple Runtime Feature Gating on Android

I recently saw a tweet that got me thinking:

The key point here is absolutely true. On the backend or frontend, you can deploy code whenever you want, all day every day. Your users will instantly get these updates.

That's not how it works with mobile apps. If you want to roll an updated version of your app, you'll at minimum need to upload the new version to Google or Apple, and await their review/acceptance.

If you work on a mobile app team, you're probably doing a weekly or maybe bi-weekly release. In this environment, it'll take even longer before you can get a fix out.

But even if you could update your app every time you make a change - should you? No. Each update you make is potentially an annoyance for the mobile app user, and a waste of their bandwidth if you're updating too frequently.


"Rollback" refers to pulling a defective deployment and going back to the last known working version. In the Google Play and Apple App stores, there is nothing exactly like this. So, the common approach to rollback involves re-publishing the content of an old release, but with a new, later version number. It is in fact a new release artifact, it just functions like an older one.

A better solution is runtime feature-gating. The idea of feature-gating is basically to put every big new change behind a toggle that you can control remotely from a server under your control. Whenever you make a change in your mobile app code, you can add a flag for it, which controls whether or not to use the new code.

Big companies generally have robust tooling for this - and it may be tied into a larger experimentation and analytics framework. But the basic concept is very easy to apply in your own DIY app; you don't need a lot of complexity to get most of the value. Let's look at how.


Let's suppose we've built a Halloween-themed screen for our app, and want to show it only on October 31st. There are a few ways we could achieve this:

  1. Hard code a date check on the client;
  2. Try to update the Play Store before and after halloween with new app versions;
  3. Use a runtime feature gate.

Option 1 is alright, but if something goes wrong - faulty date logic - we'll be celebrating Halloween longer than we wanted.

Option 2 assumes that our timing with the Play Store will work out perfectly, and also creates a lot of operational churn.

Option 3 looks really appealing. When Halloween comes, all we have to do is update a file on our backend (or saved to any public location like GitHub Pages, even.) If something goes wrong, we just update the file and no one knows any wiser. Either way, we can update the file at the end of the holiday.


To gate this change on the client with a runtime feature gate, let's take the following approach:

  1. Create a flat JSON file and make it available for download somewhere;
  2. When the client starts up, go download this file and check its contents;
  3. Check the feature enablement state before showing the user the Halloween-specific feature(s).

A simple incantation of this JSON file would look like this:

{
  "features": [
    {
      "name": "halloween_screen"
      "enabled": true
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now, on the client, let's whip together a little network client with Retrofit and KotlinX Serialization. This code will download this file and parse it into a data type.

interface RuntimeFeaturesService {
  @GET("features.json")
  suspend fun features(): Features

  companion object {
    fun instance(): RuntimeFeaturesService {
      return Retrofit.Builder()
        .baseUrl("https://raw.github.com/jameson/proj/")
        .addConverterFactory(
          Json.asConverterFactory(
            MediaType.get("application/json")
          )
        )
        .build()
        .create(RuntimeFeaturesService::class.java)
    }
  }

  @Serializable 
  data class FeatureList(features: List<Feature>) {
    @Serializable 
    data class Feature(name: String, enabled: Boolean)
  }
}
Enter fullscreen mode Exit fullscreen mode

Of course, you'll probably want to layer on some caching and a repository on top of this. But you can start using the new check immediately:

val halloweenFeaturesEnabled = 
  runtimeFeaturesService.features()
    .filter { f -> f.name == "halloween_screen" }
    .firstOrNull()
    ?.enabled ?: false

if (halloweenFeaturesEnabled) {
  binding.pumpkinImage.visibility = View.VISIBLE
} else {
  binding.pumpkinImage.visibility = View.GONE
}
Enter fullscreen mode Exit fullscreen mode

When it's time to get rid of the pumpkin image, just update the JSON:

{
  "features": [
    {
      "name": "halloween_screen"
      "enabled": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The example above is intentionally simplistic, and uses minimal third-party tooling to achieve its goals. You may quickly want to pivot towards a more robust solution like Firebase Remote Config.

💖 💪 🙅 🚩
jameson
Jameson

Posted on October 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related