Android DevTools Flags

A flag is a setting that allows the configuration of or gating of some logic or feature. It can almost be thought of as a simple constant but with some extra functionality that we will go over in this document.

The most common flags specify whether a feature should simply be enabled or disabled (useful for devs working on a new feature that's not ready to launch, for example). Flags can also be used to branch the internal behavior of a feature (e.g. applying a new but not thoroughly tested algorithm, or altering the UI to see if the new layout is more intuitive), while still leaving the old behavior in the code in case a quick fallback is necessary.

The goal of this library is to allow defining, enumerating, querying, and updating a collection of flags. At its simplest, it will allow developers to define all flags in a standard way in a central location, with an easy-to-read API.

However, in addition, it will also enable the easy configuration of flags via command-line arguments and configuration files. The system will even allow for running experiments, such as having 10% of random users running with a flag set to one value while the other 90% run with another. Just by defining flags with this system, these extra features are available with very little additional effort.

Defining flags

A codebase should determine a central location where all flags should live. There, it should create a class whose sole purpose is to contain static flags and nothing more.

For a concrete example, let‘s assume we’re going to make a game with several systems, two of which are: audio and graphics.

And these systems have a few features we‘ll want to configure: whether to use 3D audio, an initial resolution, and an FPS cap. For these, we’ll create the following flags: audio.3d, graphics.resolution, and graphics.fps.cap.

Note: Flag IDs are all lowercase and may only contain letters and numbers. Spaces are not allowed. Instead, use periods to indicate spacing, as in fps.cap above.

While the program will provide its own defaults for all flags, you might imagine configuring them via the command-line, for example:

java ... -Daudio.3d=false -Dgraphics.resolution=640x480

Flags Container Class

First, define the shell.

object GameFlags

Flags

Next, create a Flags instance, which you can think of as the owner of all flags.

object GameFlags {
   val FLAGS = Flags()
}

For these first examples, this class won't do anything; you simply have to define it since the next section requires it. However, for later, you can think of this class as the only mutable part of the flag API. Whereas everything else defines fixed values, Flags provides a mechanism for overriding them, allowing user customization and A/B testing experiments.

Flag Groups

FlagGroups allow each system to define their own scoped flags without worrying about conflicting with any other flags:

object GameFlags {
   val FLAGS = Flags()

   val AUDIO = FlagGroup(FLAGS, "audio", "Audio")
   val GRAPHICS = new FlagGroup(FLAGS, "graphics", "Graphics")
}

By specifying the group names as we did above, any flags created within these groups will look like audio.xxx and graphics.xxx. You also have to specify a display name for the group, which should be easy to understand if shown to users.

Flag

At last, let's specify some flags.

Flags support common primitive value types: Int, Long, Boolean, and String. Use the appropriate constructors to create them.

class GameFlags {
   ...

   val USE_3D_AUDIO = BooleanFlag(
      AUDIO, "3d", "Enable 3D audio", "... description ...", true)

   val RESOLUTION = StringFlag(
      GRAPHICS, "resolution", "Initial resolution", "... description ...",
      "1280x720")

   val FPS_CAP = IntFlag(
      GRAPHICS, "fps.cap", "FPS cap", "... description ...", 30)

   val MAX_HEAP_SIZE = LongFlag(
      MEMORY, "max.heap.size", "Max Heap Size in bytes", "... description ...",
      4_000_000_000L)
   ...
}

The name passed into create will be prefixed by the FlagGroup's name, e.g. 3d above becomes audio.3d.

And that's it! Now that these flags are defined, they can be used in code:

if (GameFlags.USE_3D_AUDIO.get()) {
   ... 3D audio logic ...
}

val resolution = Dimension.parse(GameFlags.RESOLUTION.get())

val currFps = update(...)
if (currFps > GameFlags.FPS_CAP.get()) {
   ... sleep ...
}

val maxHeapSize = GameFlags.MAX_HEAP_SIZE.get()
configureMemorySettings(..., maxHeapSize, ...)

Overriding Flags

A flag that cannot be overridden is no better than a constant. This section discusses the various ways you can override a flag.

FlagOverrides

The FlagOverrides class represents a collection of flag-to-value mappings, where if a value exists it should act as an override to its associated flag's default value.

A Flags instance will always contain a single, mutable FlagOverrides instance, plus 0 or more fallback instances. Since a mutable collection always exists, this ensures a user can always override any flag manually.

A concrete example can help, here. Say you have some values that are read in from the command line, others pulled down from a server, and finally others chosen by a user in some “Edit Settings” UI. In this example, the user's settings should be respected first, followed by the remote configuration, followed by the command-line values.

You would specify this by constructing Flags like:

val flags = Flags(userSettings, remoteSettings, commandLineSettings)

The first argument must always be the mutable collection, and it will always be checked first; the remaining arguments are checked in the order specified.

Note: If you do not explicitly pass in a mutable FlagOverrides instance as the first argument, Flags will automatically create one. The mutable FlagOverrides instance can be accessed via Flags.getOverrides().

Java System Properties

To use Java System properties as a source of flag overrides, use the provided PropertyOverrides class.

val FLAGS = Flags(PropertyOverrides())

This class reads in all System properties and treats them as potential flags overrides. Most system properties are just noise and will never be used, but if a property name matches the IDs of a flag, its value will be used as an override.

For example, if you start an application like so:

java ... -Dgraphics.fps.cap=60

then, even if you define a flag with a default value of 30:

val FPS_CAP = IntFlag(GRAPHICS, ..., 30)

get() will return the overridden value, 60:

assertThat(System.getProperty("graphics.fps.cap")).isEqualTo("60")
assertThat(FPS_CAP.get()).isEqualTo(60)

Flag API

Although you can technically override a flag directly through the FlagOverrides instance returned by Flags.getOverrides(), in practice, you often won't have access to the parent Flags class (which should be declared private).

The recommended way to override a flag's value is through the Flag.override method. Note that this call does not actually modify the flag itself but rather updates the mutable FlagOverrides collection in its parent Flags class for you. Convenient!

GameFlags.FPS_CAP.override(45)
// Same as: GameFlags.FLAGS.getOverrides().put("graphics.fps.cap", "45")

Besides being easier to read, this API has the additional advantage of ensuring type-safety. Flag.override won‘t let you set a flag’s override value to an incompatible String value by mistake, like this typo for example: FLAGS.getOverrides().put("graphics.fps.cap", "45'")

Note: Overriding a flag is not thread safe, so you must be careful if you are overriding flags in one thread while reading their values in another.

Serialization

If you‘d like to persist a user’s flag settings across multiple sessions, all you need to do is save the values in Flags.getOverrides() to disk on exiting and restore them on load.

There are many libraries and approaches on serializing data, but the skeleton of a simple example is provided for concreteness:

// On closing your application...
val flagValues = mutableListOf<String>()
val flags: List<Flag<*>> = ... // Use reflection to get all flags?
for (flag in flags) {
  val value = FLAGS.getUserOverrides().get(flag)
  if (value != null) {
    flagValues.add(String.format("%s=%s", flag.getId(), value))
  }
}
// Write flagValues to file
// On starting up your application...
val flagValuesLines: List<String> = ... // load from disk
val flagValues = mutableMapOf<String, String>()
for (line in flagValuesLines) {
    val split = line.split('=', 2)
    flagValues.put(split[0], split[1])
}

// Flags.getOverrides() will be empty on startup
val flags: List<Flag<*>> = ... // Use reflection to get all flags?
for (flag in flags) {
    val value = flagValues.get(flag.getId())
    if (value != null) {
       FLAGS.getUserOverrides().put(flag, value)
    }
}

More robust validation and error handling is left as an exercise to the reader.

You might also create your own custom FlagOverrides implementation class and pass that into your Flags constructor:

class PersistedOverrides implements FlagOverrides {
    val overrides = mutableMapOf<String, String>()
    fun PersistedOverrides() = deserialize()
    fun serialize() { ... }
    fun deserialize() { ... }
}
...
val FLAGS = Flags(PersistedOverrides(), ...)