Constraints:
We will implement a local, DataStore-backed feature flag system with compile-time flag definitions and runtime override capability.
All flags are defined in a single Kotlin object, providing compile-time discoverability and preventing key typos:
// domain/featureflags/FeatureFlags.kt
object FeatureFlags {
val LLM_REFLECTION_SUMMARY = FeatureFlag(
key = "feature_llm_reflection_summary",
defaultEnabled = true,
description = "LLM-powered daily reflection summaries"
)
val LLM_MORNING_BRIEFING = FeatureFlag(
key = "feature_llm_morning_briefing",
defaultEnabled = false,
description = "LLM-generated morning briefing"
)
val LLM_WEEKLY_SYNTHESIS = FeatureFlag(
key = "feature_llm_weekly_synthesis",
defaultEnabled = false,
description = "LLM-assisted weekly synthesis"
)
val HABIT_TRACKING = FeatureFlag(
key = "feature_habit_tracking",
defaultEnabled = false,
description = "Habit definition and tracking"
)
/** All registered flags, for Settings screen enumeration. */
val all: List<FeatureFlag> = listOf(
LLM_REFLECTION_SUMMARY,
LLM_MORNING_BRIEFING,
LLM_WEEKLY_SYNTHESIS,
HABIT_TRACKING,
)
}
data class FeatureFlag(
val key: String,
val defaultEnabled: Boolean,
val description: String,
)
// domain/repository/FeatureFlagRepository.kt
interface FeatureFlagRepository {
fun isEnabled(flag: FeatureFlag): Flow<Boolean>
suspend fun setEnabled(flag: FeatureFlag, enabled: Boolean)
suspend fun resetToDefaults()
}
The implementation (DataStoreFeatureFlagRepository) reads and writes to the same Preferences DataStore instance used by SettingsRepository (ADR-0027), using the flag's key string directly.
app/
├── domain/
│ ├── featureflags/
│ │ └── FeatureFlags.kt # Flag definitions + FeatureFlag data class
│ └── repository/
│ └── FeatureFlagRepository.kt # Interface
├── data/
│ └── settings/
│ └── DataStoreFeatureFlagRepository.kt # DataStore implementation
└── di/
└── SettingsModule.kt # Binds FeatureFlagRepository (reuses DataStore singleton)
In ViewModels — observe a flag to control behavior:
class ReflectionViewModel @Inject constructor(
private val featureFlagRepository: FeatureFlagRepository,
// ...
) : ViewModel() {
val isLlmSummaryEnabled = featureFlagRepository
.isEnabled(FeatureFlags.LLM_REFLECTION_SUMMARY)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
}
In Navigation / UI — conditionally show destinations:
if (isFeatureEnabled) {
composable(NavigationRoutes.MORNING) { MorningScreen(...) }
}
In Settings screen — enumerate all flags with toggle switches:
FeatureFlags.all.forEach { flag ->
SettingsToggleRow(
label = flag.description,
checked = flagStates[flag.key] ?: flag.defaultEnabled,
onCheckedChange = { viewModel.toggleFlag(flag, it) }
)
}
BuildConfig.DEBUG); those remain compile-time constants and are orthogonal to this system.FeatureFlags object prevents key duplication, enables IDE auto-complete, and makes it trivial to enumerate all flags in the Settings UI.BuildConfig flags are useful for debug-vs-release distinctions but cannot be changed at runtime.resetToDefaults() method supports the "clear all feature flags" action in the data management section of Settings, without affecting other settings.Positive:
FeatureFlagRepository that returns controlled values.FeatureFlags object plus a DataStore key.Downsides:
Follow-up work:
FeatureFlags registry, FeatureFlagRepository, and DataStoreFeatureFlagRepository.DebugFeatureFlagRepository that wraps the real one and overrides isEnabled to always return true, or by calling resetToDefaults() with debug-specific defaults.BuildConfig.DEBUG compile-time constant. Use BuildConfig.DEBUG for developer-only tooling (e.g., strict mode, logging); use feature flags for user-visible functionality that may or may not be ready.FeatureFlag.description field is user-facing and should be written in plain language suitable for the Settings screen.