How to implements Remote Config – future flag in KMP

Hello guys.
If you arrived here I hope this post will be useful.
First I will be show how to add future flag using Remote Config – Firebase.

I hope you have minimal know em native platforms (Android and iOS); remember this post I don’t use libs what will be facilitate to implementation Firebase Remote Config.

Let’s go; I´m going this post in three parts.

– Native implementation (basic configs)
– KMP implementation
– Native implementation (iOS configs)

Native implementation

For the natives platform follow the documentation official.

https://firebase.google.com/docs/remote-config/get-started

Also lets go use dependency injection in this project.
The dependency injection used is Koin; follow the official documentation.

https://insert-koin.io/docs/reference/koin-mp/kmp

KMP implementation

Let’s remember how to organize the project KMP.

-Project
-Project/ComposeApp
-Project/ComposeApp/androidMain
-Project/ComposeApp/commonMain
-Project/ComposeApp/iosMain
-Project/iosApp/iosApp

These are the main classes to be created in the project.

-Project/ComposeApp/androidMain/pack/FirebaseRemoteConfigsBridge.kt
-Project/ComposeApp/androidMain/pack/KoinInit.kt
-Project/ComposeApp/androidMain/pack/KoinModuleAndroid.kt

-Project/ComposeApp/commonMain/pack/FirebaseRemoteConfigsBridge.kt
-Project/ComposeApp/commonMain/pack/FirebaseRemoteConfigs.kt
-Project/ComposeApp/commonMain/pack/KoinInit.kt
-Project/ComposeApp/commonMain/pack/KoinModule.kt
-Project/ComposeApp/commonMain/pack/ListItemScreenViewModel.kt

-Project/ComposeApp/iosMain/pack/FirebaseRemoteConfigsBridge.kt
-Project/ComposeApp/iosMain/pack/KoinInit.kt
-Project/ComposeApp/iosMain/pack/KoinModuleAndroid.kt

-Project/iosApp/iosApp/SwiftFirebaseRemoteConfig.swift

We will create the following class inside “Project/ComposeApp/androidMain”:

First we will create MyApplication class and initialize all dependency injection do Android

class MyApplication: Application() {

    companion object {
        @JvmStatic
        private var instance: MyApplication? = null

        @JvmStatic
        public final fun getContext(): Context? {
            return instance
        }
    }

    override fun onCreate() {
        instance = this
        super.onCreate()
        KoinInit(androidContext = this@MyApplication)
    }

}

Edit your Manifest adding permissions to access internet and create MyApplication

<?xml version="1.0" encoding="utf-8"?>
<manifest
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:android="http://schemas.android.com/apk/res/android" >

    <uses-permission android:name="android.permission.INTERNET" />

    <application
            android:name=".demo.MyApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:enableOnBackInvokedCallback="true"
            android:theme="@android:style/Theme.Material.Light.NoActionBar" tools:targetApi="33">
        
        <activity
                android:exported="true"
            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
                android:name="br.com.kmp.demo.demo.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

    </application>

</manifest>

Now we will create the class in have connection with Firebase and this class have a implementation of interface in the common project.

actual class FirebaseRemoteConfigsBridge : FirebaseRemoteConfigs {
    private val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig

    actual override fun fetchAndActivateFirebaseRemoteConfigs(fetchIntervalInSeconds: Double) {
        val configSettings = remoteConfigSettings {
            this.fetchTimeoutInSeconds = fetchIntervalInSeconds.toLong()
        }
        remoteConfig.setConfigSettingsAsync(configSettings)
        remoteConfig.fetchAndActivate()
    }

    actual override fun getRemoteConfigString(key: String): String? {
        return remoteConfig.getString(key)
    }

}

Don’t forget to configure dependency injection; follow the examples below:

actual class KoinInit() {

    constructor(androidContext: Context) : this() {
        initKoin(androidContext)
    }

    fun initKoin(androidContext: Context) {
        val modules = sharedModules() + moduleAndroid()
        startKoin {
            androidLogger()
            androidContext(androidContext = androidContext)
            modules(modules = modules)
        }
    }
}
fun moduleAndroid() = module {

    single<FirebaseRemoteConfigs> { FirebaseRemoteConfigsBridge() }
    factory {  ListItemScreenViewModel(get<FirebaseRemoteConfigs>() as FirebaseRemoteConfigsBridge) }

}

