Skip to content

Instantly share code, notes, and snippets.

@kimji1
Last active February 24, 2025 15:52
Show Gist options
  • Save kimji1/658b1bdce6eaa73ea71c0f4a6411323a to your computer and use it in GitHub Desktop.
Save kimji1/658b1bdce6eaa73ea71c0f4a6411323a to your computer and use it in GitHub Desktop.
interface FeatureToggleSource {
val featureToggles: StateFlow<Map<String, StateFlow<Boolean>>>
}
enum class FeatureToggleSourceType {
REMOTE_CONFIG,
}
@MapKey
@Retention(AnnotationRetention.RUNTIME)
annotation class FeatureToggleSourceTypeKey(val value: FeatureToggleSourceType)
@Module
@InstallIn(SingletonComponent::class)
interface FeatureToggleSourceModule {
@Binds
@IntoMap
@FeatureToggleSourceTypeKey(FeatureToggleSourceType.REMOTE_CONFIG)
@Singleton
fun bindRemoteConfigFeatureToggleSource(remoteConfigFeatureToggleSource: RemoteConfigFeatureToggleSource): FeatureToggleSource
}
@Module
@InstallIn(SingletonComponent::class)
object FeatureToggleManagerModule {
@Provides
@Singleton
fun provideFeatureToggleManager(
map: Map<FeatureToggleSourceType, @JvmSuppressWildcards Provider<FeatureToggleSource>>
) = FeatureToggle(dispatcher = Dispatchers.IO, featureToggleSourceMap = map.mapValues { it.value.get() })
}
class RemoteConfigFeatureToggleSource @Inject constructor() : FeatureToggleSource {
private val TAG = this::class.java.simpleName
val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig
private val featureToggleKeys = FeatureToggleKey.entries
.filter { it.sourceType == FeatureToggleSourceType.REMOTE_CONFIG }
.map { it.name }
private val _featureToggles: MutableStateFlow<Map<String, MutableStateFlow<Boolean>>> =
MutableStateFlow(mapOf())
override val featureToggles: StateFlow<Map<String, StateFlow<Boolean>>>
get() = _featureToggles.asStateFlow()
init {
observeChangeEvent()
}
private fun observeChangeEvent() {
remoteConfig.fetchAndActivate()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
updateFeatureToggles(getKeysFeatureToggles(featureToggleKeys))
} else {
Timber.tag(TAG).d("Config params update fail")
}
}
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
remoteConfig.activate().addOnCompleteListener {
updateFeatureToggles(getKeysFeatureToggles(configUpdate.updatedKeys.toList()))
}
}
override fun onError(error: FirebaseRemoteConfigException) {
Timber.tag(TAG).w("Config update error with code: ${error.code} $error")
}
})
}
private fun getKeysFeatureToggles(keys: List<String>): List<Pair<String, Boolean>> {
return keys.map { key ->
runCatching {
check(
value = remoteConfig.all.containsKey(key),
lazyMessage = { "No value of type '$key' exists for parameter key '$key'. Clean the key($key) or add the key($key) in console." }
)
}.onFailure { if (BuildConfig.DEBUG) Timber.tag(TAG).w(it) }
key to (remoteConfig.all[key]?.asBoolean() ?: FeatureToggleKey.getDefault(key))
}
}
private fun updateFeatureToggles(featureToggles: List<Pair<String, Boolean>>) {
val map = _featureToggles.value.toMutableMap()
featureToggles.forEach { featureToggle ->
val (key, value) = featureToggle
map[key] = map.getOrDefault(key, MutableStateFlow(value)).apply { update { value } }
}
_featureToggles.update { map.toMap() }
}
}
class FeatureToggle(
dispatcher: CoroutineDispatcher,
featureToggleSourceMap: Map<FeatureToggleSourceType, FeatureToggleSource>
) {
private val coroutineScope = CoroutineScope(dispatcher)
private val featureToggleMapList = featureToggleSourceMap.map { it.value.featureToggles }
private val combinedFeatureToggleFlow =
combine(featureToggleMapList) { featureToggleMapList ->
featureToggleMapList.toList()
.flatMap { it.entries }
.associate { it.key to it.value }
}.stateIn(coroutineScope, SharingStarted.Eagerly, emptyMap())
fun getToggle(key: FeatureToggleKey): Boolean =
combinedFeatureToggleFlow.value[key.name]?.value ?: FeatureToggleKey.getDefault(key.name)
fun getToggleStream(key: FeatureToggleKey): StateFlow<Boolean> =
combinedFeatureToggleFlow.value[key.name] ?: MutableStateFlow(FeatureToggleKey.getDefault(key.name))
}
enum class FeatureToggleKey(val sourceType: FeatureToggleSourceType, val default: Boolean) {
Test(sourceType = FeatureToggleSourceType.REMOTE_CONFIG, default = false),
companion object {
fun getDefault(key: String) = entries.firstOrNull { it.name == key }?.default ?: false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment