This article goals to show combination of below features together:
- Firebase Remote Config for feature flags
- Kotlin Flow to observe Firebase Remote Config
- Jetpack Compose for screen components
Firebase Remote Config in Android with Kotlin Flow and Jetpack Compose.
To have not a long article, I will not mention about adding firebase into Android project, such as putting google-services.json file, etc. For any concerns about the article, contact me here. 🤝
Quick example:
Example of Firebase Remote Config implementation in Android.
Table of contents:
- Quick look Firebase Remote Config file
- Example Android project structure and its Gradle files
- Observe Firebase Remote Config file with Kotlin Flow
- Collect Firebase Remote Config in ViewModel and update compose screen
1. Quick look Firebase Remote Config
In this example project, there are three properties in firebase remote config file. You can see below them with defined values:
- breaking_news_count = 23
- breaking_news_message = “breaking news from Türkiye!”
- is_visible_breaking_news_message = true
For any case, you can see screenshot of firebase remote config below:
. . .
2. Example Android project structure and its Gradle files
I would like to share gradle files to show plugins and dependencies quickly.
Project build.gradle.kts file:
// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false id("com.google.dagger.hilt.android") version "2.51.1" apply false id("com.google.gms.google-services") version "4.4.2" apply false }
App module build.gradle.kts file:
plugins { ... id("kotlin-kapt") id("com.google.dagger.hilt.android") id("com.google.gms.google-services") } ... dependencies { ... implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7") implementation("com.google.dagger:hilt-android:2.51.1") kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("com.google.firebase:firebase-config:22.0.1") }
You can see project files and google-services.json file below:

Structure of example Android Project.
Before jumping the details, there are HomeScreen (Compose screen design) and HomeViewModel (ViewModel) classes. They are very obvious, so I will not explain them at least in this part. You can find the project on GitHub.
I used Hilt for the dependency injection, so there is App class:
import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class App: Application()
FirebaseRemoteConfigDi class provides two things:
- FirebaseRemoteConfig for FirebaseRemoteConfigProvider
- FirebaseRemoteConfigProvider for HomeViewModel
import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) class FirebaseRemoteConfigDi { @Provides fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig { val firebaseRemoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance() firebaseRemoteConfig .setConfigSettingsAsync( FirebaseRemoteConfigSettings.Builder() .setMinimumFetchIntervalInSeconds(2L) .build(), ) firebaseRemoteConfig.setDefaultsAsync(defaultValueMap) // Important! return firebaseRemoteConfig } @Provides fun provideFirebaseRemoteConfigProvider( firebaseRemoteConfig: FirebaseRemoteConfig ): FirebaseRemoteConfigProvider { return FirebaseRemoteConfigProvider(firebaseRemoteConfig) } }
About this article’s topic, the most significant point is defaultValueMap in the class. I will explain it in next section!
3. Observe Firebase Remote Config with Kotlin Flow
I will move step by step. We defined feature flags in firebase remote config. So, let’s create them in our project:
// FirebaseRemoteConfigProvider class includes below code. enum class HomeScreenFeatureFlag( val keyName: String ) { BREAKING_NEWS_COUNTS( keyName = "breaking_news_count" // It has to be similar with Firebase. ), BREAKING_NEWS_MESSAGE( keyName = "breaking_news_message" ), IS_VISIBLE_BREAKING_NEWS_MESSAGE( keyName = "is_visible_breaking_news_message" ), }
Do not forget that keyName value has to be same with Firebase Remote Config.
Now, we need to define default value map. I mentioned it in previous section but did not explain. Now, let’s understand what is defaultValueMap and why it is important?
defaultValueMap is important because of that firebase provides remote config according to your defaultValueMap when firebase cannot fetch remote config file, for instance no internet connection issue, etc.
So, be careful about your app’s default values. In this example, defaultValueMap is:
// FirebaseRemoteConfigProvider class includes below code. val defaultValueMap = mapOf( HomeScreenFeatureFlag.IS_VISIBLE_BREAKING_NEWS_MESSAGE.keyName to false, HomeScreenFeatureFlag.BREAKING_NEWS_COUNTS.keyName to 0L, HomeScreenFeatureFlag.BREAKING_NEWS_MESSAGE.keyName to "Breaking news message", )
I used defaultValueMap in configuration step as below, you can see in section 2.
firebaseRemoteConfig.setDefaultsAsync(defaultValueMap)
Now, when you search about “listen/observe firebase remote config in android”, you will find below code probably:
firebaseRemoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { override fun onUpdate(configUpdate: ConfigUpdate) { firebaseRemoteConfig.activate().addOnCompleteListener { // Success. } } override fun onError(error: FirebaseRemoteConfigException) { // Error. } })
However, if you use Kotlin and modern approaches, you need to leverage latest updated config file for your view models or other business logics. To achieve this, channelFlow is a great option. I will wrap config update listener with channelFlow and send result.
// FirebaseRemoteConfigProvider class includes below code. private fun observeFeatureFlagList( keyList: List<String> // Observe specific feature flags. ): Flow<List<String>> = callbackFlow { firebaseRemoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { override fun onUpdate(configUpdate: ConfigUpdate) { firebaseRemoteConfig.activate().addOnCompleteListener { trySend( configUpdate .updatedKeys // Set<String>. .toList() // List<String>. .filter { it in keyList } // Observe value of specific feature flags. ) } } override fun onError(error: FirebaseRemoteConfigException) { close(error.cause) } }) awaitClose() }.onStart { emit(keyList) }
As you can see, there is keyList parameter because I would like to consider for only specific feature flags and their values.
Now, we can convert it to SharedFlow to collect in ViewModels. During this time, I prefered to use HashMap to have key-value pair but you can use different data structures of-course.
// FirebaseRemoteConfigProvider class includes below code. fun configKeys(keyList: List<String>) : SharedFlow<HashMap<String, Any>> = observeFeatureFlagList(keyList) .map { updatedKeys -> val hashMap = HashMap<String, Any>() updatedKeys.map { key -> hashMap[key] = getFlagValue(key = key) // What is getFlagValue()? } hashMap } .shareIn( scope = CoroutineScope(Dispatchers.IO), started = SharingStarted.WhileSubscribed(), replay = 1, )
A little information about getFlagValue function: When we are mapping in “updatedKeys.map { key -> …” step, we do not know value type. However, our previously created defaultValueMap must have same data type with remote config file. So, I looked data type of the key in default value map firstly, then I fetched data from remote config according to default data type:
// FirebaseRemoteConfigProvider class includes below code. private fun getFlagValue(key: String): Any { val defaultValue = defaultValueMap[key] return when (defaultValue) { is String -> firebaseRemoteConfig.getString(key) is Boolean -> firebaseRemoteConfig.getBoolean(key) is Long -> firebaseRemoteConfig.getLong(key) else -> "" } }
Now, we are ready to collect updates of Firebase Remote Config in ViewModel side.
Job Offers
4. Collect Firebase Remote Config updates in ViewModel and update compose screen state
Firstly, let us start with screen state data class. It includes properties for feature flags but default values are same with previously defined defaultValueMap as below. This will provide consistence across the project.
// HomeViewModel class includes below code. data class HomeUiState( val breakingNewsCount: Long = defaultValueMap[HomeScreenFeatureFlag.BREAKING_NEWS_COUNTS.keyName] as Long, val isVisibleBreakingNewsMessage: Boolean = defaultValueMap[HomeScreenFeatureFlag.IS_VISIBLE_BREAKING_NEWS_MESSAGE.keyName] as Boolean, val breakingNewsMessage: String = defaultValueMap[HomeScreenFeatureFlag.BREAKING_NEWS_MESSAGE.keyName] as String, )
About state flow, I created a basic uiState for compose screen in ViewModel:
// HomeViewModel class includes below code. @HiltViewModel class HomeViewModel @Inject constructor( private val firebaseRemoteConfigProvider: FirebaseRemoteConfigProvider, ): ViewModel() { // It is for compose screen. val uiState = MutableStateFlow(value = HomeUiState()) init { viewModelScope.launch { fetchRemoteConfig() // Important! This will start observing. } } ... }
As you can see, when View Model is initializing, app starts to fetch remote config file. Now, we can focus on it. I put only one feature flag example but you can find full code on GitHub:
// HomeViewModel class includes below code. private suspend fun fetchRemoteConfig() { firebaseRemoteConfigProvider .configKeys( keyList = listOf( HomeScreenFeatureFlag.BREAKING_NEWS_MESSAGE.keyName, ... // Here is other keys. ) ) .collectLatest { configMap -> uiState.update { HomeUiState( breakingNewsMessage = if (configMap.contains(HomeScreenFeatureFlag.BREAKING_NEWS_MESSAGE.keyName)) { firebaseRemoteConfigProvider.getStringFlagValue( map = configMap, key = HomeScreenFeatureFlag.BREAKING_NEWS_MESSAGE.keyName, ) } else { uiState.value.breakingNewsMessage }, ... // Here is same checkings. ) } } }
To remember, configKeys was a SharedFlow with having key list parameter. It returns updated values of giving keys. Then, we collect the results with “.collectLatest { configMap -> … }” part.
In the part, app checks that “Does updated key-value map include the key?”
- If yes, get new value.
- If not, use latest value.
Finally, compose screen, which is HomeScreen(), collect uiState and manage UI components:
// HomeScreen class includes below code. @Composable fun HomeScreen( modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { // Collect uiState from HomeViewModel. val screenState by viewModel.uiState.collectAsStateWithLifecycle() HomeScreenContent( modifier = modifier, isVisibleBreakingNewsMessage = screenState.isVisibleBreakingNewsMessage, breakingNewsMessage = screenState.breakingNewsMessage, breakingNewsCount = screenState.breakingNewsCount, ) } ...
When you check GitHub project, HomeScreenContentPreview() will show below preview:

Preview of example Android project.
When you install this example Android project from GitHub and put google-services.json file with correct keys and values (then update them), you should be able to see below gif:
Example of Firebase Remote Config implementation in Android.
. . .
I hope, this article will be helpful for you. If you would like to support my work, coffee is my best friend for writing code and articles: https://buymeacoffee.com/canerkaseler ☕️
Do not forget to take a look at my other Medium articles. You can reach me on social media and other platforms, stay tuned: https://linktr.ee/canerkaseler 🤝
This article is previously published on proandroiddev.com.