Blog Infos
Author
Published
Topics
, , , ,
Published

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:

  1. Quick look Firebase Remote Config file
  2. Example Android project structure and its Gradle files
  3. Observe Firebase Remote Config file with Kotlin Flow
  4. 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:

Screenshot of Firebase Project.

. . .

 

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:

  1. FirebaseRemoteConfig for FirebaseRemoteConfigProvider
  2. 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Tests unitaires avec KotlinFlow

Lorsque nous développons une fonctionnalité, nous devons écrire les tests unitaires. C’est une partie essentielle du développement. Cela assure le bon fonctionnement du code lors de futurs changements ou refactorisations. Kotlin Flow ne fait pas…
Watch Video

Tests unitaires avec KotlinFlow

Florent Blot
Android Developer
Geev

Tests unitaires avec KotlinFlow

Florent Blot
Android Developer
Geev

Tests unitaires avec KotlinFlow

Florent Blot
Android Developer
Geev

Jobs

No results found.

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.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu