Aircon
Remote config management Android library powered by annotation processing and code generation.
Install / Use
/learn @ironSource/AirconREADME
AirCon
Control over the air
Remote config management Android library powered by annotation processing and code generation. https://medium.com/@hanan_rofe_haim/remote-config-in-android-one-release-to-rule-them-all-5ffa7750dec9
- Manage your app's remote configs using simple annotations, use generated providers classes to obtain config values.
- Supports adding any config source (FireBase support included in library).
- Supports custom validation and adaptation of config values.
- Inject remotely configured colors/Strings directly to XML layouts.
Usage
@Source(FireBaseConfigSource.class) // Can use any custom source instead of Firebase
@FeatureRemoteConfig
interface CoolFeature {
@BooleanConfig(defaultValue = false)
String ENABLED = "enabled";
@Mutable
@IntConfig(defaultValue = 1, maxValue = 3)
String MAX_TIMES_TO_SHOW = "maxTimeToShow";
@StringConfig(defaultValue = "Hello world")
String MESSAGE = "msg";
}
final boolean enabled = CoolFeatureConfigProvider.isEnabled();
final int maxTimesToShow = CoolFeatureConfigProvider.getMaxTimesToShow();
final String message = CoolFeatureConfigProvider.getMessage();
Initializing the SDK
The AirCon SDK should be initialized in the Application.onCreate() method using a AirConConfiguration object.
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
AirCon.get().init(new AirConConfiguration.Builder(this).setLoggingEnabled(BuildConfig.DEBUG)
.setJsonConverter(new GsonConverter())
.addConfigSource(new FireBaseConfigSource(this, FirebaseRemoteConfig.getInstance())
.build());
}
}
Supported config types
Primitives
@BooleanConfig@IntConfig@LongConfig@FloatConfig@StringConfig
Collection
@StringSetConfig
Json
@JsonConfig
Enums
@StringEnumConfig@IntEnumConfig
Special
@ColorConfig@TimeConfig@TextConfig@UrlConfig@HtmlConfig
Mutable configs
All configs are read-only by default.
If the custom source supports overriding local values (e.g the built-in Firebase config source) use the @Mutable annotation
on a config field to generate a setter for that config.
Remote config sources
AirCon supports any source of remote config.
For the common case of using FireBase as a remote config source, the library is packaged with a FireBaseConfigSource.
Config sources can either be added in the SDK init phase:
AirCon.get().init(new AirConConfiguration.Builder(this).addConfigSource(new FireBaseConfigSource(this, FirebaseRemoteConfig.getInstance())
Or at a later stage:
AirCon.get().getConfigSourceRepository().addSource(new FireBaseConfigSource(this, FirebaseRemoteConfig.getInstance());
Multiple remote config sources for the same app are supported.
Each config is a assigned a source using the @Source annotation.
Either define a source for the entire feature:
@Source(FireBaseConfigSource.class)
@FeatureRemoteConfig
interface CoolFeature {
@BooleanConfig(defaultValue = false)
String ENABLED = "enabled";
Or define a specific source for each config:
@Source(FireBaseConfigSource.class)
@StringConfig(defaultValue = "Hello world")
String MESSAGE = "msg";
If a source is defined on both the feature interface and the config field, the config field source will be used.
Default value
A default value for a config can be defined in several ways (only one way can be used for the same config):
- Define the
defaultValueattribute of the config annotation:@BooleanConfig(defaultValue = false) String ENABLED = "enabled"; - Define a resource as the default value:
@DefaultRes(R.string.title) @StringConfig String TITLE = "title"; - Define another config value as a default value:
@BooleanConfig(defaultValue = false) String ENABLED = "enabled"; @DefaultConfig(ENABLED) // If "flag" is not configured, the provider will fallback to the configured value of "enabled". @BooleanConfig String ANOTHER_FLAG = "flag"; - Define a custom default value provider method:
@BooleanConfig String ENABLED = "enabled"; @ConfigDefaultValueProvider(ENABLED) public static boolean getEnabledDefault() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; }
Custom value validation
Some config types provide inherit validation.
For example, the @UrlConfig verifies that the configured URL is a valid URL, otherwise the default value is returned.
If custom validation is needed for a config the then a validation predicate method can be defined:
@ConfigValidator(TITLE)
public static boolean isTitleValid(String title) {
return title.length <= 20; // If a title longer than 20 chars is configured, fallback to the default value.
}
Custom adapters
Some config types provide implementation for common adaptation needs.
For example, the @JsonConfig converts a remotely configured json string to an object.
In some cases extra processing is needed on the returned config value, which can be achieved be defining an adapter method:
@ConfigAdapter(ENABLED)
public static boolean processEnabled(boolean enabled) {
return enabled && BuildConfig.DEBUG; // Prevent feature from being enabled in production builds.
}
An adapter method can return a different type than the original config type:
@ConfigAdapter(TITLE)
public static Label processTitle(String title) {
return new Label(title);
}
Enum configs
A common use case is converting a remotely configured int/String to an Enum:
public enum TextLocation {
@RemoteIntValue(0) BOTTOM,
@RemoteIntValue(1) CENTER,
@RemoteIntValue(2) TOP
}
@IntEnumConfig(defaultValue = 2, enumClass = TextLocation.class)
String TEXT_LOCATION = "textLocation";
Usage:
TextLocation textLocation = CoolFeatureConfigProvider.getTextLocation();
@StringEnumConfig is also supported for converting strings to enum consts.
Config groups
Multiple configs can be groups together. A POJO class containing all the config values will be generated and can be passed around to methods.
Groups can be used in 2 ways:
- For every feature annotated with
@FeatureRemoteConfiga group containing all configs is generated. Usage:
final CoolFeatureConfig config = CoolFeatureConfigProvider.getAll();
- Creating a custom group and defining the contained configs. The value of the field defines the group name.
@ConfigGroup({MAX_TIMES_TO_SHOW, MESSAGE})
String MY_GROUP = "myGroup";
Usage:
MyGroupConfig myGroupConfig = CoolFeatureConfigProvider.getMyGroup();
Mock values
Mocking remotely configured values is useful for testing purposes. To mock a config value a mock method should be defined:
@ConfigMock(RemoteConfigs.CoolFeature.ENABLED)
public static boolean mockEnabled() {
return true;
}
A good practice for preventing mocks in release builds is defining all the config mock methods in the same class and ignore it in git. In addition, a custom lint check will throw an error if a config mock is defined in a release build.
Custom config types
AirCon supports defining custom config type annotations in cases where the built-in types are not sufficient or when common validation and processing is used across multiple config keys.
To define a custom config type:
- Define a config type resolver
public class LabelConfigResolver implements ConfigTypeResolver<LabelConfig, String, Label> {
@Override
public Class<LabelConfig> getAnnotationClass() {
return LabelConfig.class;
}
@Override
public boolean isValid(final LabelConfig annotation, final String value) {
final String[] invalidValues = annotation.invalidValues();
return !Arrays.asList(invalidValues).contains(value);
}
@Override
public Label process(final LabelConfig annotation, final String value) {
return Label.from(value);
}
}
The resolver defines:
- The custom config annotation (defined in step 2) to which it defines the processing logic.
- How a configured value is processed for the custom type.
- The primitive type of the config (i.e the type that will be used remotely).
- The processed type of the config, which can be whatever you like.
- Define a config type annotation and annotate it with
@ConfigTypeand bind it to the resolver you've created via the parameter to the annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@ConfigType(LabelConfigResolver.class)
public @interface LabelConfig {
String[] invalidValues() default {};
String defaultValue() default "";
}
The annotation can contain any number of attributes but must contain a defaultValue attribute.
Note that the annotation must be retained in runtime and be targeted only for fields.
- Register the custom config type when initializing the SDK
new AirConConfiguration.Builder(this).registerConfigType(new LabelConfigResolver())
- Use the custom config
@DefaultRes(R.string.app_name)
@LabelConfig(defaultValue = "", invalidValues = {"invalid1", "invalid2"})
String SOME_CUSTOM_LABEL = "someCustomLabel";
The new config type acts exactly the same as the build-in config annotations and therefore can be used in conjunction with all other AirCon features such as mocks, custom adapters, custom validators and
