
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 KoinInitfun 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;
If you have doubts, question don’t hesitate to ask; Will be a pleasure to help.
