Last active
February 24, 2025 15:52
-
-
Save kimji1/658b1bdce6eaa73ea71c0f4a6411323a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface FeatureToggleSource { | |
val featureToggles: StateFlow<Map<String, StateFlow<Boolean>>> | |
} | |
enum class FeatureToggleSourceType { | |
REMOTE_CONFIG, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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() }) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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