We will create the following class inside “Project/ComposeApp/iosMain”:
Note: Firebase Remote Config is sended to parameter, this explanation will be below.

actual class FirebaseRemoteConfigsBridge(private val delegateFirebaseRemoteConfigs: FirebaseRemoteConfigs) :
    FirebaseRemoteConfigs {

    actual override fun fetchAndActivateFirebaseRemoteConfigs(fetchIntervalInSeconds: Double) {
        delegateFirebaseRemoteConfigs.fetchAndActivateFirebaseRemoteConfigs(fetchIntervalInSeconds)
    }

    actual override fun getRemoteConfigString(key: String): String? {
        return delegateFirebaseRemoteConfigs.getRemoteConfigString(key = key)
    }

}

Don’t forget to configure dependency injection in the iOS; follow the examples below:

Note: one passed as a parameter cames iOS native;

@ObjCName(name = "KoinInit", exact = true)
@ExportObjCClass
actual class KoinInit() {

    fun initKoin(delegateFirebaseRemoteConfigs: FirebaseRemoteConfigs) {

        val modules = sharedModules() + moduleIos(delegateFirebaseRemoteConfigs)
        startKoin {
            modules(modules = modules)
        }.also {
            koinInstance = it
        }

    }

}

We will create the following class inside “Project/ComposeApp/commonMain”:

expect class KoinInit
fun sharedModules() = module { }

interface FirebaseRemoteConfigs{
    fun fetchAndActivateFirebaseRemoteConfigs(fetchIntervalInSeconds: Double)
    fun getRemoteConfigString(key: String): String?
}
expect class FirebaseRemoteConfigsBridge : FirebaseRemoteConfigs {
    override fun fetchAndActivateFirebaseRemoteConfigs(fetchIntervalInSeconds: Double)
    override fun getRemoteConfigString(key: String): String?
}
class ListItemScreenViewModel(
    private val firebaseRemoteConfigsBridge: FirebaseRemoteConfigsBridge) : BaseViewModel() {

    private val _stateRemoteConfig = MutableStateFlow<RemoteConfigUiState>(RemoteConfigUiState.Loading(step = "loading"))
    val stateRemoteConfig: StateFlow<RemoteConfigUiState> = _stateRemoteConfig
    var jobRemoteConfig: Job? = SupervisorJob()

    private val _myByteArray = MutableStateFlow<ByteArray>(ByteArray(0))
    val myByteArray = _myByteArray

    init {
        firebaseRemoteConfigsBridge.fetchAndActivateFirebaseRemoteConfigs(2.0)
        viewModelScope.launch {
            getValueRemoteConfigs()
        }
    }

    suspend fun getValueRemoteConfigs() {
        jobRemoteConfig?.cancel()
        jobRemoteConfig = viewModelScope.launch {
        
            try {
            
                _stateRemoteConfig.value = RemoteConfigUiState.Loading(step = "loading")
                val remoteConfigvalue = firebaseRemoteConfigsBridge.getRemoteConfigString(key = "forteach").toBoolean()
                _stateRemoteConfig.value = RemoteConfigUiState.Success(valueRemoteConfig = remoteConfigvalue)
                delay(100)
                _stateRemoteConfig.value = RemoteConfigUiState.Loading(step = "")
            
            }catch (e: Exception){
                _stateRemoteConfig.value = RemoteConfigUiState.Error(message = "Error ${e.message}")
                delay(100)
                _stateRemoteConfig.value = RemoteConfigUiState.Loading(step = "")
            }
            
        }
        jobRemoteConfig?.join()
    }

    sealed class RemoteConfigUiState {
        data class Loading(val step: String = "loading") : RemoteConfigUiState()
        data class Success(val valueRemoteConfig: Boolean = false) : RemoteConfigUiState()
        data class Error(val message: String = "") : RemoteConfigUiState()
    }

}

In the class App should be call your screen with Remote Config implementation

