Posted by: Hadi Lashkari Ghouchani
This article focus on Android projects, but the idea can be applied on other kind of projects too. To start, let’s explain all these concepts then try to figure out their relationship.
Feature Flags
Feature flags are just some if
, when
conditions, or basically some logic in the code base to turn on/off some features. There are multiple reasons that we need to turn off a feature.
- The feature is a work in progress, so it’s not ready to be enabled.
- The business is not ready to ship the feature or the feature got expired, so no need to be enabled.
- Some kind of A/B test is going on so we need to turn the feature on only for a segment of users.
There can be more reasons. Below, we’ll actually add multiple new reasons to this list.
The important aspect of a feature flag is the ability to separate the feature from the other parts of the code. But hey, this is exactly why we create Gradle modules. To follow Single Responsibility Principle, SRP, we often make a new module for each feature, that makes them separated from the rest of the code. If we apply the Dependency Inversion Principle, DIP, to our modules, then we have one :api
module to put the interfaces of the feature there and one :impl
module to put their implementation. This kind of setup will let us have a flat dependency graph, which implies fast iterations. To do so, we need to have a dependency injection framework, DIframework, to inject the implementation in the root module, aka :app
module. This is what we all do to have a better life, right?
Anyway, if you need a sample code, I have created one, however, I invite you to read to the end then checkout the sample code, because we’re not here to just revive old stuff!
In this article, we assume we follow above principles/practices so feature flags are not hard to apply. We just need to turn on/off a Gradle module in runtime! The detail is below!
Build Variants
In Android, build variants and flavors are some kind of compile time flags to change the configuration of the project. In most cases, where we don’t want to change the Android resources, or package name, etc., changing the configuration means using a new implementation of an interface, if we have followed the DIP in our code base. Someone can put those implementations in different Gradle modules, for the sake of SRP, then changing build variants would translate to turning on/off some Gradle modules in compile time. The build variants and flavors, in most cases, are the same as feature flags, but in compile time.
To emphasis the importance of having compile time feature flags, just take a look at Dynamic Feature. Someone can leverage this method for Dynamic Features too, but here we don’t have a sample to talk about, so maybe another time.
The api/impl
practice can make the iteration on a single task fast by invalidating and rebuilding only the :impl
module, which the developer is working on, and the root module. Unfortunately the overall build time, by that I mean ./gradlew clean build --no-build-cache
, is still slowing down as the project scales up. But assume we could turn off some Gradle modules in compile time, then overall build could be short even in huge projects. It must be in-demand.
Sample Apps
To have fast iterations on development and also represent a feature, which is a collection of multiple Gradle modules, we have sample apps. Sample apps are dedicated com.android.application
modules as root modules of small apps. These dedicated small apps, which spread out all over the code base, need to take care of to keep working. But if we could turn on/off Gradle modules in compile time, then we can have one root module in a monorepo code base to generate all kind of variants, flavors, sample apps, etc. just by switching on/off different Gradle modules. Good news is that we can do that.
Merging Concepts
Before starting to switch on/off :impl
modules in compile time, we notice we need some logic to define the behavior of the app in runtime, while a module is missing since compile time. At first, it looks like we need to implement a new set of logic to handle that, but soon we realize that if we prepared the app for feature flags, which can make a module missed in runtime, then we can use their logic. It doesn’t matter when the module is missed, runtime or compile time, it just matter to handle its unavailability.
In this way, we actually merge the concept of build variant, flavors, etc., that’s happening in compile time, with the concept of feature flags. So we can say we have runtime feature flags, as we had before, and compile time feature flags, where both of them have the same implementation for runtime logic.
Implementation
For feature flags we need to check the availability of a Gradle module in runtime, which translates to availability of the implementation of a feature. So there’s no turn on/off for the :api
modules, we just make the :impl
modules available or not-available, so the code that needs that particular module can establish the feature flags logic based on the availability of the :impl
module of that particular feature. Ideally, the feature flags’ logic should be agnostic about why the module is not available, it’s missed since compile time or just a runtime/remote feature flag.
Job Offers
To inject the implementation and replace it with the interfaces of :api
, we can have a reactive solution. We need to ask for availability of the implementation and the feature module should return the result of that. The result must be matched up with request, which needs more unrelated implementation. Here we need an external library to support request-result pairs of messages. To achieve that along with developing this method, we developed CommandKU
idea, which is available in https://github.com/hadilq/CommandKU. So problem solved. We can implement the feature flag’s logic based on availability of the implementation.
Inversion Of Control
Here we need each module be able to install its implementation to the :app
module, if they are in the classpath, because we don’t want to change the the code of :app
module every time we switch on/off an :impl
module. It is another way to say the DI framework needs to support Inversion of Control, IoC. IoC is supper cool. I wanted to write an article about it but Iron Man Suit can explain it better. You know Iron Man, right? The Marvel’s Batman! Take a look at Mark XLII [42] suit up in this video.
As you can see Mark XLII [42] suit have modules like hand, leg, etc. and they can install themselves on Iron Man. That’s IoC. Cool right?
Good news is we have Anvil to apply IoC on our Dagger components and modules. With this technology we can gather all implementations, in the classpath, of an interface we called this interface CommandHook
in the sample code, where are related to all plugable modules. Plugable modules are the Gradle modules that can be switch on/off. By using the set of those implementations, we can call and ask each module to install itself in the reactive channel we provide on the onCreate
of Android Application
. Check out the code if you have trouble with my long detailed sentences! I have trouble with them too!
Up to here
- We can pass the implementation reactively.
- Modules can install themselves on compile time if they are in the classpath.
But how we can switch on/off the :impl
modules. Maybe by putting some if
on the implementation(...)
methods in the build.gradle
file of the :app
module. No!
Scenarios
A scenario is a recipe to switch on/off a list of :impl
modules, we call this list the dependency list. To make switching modules a scalable solution, we assume the number of scenarios are a lot. Which makes sense, because we have replaced variants, flavors, and sample apps, etc. with these scenarios. To handle a long list of these scenarios we can arrange them in a tree in a way that the dependency list of each node has all :impl
modules of dependency list of its parent. The following is the example in the sample code. For instance, in the sample code we can choose
scenario=>guidomia>database
by defining it in the local.properties
. Did you notice >
is the delimiter for tree structure of scenarios? Or choose
scenario=>
which is the root scenario that have no :impl
module in its dependency list. The other defined scenarios in there is
scenario=>guidomia
Which is a feature module without database. Also there is a default scenario there, for whenever the developer doesn’t provide a scenario in local.properties
. With this mechanism,
- We can play with all different kind of setups we need while we are developing.
- We can have some scenarios for sample apps to present a demo, etc.
- We can release different apps from a single
:app
module. - etc.
You can find the sample code in my sample repository: https://github.com/hadilq/CleanArchitecture.
Conclusion
Even that there are more work to do, but achieving compile-time feature flags is possible, thanks to Anvil and its support for IoC.
References
- Single Responsibility Principle
- Dependency Inversion Principle
- Dynamic Feature
- Dagger
- Inversion of Control
- Anvil
Thanks to Andy Dyer.