
Type-safe, composable feature-flag system with priority-based sources (memory, YAML, custom), contextual rule evaluation, reactive change observation, caching, and lifecycle/error handling for production use.
Toggle is a modern, type-safe feature flag library designed for Kotlin Multiplatform. It provides a clean, composable API for managing feature flags across Android, iOS, JVM.
What makes Toggle different:
Toggle follows a layered architecture where multiple sources can provide feature flags, and custom rule evaluators determine the final enabled state based on context.
Toggle follows a layered architecture with clear separation of concerns:
flowchart TB
Toggle[Toggle]
Toggle --> Cache[CachingFeatureResolver]
Cache --> Resolver[FeatureResolver]
Resolver --> Source[(FeatureSource)]
Resolver --> Evaluator[RuleEvaluator]
Source --> Memory[Memory Source]
Source --> YAML[YAML Source]
Source --> Remote[Remote Source]
Evaluator --> Context[ToggleContext]
Source --> Flag[FeatureFlag]
Context -.->|attributes| Evaluator
style Toggle fill:#4a90e2,color:#fffAdd the dependency to your build.gradle.kts:
// For Kotlin Multiplatform
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.behzodhalil:toggle-core:0.1.0")
}
}
}
}
// For Jetpack Compose support
dependencies {
implementation("io.github.behzodhalil:toggle-compose:0.1.0")
}// Android/JVM
dependencies {
implementation 'io.github.behzodhalil:toggle-core:0.1.0'
implementation 'io.github.behzodhalil:toggle-compose:0.1.0'
}Toggle uses a builder pattern for configuration. The core concept revolves around three main components:
val toggle = Toggle {
// Configure feature sources
sources {
memory {
priority = 200 // Highest priority
feature(Features.DEBUG_MODE, true)
}
yaml {
priority = 100
resourcePath = "features.yaml"
}
}
// Set evaluation context
context {
userId("user_123")
attribute("country", "US")
attribute("appVersion", "2.0.0")
}
// Add custom evaluators
evaluation {
percentageRollout(50) // 50% rollout
}
// Enable debug logging
debug()
}Sources provide feature flag values. Toggle queries sources in descending priority order and uses the first non-null result.
In-memory feature storage, ideal for runtime overrides and testing.
Toggle {
sources {
memory {
priority = 200
// Add features
feature(Features.DARK_MODE, enabled = true)
feature(Features.PREMIUM, enabled = false, metadata = mapOf(
"tier" to "gold",
"trial_days" to "30"
))
}
}
}Use cases:
Load features from YAML files in your resources.
Toggle {
sources {
yaml {
priority = 100
resourcePath = "features.yaml"
// or: content = yamlString
}
}
}features.yaml:
features:
dark_mode:
enabled: true
metadata:
owner: "design-team"
description: "Dark theme support"
premium_feature:
enabled: false
metadata:
tier: "gold"
min_version: "2.0.0"
beta_ui:
enabled: true
metadata:
rollout_percentage: "50"
experiment_id: "exp_123"Use cases:
Implement the FeatureSource interface for custom backends:
class RemoteConfigSource(
private val remoteConfig: FirebaseRemoteConfig
) : FeatureSource {
override val priority: Int = 150
override val sourceName: String = "remote_config"
override suspend fun refresh() {
remoteConfig.fetchAndActivate().await()
}
override fun get(key: String): FeatureFlag? {
val enabled = remoteConfig.getBoolean(key)
return FeatureFlag(
key = key,
enabled = enabled,
source = sourceName
)
}
override fun getAll(): List<FeatureFlag> = emptyList()
override fun close() {}
}
// Use it:
Toggle {
sources {
source(RemoteConfigSource(firebaseRemoteConfig))
memory { priority = 200 } // Fallback
}
}Use cases:
Sources are queried in descending priority order:
Toggle {
sources {
memory { priority = 200 } // Checked first
remote { priority = 150 } // Checked second
yaml { priority = 100 } // Checked third (fallback)
}
}Common priority patterns:
Context provides attributes for targeting rules. Use context to enable features for specific users, regions, or app versions.
Toggle {
context {
// User attributes
userId("user_123")
attribute("email", "user@example.com")
attribute("subscription", "premium")
// Environment
attribute("country", "US")
attribute("language", "en")
attribute("timezone", "America/New_York")
// Application
attribute("appVersion", "2.1.0")
attribute("platform", "Android")
attribute("deviceType", "tablet")
// Custom
attribute("experimentGroup", "variant_a")
attribute("accountAge", 365)
}
}val context = toggle.context
val userId = context.get("userId")
val country = context.get("country")Evaluators contain the logic for determining if a feature should be enabled based on context.
Gradually roll out features to a percentage of users:
class PercentageRolloutEvaluator(
private val percentage: Int
) : RuleEvaluator {
override fun evaluate(flag: FeatureFlag, context: ToggleContext): Boolean {
if (!flag.enabled) return false
val userId = context.get("userId")?.toString() ?: return false
val hash = (userId.hashCode() and Int.MAX_VALUE) % 100
return hash < percentage
}
}
// Usage:
Toggle {
sources {
yaml { resourcePath = "features.yaml" }
}
evaluation {
rule(PercentageRolloutEvaluator(percentage = 25)) // 25% rollout
}
}Toggle supports reactive feature observation using Kotlin Flow. This is particularly useful for UI updates when features change.
val toggle = Toggle {
sources {
memory {
feature(Features.DARK_MODE, true)
}
}
}
val observableToggle = ObservableToggle {
this.toggle = toggle
bufferCapacity = 64 // Flow buffer size
}
// Observe a feature
observableToggle.observe(Features.DARK_MODE)
.onEach { enabled ->
println("Dark mode is now: $enabled")
}
.launchIn(scope)// Update feature in memory source
memorySource.setFeature(Features.DARK_MODE, false)
// Notify observers
observableToggle.notifyChanged(Features.DARK_MODE)Toggle provides first-class support for Jetpack Compose with reactive state observation.
@Composable
fun FeatureToggleExample() {
val toggle = rememberToggle {
sources {
memory {
feature(Features.DARK_MODE, true)
feature(Features.BETA_UI, false)
}
}
}
val observable = rememberObservableToggle(toggle)
// Observe features
val isDarkMode by observable.observeAsState(Features.DARK_MODE)
val isBetaUI by observable.observeAsState(Features.BETA_UI)
Column {
Text("Dark Mode: ${if (isDarkMode) "ON" else "OFF"}")
Text("Beta UI: ${if (isBetaUI) "ON" else "OFF"}")
}
}// At app level
@Composable
fun App() {
val toggle = rememberToggle {
sources {
yaml { resourcePath = "features.yaml" }
}
}
val observable = rememberObservableToggle(toggle)
CompositionLocalProvider(
LocalToggle provides toggle,
LocalObservableToggle provides observable
) {
MainScreen()
}
}
// In child composables
@Composable
fun ChildScreen() {
val observable = LocalObservableToggle.current
val isPremium by observable.observeAsState(Features.PREMIUM)
if (isPremium) {
PremiumContent()
} else {
FreeContent()
}
}@Composable
fun ConditionalFeature() {
val observable = LocalObservableToggle.current
val showNewUI by observable.observeAsState(Features.NEW_UI)
if (showNewUI) {
NewUserInterface()
} else {
LegacyUserInterface()
}
}The observeAsState composable uses collectAsStateWithLifecycle, which means:
@Composable
fun LifecycleAwareFeature() {
val observable = LocalObservableToggle.current
// Automatically pauses/resumes with lifecycle
val isEnabled by observable.observeAsState(Features.EXPERIMENTAL)
// Use isEnabled...
}Copyright 2025 Behzod Halil
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Toggle is a modern, type-safe feature flag library designed for Kotlin Multiplatform. It provides a clean, composable API for managing feature flags across Android, iOS, JVM.
What makes Toggle different:
Toggle follows a layered architecture where multiple sources can provide feature flags, and custom rule evaluators determine the final enabled state based on context.
Toggle follows a layered architecture with clear separation of concerns:
flowchart TB
Toggle[Toggle]
Toggle --> Cache[CachingFeatureResolver]
Cache --> Resolver[FeatureResolver]
Resolver --> Source[(FeatureSource)]
Resolver --> Evaluator[RuleEvaluator]
Source --> Memory[Memory Source]
Source --> YAML[YAML Source]
Source --> Remote[Remote Source]
Evaluator --> Context[ToggleContext]
Source --> Flag[FeatureFlag]
Context -.->|attributes| Evaluator
style Toggle fill:#4a90e2,color:#fffAdd the dependency to your build.gradle.kts:
// For Kotlin Multiplatform
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.behzodhalil:toggle-core:0.1.0")
}
}
}
}
// For Jetpack Compose support
dependencies {
implementation("io.github.behzodhalil:toggle-compose:0.1.0")
}// Android/JVM
dependencies {
implementation 'io.github.behzodhalil:toggle-core:0.1.0'
implementation 'io.github.behzodhalil:toggle-compose:0.1.0'
}Toggle uses a builder pattern for configuration. The core concept revolves around three main components:
val toggle = Toggle {
// Configure feature sources
sources {
memory {
priority = 200 // Highest priority
feature(Features.DEBUG_MODE, true)
}
yaml {
priority = 100
resourcePath = "features.yaml"
}
}
// Set evaluation context
context {
userId("user_123")
attribute("country", "US")
attribute("appVersion", "2.0.0")
}
// Add custom evaluators
evaluation {
percentageRollout(50) // 50% rollout
}
// Enable debug logging
debug()
}Sources provide feature flag values. Toggle queries sources in descending priority order and uses the first non-null result.
In-memory feature storage, ideal for runtime overrides and testing.
Toggle {
sources {
memory {
priority = 200
// Add features
feature(Features.DARK_MODE, enabled = true)
feature(Features.PREMIUM, enabled = false, metadata = mapOf(
"tier" to "gold",
"trial_days" to "30"
))
}
}
}Use cases:
Load features from YAML files in your resources.
Toggle {
sources {
yaml {
priority = 100
resourcePath = "features.yaml"
// or: content = yamlString
}
}
}features.yaml:
features:
dark_mode:
enabled: true
metadata:
owner: "design-team"
description: "Dark theme support"
premium_feature:
enabled: false
metadata:
tier: "gold"
min_version: "2.0.0"
beta_ui:
enabled: true
metadata:
rollout_percentage: "50"
experiment_id: "exp_123"Use cases:
Implement the FeatureSource interface for custom backends:
class RemoteConfigSource(
private val remoteConfig: FirebaseRemoteConfig
) : FeatureSource {
override val priority: Int = 150
override val sourceName: String = "remote_config"
override suspend fun refresh() {
remoteConfig.fetchAndActivate().await()
}
override fun get(key: String): FeatureFlag? {
val enabled = remoteConfig.getBoolean(key)
return FeatureFlag(
key = key,
enabled = enabled,
source = sourceName
)
}
override fun getAll(): List<FeatureFlag> = emptyList()
override fun close() {}
}
// Use it:
Toggle {
sources {
source(RemoteConfigSource(firebaseRemoteConfig))
memory { priority = 200 } // Fallback
}
}Use cases:
Sources are queried in descending priority order:
Toggle {
sources {
memory { priority = 200 } // Checked first
remote { priority = 150 } // Checked second
yaml { priority = 100 } // Checked third (fallback)
}
}Common priority patterns:
Context provides attributes for targeting rules. Use context to enable features for specific users, regions, or app versions.
Toggle {
context {
// User attributes
userId("user_123")
attribute("email", "user@example.com")
attribute("subscription", "premium")
// Environment
attribute("country", "US")
attribute("language", "en")
attribute("timezone", "America/New_York")
// Application
attribute("appVersion", "2.1.0")
attribute("platform", "Android")
attribute("deviceType", "tablet")
// Custom
attribute("experimentGroup", "variant_a")
attribute("accountAge", 365)
}
}val context = toggle.context
val userId = context.get("userId")
val country = context.get("country")Evaluators contain the logic for determining if a feature should be enabled based on context.
Gradually roll out features to a percentage of users:
class PercentageRolloutEvaluator(
private val percentage: Int
) : RuleEvaluator {
override fun evaluate(flag: FeatureFlag, context: ToggleContext): Boolean {
if (!flag.enabled) return false
val userId = context.get("userId")?.toString() ?: return false
val hash = (userId.hashCode() and Int.MAX_VALUE) % 100
return hash < percentage
}
}
// Usage:
Toggle {
sources {
yaml { resourcePath = "features.yaml" }
}
evaluation {
rule(PercentageRolloutEvaluator(percentage = 25)) // 25% rollout
}
}Toggle supports reactive feature observation using Kotlin Flow. This is particularly useful for UI updates when features change.
val toggle = Toggle {
sources {
memory {
feature(Features.DARK_MODE, true)
}
}
}
val observableToggle = ObservableToggle {
this.toggle = toggle
bufferCapacity = 64 // Flow buffer size
}
// Observe a feature
observableToggle.observe(Features.DARK_MODE)
.onEach { enabled ->
println("Dark mode is now: $enabled")
}
.launchIn(scope)// Update feature in memory source
memorySource.setFeature(Features.DARK_MODE, false)
// Notify observers
observableToggle.notifyChanged(Features.DARK_MODE)Toggle provides first-class support for Jetpack Compose with reactive state observation.
@Composable
fun FeatureToggleExample() {
val toggle = rememberToggle {
sources {
memory {
feature(Features.DARK_MODE, true)
feature(Features.BETA_UI, false)
}
}
}
val observable = rememberObservableToggle(toggle)
// Observe features
val isDarkMode by observable.observeAsState(Features.DARK_MODE)
val isBetaUI by observable.observeAsState(Features.BETA_UI)
Column {
Text("Dark Mode: ${if (isDarkMode) "ON" else "OFF"}")
Text("Beta UI: ${if (isBetaUI) "ON" else "OFF"}")
}
}// At app level
@Composable
fun App() {
val toggle = rememberToggle {
sources {
yaml { resourcePath = "features.yaml" }
}
}
val observable = rememberObservableToggle(toggle)
CompositionLocalProvider(
LocalToggle provides toggle,
LocalObservableToggle provides observable
) {
MainScreen()
}
}
// In child composables
@Composable
fun ChildScreen() {
val observable = LocalObservableToggle.current
val isPremium by observable.observeAsState(Features.PREMIUM)
if (isPremium) {
PremiumContent()
} else {
FreeContent()
}
}@Composable
fun ConditionalFeature() {
val observable = LocalObservableToggle.current
val showNewUI by observable.observeAsState(Features.NEW_UI)
if (showNewUI) {
NewUserInterface()
} else {
LegacyUserInterface()
}
}The observeAsState composable uses collectAsStateWithLifecycle, which means:
@Composable
fun LifecycleAwareFeature() {
val observable = LocalObservableToggle.current
// Automatically pauses/resumes with lifecycle
val isEnabled by observable.observeAsState(Features.EXPERIMENTAL)
// Use isEnabled...
}Copyright 2025 Behzod Halil
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.