Inject the experiment in Android

This article will focus on how you can use injection with experiments, for example I am using Firebase A/B testing platform with Dagger for Android development, but you have many more tools that you can use.

I am a big fan of experiments, you can experiment everything, start from simple ui tweaks and continue with algorithms in your code. You can read more about the setup in my previous article.

Experiments, this is the way to know things are working

Experiments, this is the way to know things are working, When you’re working on your project, and you want to add new functionality or modify existing one, It is great to check that you are not harming your app, I prefer to create new modules and inject the logic into my code, so how it could be done?

The simplest way would be to create simple module, that will get the experiment allocation, and select the implementation variant that you want in your experiment. For example:

@Provides INewCode provideNewCode(IRemoteConfig remoteConfig) {
if(remoteConfig.isExp(IRemoteConfig.EXPERIMENT_CODE)){
return new NewImp();
}

return new OldImp();
}

But now there is a question, what will happen if your experiment allocation was not yet received? For the first time, when dagger creates the module, it will take the default value of the experiment. Only after the experiment allocation on next application start, you will get the real experiment. This situation is not perfect, but this is the way how dagger works.

So what can we do? Dagger have the option to use lazy injection. The experiment will be created on first call to .get() and cached, and the same instance will be returned on every next call. For example:

@Inject Lazy<INewCode> lazyNewCode;lazyNewCode.get().method();

Great! so now my injection will be created only after I really need it, but still we might have problem with the allocation, because we might call the module before allocation finished. We will end up having still the default value, so it is not completely solve the problem.

What we are looking is a solution that will give us the correct value of implementation even if the allocation changed in runtime, so how we can do it?
The answer is using supplier. In this approach we will use injection but we will check the allocation in the supplier and provide the implementation that we need.

First of all lets define supplier (Or you can use one from Guava):

interface Supplier<T> {
T get();
}

After that we will implement the supplier:

public class ExperimentSupplier implements Supplier<INewCode>{
IRemoteConfig remoteConfig;
public ExperimentSupplier(IRemoteConfig remoteConfig){
this.remoteConfig = remoteConfig;
}
@Override
public INewCode get(){
if(remoteConfig.isExp(IRemoteConfig.EXPERIMENT_CODE)){
return new NewImp();
}

return new OldImp();
}
}
// in Dagger module
@Provides
Supplier<INewCode> provideExperimentSupplier(IRemoteConfig remoteConfig) {
return new ExperimentSupplier(remoteConfig);
}
// usage
@Inject Supplier<INewCode> codeSupplier;
codeSupplier.get().method();

Now every time supplier’s `get()` is called, we will return a new instance each time, even when the allocation is not changed. We can improve our supplier implementation, by storing current variant implementation:

public class ExperimentSupplier implements Supplier<INewCode>{
IRemoteConfig remoteConfig;
boolean isNewImplementation;
INewCode implementation;
public ExperimentSupplier(IRemoteConfig remoteConfig){
this.remoteConfig = remoteConfig;
}
@Override
public INewCode get(){
// change supplier only if allocation is changed
boolean shouldBeNew =
remoteConfig.isExp(IRemoteConfig.EXPERIMENT_CODE);

if(isNewImplementation != shouldBeNew) {
implementation = shouldBeNew ?
new NewImp() : new OldImp();
isNewImplementation = shouldBeNew;
}
if (implementation == null) {
implementation = new OldImp();
}
return implementation;
}
}
// usage
@Inject ExperimentSupplier codeSupplier;
codeSupplier.get().method();

With this solution we will have the code in memory only when we need it and the correct experiment implementation by the allocation.

I want to thanks Andrzej Polatyński for helping with the idea of using suppliers.

Head of engineering @ Omise