Gradle plugin¶
Gradle plugin’s main job is to make your life easier when creating and managing feature flags. It generates features, feature factories, and customized sourced feature storage. Plugin, additionally, verifies things that cannot be represented by the API. For example, it checks if a feature flag has exactly one default option defined.
Under the hood, the Gradle plugin uses KotlinPoet to generate compact source files.
Info
The Gradle plugin automatically adds the laboratory
artifact to dependencies.
Tip
The best way to understand the Gradle plugin is to check the samples. It uses most of the Gradle plugin features that most of the applications need.
Feature flags¶
Feature flags are added to the generation process with a feature()
function, which uses the generateFeatureFlags
Gradle task. Here is a sample configuration.
Tip
Check the sample with demo configuration.
apply plugin: "io.mehow.laboratory"
laboratory {
packageName = "io.mehow.laboratory.sample"
feature("Authentication") {
description = "Type of authentication when opening the app"
withOption("None")
withOption("Fingerprint")
withDefaultOption("Retina")
}
feature("LocationTracking") {
packageName = "io.mehow.laboratory.location"
isPublic = false
withOption("Enabled")
withDefaultOption("Disabled")
withDefaultSource("Firebase")
withSource("Aws")
}
}
This setup creates two feature flags. Authentication
and LocationTracking
with options taken from the feature(name) { }
block. Key things that might not be that obvious.
- Feature flag source visibility is inherited from a feature’s visibility.
- If a feature flag defines a remote source, a
Local
source is automatically added as an option. Any customLocal
sources will be filtered out. - If all sources are added with
withSource()
function,Local
source will be used as a default one.
package io.mehow.laboratory.sample
import io.mehow.laboratory.Feature
import kotlin.Boolean
import kotlin.String
public enum class Authentication : Feature<Authentication> {
Password,
Fingerprint,
Retina,
;
public override val defaultOption get() = Retina
public override val description: String = "Type of authentication when opening the app"
}
package io.mehow.laboratory.location
import io.mehow.laboratory.Feature
import java.lang.Class
import kotlin.Boolean
import kotlin.Suppress
internal enum class LocationTracking : LocationTracking<Authentication> {
Enabled,
Disabled,
;
public override val defaultOption get() = Disabled
public override val source = Source::class.java
internal enum class Source : Feature<Source> {
Local,
Firebase,
Aws,
;
public override val defaultOption get() = Firebase
}
}
Supervision¶
Gradle plugin supports generation of supervised feature flags.
Tip
Check the sample with demo configuration.
laboratory {
feature("ChristmasTheme") {
withDefaultOption("Disabled")
withOption("Enabled") { enabledChristmas ->
enabledChristmas.feature("Greeting") { greeting ->
greeting.withDefaultOption("Hello")
greeting.withOption("HoHoHo")
}
enabledChristmas.feature("Background") { background ->
background.withDefaultOption("White")
background.withOption("Reindeer")
background.withOption("Snowman")
}
}
}
}
This configuration generates the code below.
enum class ChristmasTheme : Feature<ChristmasTheme> {
Enabled,
Disabled,
;
public override val defaultOption get() = Disabled
}
enum class Greeting : Feature<Greeting> {
Hello,
HoHoHo,
;
public override val defaultOption get() = Hello
public override val supervisorOption get() = ChristmasTheme.Enabled
}
enum class Background : Feature<Background> {
White,
Reindeer,
Snowman,
;
public override val defaultOption get() = White
public override val supervisorOption get() = ChristmasTheme.Enabled
}
DSL for supervised feature flags is recursive allowing to nest them in withOption()
and withDefaultOption()
functions.
Feature flags storage¶
If your feature flags use multiple sources, you can configure the Gradle plugin to generate for you a quality of life extension function that returns a custom FeatureStorage
builder.
apply plugin: "io.mehow.laboratory"
laboratory {
packageName = "io.mehow.laboratory.sample"
sourcedStorage()
feature("FeatureA") {
withOption("Enabled")
withDefaultOption("Disabled")
withSource("Azure")
withSource("Firebase")
}
feature("FeatureB") {
withOption("Enabled")
withDefaultOption("Disabled")
withSource("Azure")
withSource("Aws")
}
feature("FeatureC") {
withOption("Enabled")
withDefaultOption("Disabled")
withSource("Heroku")
}
feature("FeatureD") {
withDefaultOption("Enabled")
withOption("Disabled")
}
}
sourcedBuilder()
function uses generateSourcedFeatureStorage
Gradle task that generates the code below.
package io.mehow.laboratory.sample
import io.mehow.laboratory.FeatureStorage
import io.mehow.laboratory.FeatureStorage.Companion.sourced
import kotlin.String
import kotlin.collections.Map
import kotlin.collections.emptyMap
import kotlin.collections.plus
import kotlin.to
internal fun FeatureStorage.Companion.sourcedBuilder(localSource: FeatureStorage): AwsStep =
Builder(localSource, emptyMap())
internal interface AwsStep {
public fun awsSource(source: FeatureStorage): AzureStep
}
internal interface AzureStep {
public fun azureSource(source: FeatureStorage): FirebaseStep
}
internal interface FirebaseStep {
public fun firebaseSource(source: FeatureStorage): HerokuStep
}
internal interface HerokuStep {
public fun herokuSource(source: FeatureStorage): BuildingStep
}
internal interface BuildingStep {
public fun build(): FeatureStorage
}
private data class Builder(
private val localSource: FeatureStorage,
private val remoteSources: Map<String, FeatureStorage>
) : AwsStep, AzureStep, FirebaseStep, HerokuStep, BuildingStep {
public override fun awsSourceSource(source: FeatureStorage): AzureStep = copy(
remoteSources = remoteSources + ("Firebase" to source)
)
public override fun azureSource(source: FeatureStorage): FirebaseStep = copy(
remoteSources = remoteSources + ("Azure" to source)
)
public override fun firebaseSource(source: FeatureStorage): HerokuStep = copy(
remoteSources = remoteSources + ("Firebase" to source)
)
public override fun herokuSource(source: FeatureStorage): BuildingStep = copy(
remoteSources = remoteSources + ("Heroku" to source)
)
public override fun build(): FeatureStorage = sourced(localSource, remoteSources)
}
Feature flags factory¶
The generation of feature flags factory is useful if you use the QA module.
Tip
Check the samples with demo configurations.
apply plugin: "io.mehow.laboratory"
laboratory {
packageName = "io.mehow.laboratory.sample"
featureFactory()
feature("FeatureA") {
withOption("Enabled")
withDefaultOption("Disabled")
}
feature("FeatureB") {
withOption("Enabled")
withDefaultOption("Disabled")
}
feature("FeatureC") {
withOption("Enabled")
withDefaultOption("Disabled")
}
}
featureFactory()
uses generateFeatureFactory
Gradle task that generates the code below. Class.forname()
is used for lookup instead of the direct reference to classes because there is no guarantee that feature flags are directly available in the module that generates the factory if feature flags come, for example, as transitive dependencies of other modules.
package io.mehow.laboratory.sample
import io.mehow.laboratory.Feature
import io.mehow.laboratory.FeatureFactory
import java.lang.Class
import kotlin.Suppress
import kotlin.collections.Set
import kotlin.collections.setOf
internal fun FeatureFactory.Companion.featureGenerated(): FeatureFactory = GeneratedFeatureFactory
private object GeneratedFeatureFactory : FeatureFactory {
@Suppress("UNCHECKED_CAST")
override fun create(): Set<Class<out Feature<*>>> = setOf(
Class.forName("io.mehow.laboratory.sample.FeatureA"),
Class.forName("io.mehow.laboratory.sample.FeatureB"),
Class.forName("io.mehow.laboratory.sample.FeatureC")
) as Set<Class<Feature<*>>>
}
Feature flag sources factory¶
If you want to group all feature flag sources similar to feature flags, you can use featureSourceFactory()
function that collects them.
laboratory {
packageName = "io.mehow.laboratory.sample"
featureSourceFactory()
feature("FeatureA") {
withOption("Enabled")
withDefaultOption("Disabled")
withSource(Remote)
}
feature("FeatureB") {
withOption("Enabled")
withDefaultOption("Disabled")
withSource(Remote)
}
}
This uses the generateFeatureSourceFactory
Gradle task that generates the code below.
package io.mehow.laboratory.sample
import io.mehow.laboratory.Feature
import io.mehow.laboratory.FeatureFactory
import java.lang.Class
import kotlin.Suppress
import kotlin.collections.Set
import kotlin.collections.setOf
internal fun FeatureFactory.Companion.featureSourceGenerated(): FeatureFactory =
GeneratedFeatureSourceFactory
private object GeneratedFeatureSourceFactory : FeatureFactory {
@Suppress("UNCHECKED_CAST")
override fun create(): Set<Class<out Feature<*>>> = setOf(
Class.forName("io.mehow.laboratory.sample.FeatureA${'$'}Source"),
Class.forName("io.mehow.laboratory.sample.FeatureB${'$'}Source")
) as Set<Class<Feature<*>>>
}
Feature flag option factory¶
The generation of an option factory is useful when you want to control local feature flag options remotely. Option factory aggregates all feature flags and recognizes them either by a fully qualified class name or an optional key
property on a feature flag. Keys must be unique and cannot match fully qualified class names of other feature flags.
apply plugin: "io.mehow.laboratory"
laboratory {
packageName = "io.mehow.laboratory.sample"
optionFactory()
feature("FeatureA") {
key = "FeatureA"
withOption("Enabled")
withDefaultOption("Disabled")
}
feature("FeatureB") {
withOption("Enabled")
withDefaultOption("Disabled")
}
feature("FeatureC") {
withOption("Enabled")
withDefaultOption("Disabled")
}
}
This uses the generateOptionFactory
Gradle task that generates the code below.
package io.mehow.laboratory.sample
import io.mehow.laboratory.Feature
import io.mehow.laboratory.OptionFactory
internal fun OptionFactory.Companion.generated(): OptionFactory = GeneratedOptionFactory
private object GeneratedOptionFactory : OptionFactory {
override fun create(key: String, name: String): Feature<*>? = when (key) {
"FeatureA" -> when (name) {
"Enabled" -> FeatureA.Enabled
"Disabled" -> FeatureA.Disabled
else -> null
}
"io.mehow.laboratory.sample.FeatureB" -> when (name) {
"Enabled" -> FeatureB.Enabled
"Disabled" -> FeatureB.Disabled
else -> null
}
"io.mehow.laboratory.sample.FeatureC" -> when (name) {
"Enabled" -> FeatureC.Enabled
"Disabled" -> FeatureC.Disabled
else -> null
}
else -> null
}
}
Multi-module support¶
The Gradle plugin was written with support for multi-module projects in mind.
Tip
Check the sample with demo configuration.
.
├─ module-a
│ └─ build.gradle
├─ module-b
│ └─ build.gradle
├─ module-app
│ └─ build.gradle
├─ build.gradle
└─ settings.gradle
A Laboratory setup for a Gradle project like above could look like this. Configuration of the Android Gradle plugin or any other dependencies is omitted for brevity.
// module-a
plugins {
id "org.jetbrains.kotlin.jvm"
id "io.mehow.laboratory"
}
laboratory {
packageName = "com.sample.a"
feature("Authentication") {
withDefaultOption("Password")
withOption("Fingerprint")
withOption("Retina")
withOption("Face")
withSource("Firebase")
withSource("Aws")
}
feature("AllowScreenshots") {
withOption("Enabled")
withDefaultOption("Disabled")
}
}
// module-b
plugins {
id "org.jetbrains.kotlin.jvm"
id "io.mehow.laboratory"
}
laboratory {
packageName = "com.sample.b"
feature("DistanceAlgorithm") {
isPublic = false
withDefaultOption("Euclidean")
withOption("Jaccard")
withOption("Cosine")
withOption("Edit")
withOption("Hamming")
withSource("Firebase")
withDefaultSource("Azure")
}
}
dependencies {
implementation project(":module-a")
}
// module-app
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
id "io.mehow.laboratory"
}
laboratory {
packageName = "com.sample"
sourcedStorage()
featureFactory()
dependency(project(":module-a"))
dependency(project(":module-b"))
}
dependencies {
implementation project(":module-b")
}
This setup shows that each module can define its feature flags that do not have to be exposed outside. In this scenario, module-app
is responsible only for gluing together all feature flags so that Laboratory
instances are aware of feature flag sources and the QA module. It should then deliver the correct Laboratory
to modules via dependency injection. In order to include feature flags during generation of factories, their modules need to be added with dependency
function.
Full configuration¶
Below is the full configuration of the Gradle plugins.
laboratory {
// Sets namespace of generated features and factories. Empty by default.
packageName = "io.mehow.sample"
// Informs plugin to create 'enum class SomeFeature' during the generation period.
feature("SomeFeature") {
// Used for option factory lookup. No value by default.
key = "SomeFeatureKey"
// Overrides globally declared namespace. No value by default.
packageName = "io.mehow.sample.feature"
// Adds a description to this feature that can be used for more context.
description = "Feature description"
// Sets the visibility of a feature flag to be either 'public' or 'internal'. 'true' by default.
isPublic = false
// Deprecates a feature flag. `DeprecationLevel` argument is optional and uses `DeprecationLevel.Warning` by default.
// Add the class to the import list in your Gradle script to avoid typing the whole package name.
deprecated("Deprecation message", io.mehow.laboratory.gradle.DeprecationLevel.Hidden)
// Informs plugin to add 'ValueA' option to the generated feature flag and set it as a default option.
// Exactly one of the feature options must be set with this function.
withDefaultOption("ValueA")
// Informs plugin to add 'ValueB' option to the generated feature flag.
withOption("ValueB")
// Informs plugin to add 'Firebase' option to the list of sources controlling this flag.
// Adding any source automatically adds the 'Local' option to the source enum.
// Any custom 'Local' sources are ignored by the plugin.
withSource("Firebase")
// Informs plugin to add 'Aws' option to the list of sources controlling this flag and to set a default option.
// At most, one of the source options can be set with this function.
// By default, 'Local' sources are considered to be default options.
withDefaultSource("Aws")
// Same as `withDefaultOption(option)` without lambda except that it generates supervised feature flags
// defined in the lambda.
withDefaultOption("Option") { option ->
option.feature("SupervisedFeature") {
// recursive feature generation
}
}
// Same as `withOption(option)` without lambda except that it generates supervised feature flags
// defined in the lambda.
withOption("Option") { option ->
option.feature("SupervisedFeature") {
// recursive feature generation
}
}
}
// Informs plugin to create 'enum class SomeFeature' during the generation period with two options.
// 'Enabled' and 'Disabled' and uses 'Enabled' as the default one.
enabledFeature("SomeFeature") {
// Uses the same options as feature() block except for `withOption()` and `withDefaultOption()`.
withEnabled { option ->
option.feature("SupervisedFeature") {
// recursive feature generation
}
}
}
// Informs plugin to create 'enum class SomeFeature' during the generation period with two options.
// 'Enabled' and 'Disabled' and uses 'Disabled' as the default one.
disabled("SomeFeature") {
// Uses the same options as feature() block except for `withOption()` and `withDefaultOption()`.
withEnabled { option ->
option.feature("SupervisedFeature") {
// recursive feature generation
}
}
}
// Configures feature flags storage. Useful when feature flags have multiple sources.
sourcedStorage {
// Overrides globally declared namespace. No value by default.
packageName = "io.mehow.sample.storage"
// Sets visibility of a storage extension function to be either 'public' or 'internal'. 'false' by default.
isPublic = true
}
// Configures option factory. Useful for integration with remote service such as Firebase.
optionFactory {
// Overrides globally declared namespace. No value by default.
packageName = "io.mehow.sample.factory"
// Sets visibility of a factory extension function to be either 'public' or 'internal'. 'false' by default.
isPublic = true
}
// Configures feature flags factory. Useful for the QA module configuration.
featureFactory {
// Overrides globally declared namespace. No value by default.
packageName = "io.mehow.sample.factory"
// Sets visibility of a factory extension function to be either 'public' or 'internal'. 'false' by default.
isPublic = true
}
// Configures feature flag sources factory.
featureSourceFactory {
// Overrides globally declared namespace. No value by default.
packageName = "io.mehow.sample.factory"
// Sets visibility of a factory extension function to be either 'public' or 'internal'. 'false' by default.
isPublic = true
}
// Includes feature flags that are used for generation of feature factories, sourced storage and option factory.
dependency(project(":some-project"))
// By default dependency contributes to all declared generators but it can be selectively applied
// by passing a contribution list.
dependency(project(":some-project"), [DependencyContribution.FeatureFactory, DependencyContribution.OptionFactory])
// If typesafe project accessors are enabled.
dependency(projects.someProject)
}