Simple Runtime Feature Gating on Android
Jameson
Posted on October 29, 2022
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:
- Hard code a date check on the client;
- Try to update the Play Store before and after halloween with new app versions;
- 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:
- Create a flat JSON file and make it available for download somewhere;
- When the client starts up, go download this file and check its contents;
- 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
}
]
}
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)
}
}
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
}
When it's time to get rid of the pumpkin image, just update the JSON:
{
"features": [
{
"name": "halloween_screen"
"enabled": false
}
]
}
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.
Posted on October 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.