@Composable
fun MainActivity(
    listItemsViewModel: ListItemScreenViewModel = koinInject<ListItemScreenViewModel>() ) {

    var errorMessage by remember { mutableStateOf("") }
    var stepMessage by remember { mutableStateOf("") }
    var valueRemoteConfigs by remember { mutableStateOf(false) }
    val stateRemoteConfig: ListItemScreenViewModel.RemoteConfigUiState by listItemsViewModel.stateRemoteConfig.collectAsState()
    
    when(stateRemoteConfig){
        is ListItemScreenViewModel.RemoteConfigUiState.Error -> {
            errorMessage = (stateRemoteConfig as ListItemScreenViewModel.RemoteConfigUiState.Error).message
        }
        is ListItemScreenViewModel.RemoteConfigUiState.Loading -> {
            stepMessage = (stateRemoteConfig as ListItemScreenViewModel.RemoteConfigUiState.Loading).step
        }
        is ListItemScreenViewModel.RemoteConfigUiState.Success -> {
            valueRemoteConfigs = (stateRemoteConfig as ListItemScreenViewModel.RemoteConfigUiState.Success).valueRemoteConfig
        }
    }

 
    DisposableEffect(Unit) {
        onDispose {
        }
    }

    MaterialTheme {
        Column(
            modifier = Modifier
                .safeContentPadding()
                .fillMaxSize().padding(4.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {

            TopAppBar(
                actions = {
                    IconButton(onClick = { }) {
                        Icon(
                            imageVector = Icons.Default.BackHand,
                            contentDescription = "Localized description"
                        )
                    }
                },
                navigationIcon = {
                    IconButton(onClick = {  }) {
                        Icon(
                            imageVector = Icons.Default.CloudOff,
                            contentDescription = "Localized description"
                        )
                    }
                },
                title = { Text("List teach") }
            )

            Column(
                modifier = Modifier
                    .verticalScroll(rememberScrollState())
                    .weight(weight = 1f, fill = false)
            ) {
            
                Spacer(Modifier.height(20.dp))

                Card(
                    modifier = Modifier.fillMaxWidth().border(
                        width = 2.dp,
                        color = AppColors.blueDark,
                        shape = RoundedCornerShape(16.dp)
                    ),
                    shape = RoundedCornerShape(20.dp),
                    colors = CardDefaults.cardColors(
                        containerColor = AppColors.blueLight,
                        contentColor = AppColors.blueNormal
                    ),
                    elevation = CardDefaults.cardElevation()
                ) {
                    Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
                        Spacer(Modifier.height(8.dp))
                        Text(
                            text = "Remote config value $errorMessage",
                            color = AppColors.blackNormal
                        )
                        Text(
                            text = stepMessage,
                            color = AppColors.blackNormal
                        )
                        Switch(
                            checked = valueRemoteConfigs,
                            onCheckedChange = { },
                            colors = SwitchDefaults.colors(
                                checkedThumbColor = AppColors.blueLight,
                                uncheckedThumbColor = AppColors.blueNormal,
                                disabledCheckedThumbColor = AppColors.blueLight,
                                disabledCheckedTrackColor = AppColors.blueNormal
                            ),
                            enabled = false,
                        )
                    }
                }

                Spacer(Modifier.height(20.dp))

            }
        }

    }
}

Native implementation (iOS configs)

Project/iosApp/iosApp

Now let’s work on the iOS part; where the remote config value will be sent as a parameter.

@objc public class SwiftFirebaseRemoteConfig: NSObject, FirebaseRemoteConfigs {
    let remoteConfig = RemoteConfig.remoteConfig()
    
    @objc public func fetchAndActivateFirebaseRemoteConfigs(fetchIntervalInSeconds: Double) {
        remoteConfig.configSettings.minimumFetchInterval = fetchIntervalInSeconds
        remoteConfig.fetchAndActivate()
    }
    
    @objc public func getRemoteConfigString(key: String) -> String? {
        return remoteConfig.configValue(forKey: key).stringValue as String?
    }
    
}
class AppDelegate: NSObject, UIApplicationDelegate {

 func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        
        FirebaseApp.configure()
        
        let swiftFirebaseRemoteConfig = SwiftFirebaseRemoteConfig()
        ComposeApp.KoinInit().doInitKoin(
            delegateFirebaseRemoteConfigs: swiftFirebaseRemoteConfig
        )
        
        return true
    }
}

@main
struct iOSApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

}

Extras

You can access also ours GitHub with all code and news features;

GitHub

If you have doubts, question don’t hesitate to ask; Will be a pleasure to help.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *