How to implements Realtime Database Firebase in KMP

This post is very similar to previous; how to implements remote config.
If you arrived here I hope this post will be useful.
First I will be show how to comunicate with Realtime Database – Firebase;

This communication will works with inputs e outputs, other words reading and writing.

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 Realtime Database.

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/FirebaseDataBaseRealTimeBridge.android.kt
-Project/ComposeApp/androidMain/pack/KoinInit.kt
-Project/ComposeApp/androidMain/pack/KoinModuleAndroid.kt

-Project/ComposeApp/commonMain/pack/FirebaseDataBaseRealTimeBridge.kt
-Project/ComposeApp/commonMain/pack/FirebaseRealTimeDataBase.kt
-Project/ComposeApp/commonMain/pack/KoinInit.kt
-Project/ComposeApp/commonMain/pack/KoinModule.kt
-Project/ComposeApp/commonMain/pack/FirebaseRealTimeDataBaseScreen.kt
-Project/ComposeApp/commonMain/pack/FirebaseRealTimeDataBaseViewModel.kt

-Project/ComposeApp/androidMain/pack/FirebaseDataBaseRealTimeBridge.ios.kt
-Project/ComposeApp/androidMain/pack/KoinInit.kt
-Project/ComposeApp/androidMain/pack/KoinModuleAndroid.kt

-Project/iosApp/iosApp/SwiftFirebaseDataBaseRealTime.swift

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

Each with their own responsibility. This class will do communication with Firebase.
Which later will do implements by ViewModel.

actual class FirebaseDataBaseRealTimeBridge : FirebaseRealTimeDataBase {

    private val myFireBaseDataBase: FirebaseDatabase = Firebase.database

    actual override fun fetchDataNodo(nodoName: String,
                                      onSuccess: (String) -> Unit,
                                      onError: (Exception) -> Unit) {

        myFireBaseDataBase.getReference( nodoName)
            .addValueEventListener(object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    onSuccess(snapshot.toString())
                }

                override fun onCancelled(error: DatabaseError) {
                    onCancelled(error)
                }
            })
    }

    actual override fun putDataByNodoName(nodoName: String, keyNewNodo: String, valueForNodo: String) {

        val newValues = mapOf(
            keyNewNodo to valueForNodo
        )

        myFireBaseDataBase.getReference(nodoName)
            .updateChildren(newValues)
            .addOnSuccessListener { }
            .addOnFailureListener { error ->
                throw Exception("Fail save")
            }
    }

}
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<FirebaseRealTimeDataBase> { FirebaseDataBaseRealTimeBridge() }
    factory { FirebaseRealTimeDataBaseViewModel(get<FirebaseRealTimeDataBase>() as FirebaseDataBaseRealTimeBridge) }

}

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

actual class FirebaseDataBaseRealTimeBridge(private val delegateFirebaseRealTimeDataBase: FirebaseRealTimeDataBase) : FirebaseRealTimeDataBase {
    actual override fun fetchDataNodo(
        nodoName: String,
        onSuccess: (String) -> Unit,
        onError: (Exception) -> Unit,
    ) {
        delegateFirebaseRealTimeDataBase.fetchDataNodo(nodoName, onSuccess, onError)
    }

    actual override fun putDataByNodoName(
        nodoName: String,
        keyNewNodo: String,
        valueForNodo: String,
    ) {
        delegateFirebaseRealTimeDataBase.putDataByNodoName(nodoName = nodoName, keyNewNodo = keyNewNodo, valueForNodo = valueForNodo)
    }
}

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

@OptIn(ExperimentalObjCName::class, BetaInteropApi::class)
@ObjCName(name = "KoinInit", exact = true)
@ExportObjCClass
actual class KoinInit() {

    fun initKoin(delegateFirebaseRemoteConfigs: FirebaseRemoteConfigs,
                 delegateFirebaseRealTimeDataBase: FirebaseRealTimeDataBase) {

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

    }

}

Note: one passed as a parameter cames iOS native;

fun moduleIos(delegateFirebaseRealTimeDataBase: FirebaseRealTimeDataBase) = module {

    single<FirebaseRealTimeDataBase> { FirebaseDataBaseRealTimeBridge(delegateFirebaseRealTimeDataBase) }
    factory { FirebaseRealTimeDataBaseViewModel(get<FirebaseRealTimeDataBase>() as FirebaseDataBaseRealTimeBridge) }

}

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

expect class KoinInit
fun sharedModules() = module { }
 expect class FirebaseDataBaseRealTimeBridge: FirebaseRealTimeDataBase {

     override fun fetchDataNodo(
         nodoName: String,
         onSuccess: (String) -> Unit,
         onError: (Exception) -> Unit,
     )

     override fun putDataByNodoName(nodoName: String, keyNewNodo: String, valueForNodo: String)

 }
interface FirebaseRealTimeDataBase {
    fun fetchDataNodo(nodoName: String = "messages",
                      onSuccess: (String) -> Unit,
                      onError: (Exception) -> Unit)
    fun putDataByNodoName(nodoName: String, keyNewNodo: String, valueForNodo: String)
}
class FirebaseRealTimeDataBaseViewModel(
    private val firebaseDataBaseRealTimeBridge: FirebaseDataBaseRealTimeBridge
): BaseViewModel() {

    private val _nodo = MutableStateFlow<String>( "messages")
    val nodo: StateFlow<String> = _nodo

    private val _stateSnapShotFiebaseDataBaseRealTimeByNodo = MutableStateFlow<GetRealTimeDataBaseUiState>(GetRealTimeDataBaseUiState.Loading(step = ""))
    val stateSnapShotFiebaseDataBaseRealTimeByNodo: StateFlow<GetRealTimeDataBaseUiState> = _stateSnapShotFiebaseDataBaseRealTimeByNodo
    var jobGetSnaptShotFirebaseDataBaseRealTime: Job? = SupervisorJob()


    private val _statePutFiebaseDataBaseRealTimeByNodo = MutableStateFlow<PutRealTimeDataBaseUiState>(PutRealTimeDataBaseUiState.Idle)
    val statePutFiebaseDataBaseRealTimeByNodo: StateFlow<PutRealTimeDataBaseUiState> = _statePutFiebaseDataBaseRealTimeByNodo
    var jobPutDataFirebaseDataBaseRealTime: Job? = SupervisorJob()

    init {
        viewModelScope.launch {
            getSnapShotFirebaseDataBaseRealTime(nodoName = nodo.value)
        }
    }
    
    fun putDataFirebaseDataBaseRealTime(nodoName: String, keyNewNodo: String, valueForNodo: String){

        if(keyNewNodo.isEmpty() || valueForNodo.isEmpty() ){
            return
        }

        jobPutDataFirebaseDataBaseRealTime?.cancel()
        jobPutDataFirebaseDataBaseRealTime = viewModelScope.launch {
            try {

                if(nodoName != nodo.value){
                    _nodo.value = nodoName
                    async {
                        getSnapShotFirebaseDataBaseRealTime(nodoName = nodoName)
                    }.await()
                }

                delay(timeMillis = 1000)
                async {
                    firebaseDataBaseRealTimeBridge.putDataByNodoName(nodoName = nodoName, keyNewNodo = keyNewNodo, valueForNodo = valueForNodo)
                }.await()
                delay(timeMillis = 100)

            }catch (exception: Exception){
                _statePutFiebaseDataBaseRealTimeByNodo.value = PutRealTimeDataBaseUiState.Error(exception.message.toString())
                delay(timeMillis = 100)
            }

        }
    }

    fun getSnapShotFirebaseDataBaseRealTime(nodoName: String) {
        jobGetSnaptShotFirebaseDataBaseRealTime?.cancel()
        jobGetSnaptShotFirebaseDataBaseRealTime = viewModelScope.launch {
            try {

                async {
                    firebaseDataBaseRealTimeBridge.fetchDataNodo(
                        nodoName = nodoName,
                        onSuccess = { it ->
                            _stateSnapShotFiebaseDataBaseRealTimeByNodo.value = GetRealTimeDataBaseUiState.Success(it)
                        },
                        onError =  { exception ->
                            _stateSnapShotFiebaseDataBaseRealTimeByNodo.value = GetRealTimeDataBaseUiState.Error(exception.message.toString())
                        }
                    )
                }.await()

            }catch (exception: Exception){
                _stateSnapShotFiebaseDataBaseRealTimeByNodo.value = GetRealTimeDataBaseUiState.Error(exception.message.toString())
                delay(timeMillis = 100)
            }
        }
    }

    sealed class GetRealTimeDataBaseUiState {
        data class Loading(val step: String = "loading") : GetRealTimeDataBaseUiState()
        data class Error(val message: String = "") : GetRealTimeDataBaseUiState()
        data class Success(val valueSnapShot: String = "") : GetRealTimeDataBaseUiState()
    }

    sealed class PutRealTimeDataBaseUiState {
        object Idle : PutRealTimeDataBaseUiState()
        data class Error(val message: String = "") : PutRealTimeDataBaseUiState()
    }

}
@Composable
fun FirebaseRealTimeDataBaseScreen(
    firebaseRealTimeDataBaseViewModel: FirebaseRealTimeDataBaseViewModel = koinInject<FirebaseRealTimeDataBaseViewModel>()) {

    var snapShotNodoValue: String by remember { mutableStateOf("") }
    var nodoValue: String by remember { mutableStateOf(firebaseRealTimeDataBaseViewModel.nodo.value ) }
    var keyByKeyNodo: String by remember { mutableStateOf("") }
    var valueByKeyNodo: String by remember { mutableStateOf("") }
    val stateRealTimeDataBaseViewModel: FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState by firebaseRealTimeDataBaseViewModel.stateSnapShotFiebaseDataBaseRealTimeByNodo.collectAsState()
    snapShotNodoValue = when(stateRealTimeDataBaseViewModel){
        is FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState.Error -> {
            (stateRealTimeDataBaseViewModel as FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState.Error).message
        }
        is FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState.Loading ->  {
            (stateRealTimeDataBaseViewModel as FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState.Loading).step
        }
        is FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState.Success ->  {
            (stateRealTimeDataBaseViewModel as FirebaseRealTimeDataBaseViewModel.GetRealTimeDataBaseUiState.Success).valueSnapShot
        }
    }

    var errorMessagePut: String by remember { mutableStateOf("") }
    val statePutRealTimeDataBaseViewModel: FirebaseRealTimeDataBaseViewModel.PutRealTimeDataBaseUiState by firebaseRealTimeDataBaseViewModel.statePutFiebaseDataBaseRealTimeByNodo.collectAsState()
    errorMessagePut = when (statePutRealTimeDataBaseViewModel){
        is FirebaseRealTimeDataBaseViewModel.PutRealTimeDataBaseUiState.Error -> {
            (statePutRealTimeDataBaseViewModel as FirebaseRealTimeDataBaseViewModel.PutRealTimeDataBaseUiState.Error) .message
        }
        FirebaseRealTimeDataBaseViewModel.PutRealTimeDataBaseUiState.Idle ->  {
            ""
        }
    }

    DisposableEffect(Unit) {
        onDispose {
        }
    }

    RegisterBackHandler  {
        firebaseRealTimeDataBaseViewModel.clear()
    }

    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") }
            )

            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 = "Get all infos nodo -> $nodoValue", color = AppColors.blackNormal)
                    Text(text = "HERE -> $snapShotNodoValue", color = AppColors.blackNormal)

                    Spacer(Modifier.height(60.dp))
                    Text(text = "Add $errorMessagePut", color = AppColors.blackNormal)

                    Spacer(Modifier.height(10.dp))
                    Text(text = "Nodo", color = AppColors.blackNormal)
                    BasicTextField(
                        maxLines = 1,
                        textStyle = TextStyle.Default.copy(
                            fontSize = 30.sp,
                            color = Color.DarkGray
                        ),
                        value = nodoValue,
                        onValueChange = { nodoValue = it },
                        modifier = Modifier
                            .padding(8.dp)
                            .border(2.dp, Color.Black)
                            .background(Color.White)
                            .fillMaxWidth()
                            .height(40.dp)
                    )

                    Spacer(Modifier.height(10.dp))
                    Text(text = "key", color = AppColors.blackNormal)
                    BasicTextField(
                        maxLines = 1,
                        textStyle = TextStyle.Default.copy(
                            fontSize = 30.sp,
                            color = Color.DarkGray
                        ),
                        value = keyByKeyNodo,
                        onValueChange = { keyByKeyNodo = it },
                        modifier = Modifier
                            .padding(8.dp)
                            .border(2.dp, Color.Black)
                            .background(Color.White)
                            .fillMaxWidth()
                            .height(40.dp)
                    )

                    Spacer(Modifier.height(10.dp))
                    Text(text = "value", color = AppColors.blackNormal)
                    BasicTextField(
                        maxLines = 1,
                        textStyle = TextStyle.Default.copy(
                            fontSize = 30.sp,
                            color = Color.DarkGray
                        ),
                        value = valueByKeyNodo,
                        onValueChange = { valueByKeyNodo = it },
                        modifier = Modifier
                            .padding(8.dp)
                            .border(2.dp, Color.Black)
                            .background(Color.White)
                            .fillMaxWidth()
                            .height(40.dp)
                    )

                    Spacer(Modifier.height(10.dp))
                    Button(onClick = {
                     firebaseRealTimeDataBaseViewModel.putDataFirebaseDataBaseRealTime(
                                nodoName = nodoValue,
                                keyNewNodo = keyByKeyNodo,
                                valueForNodo = valueByKeyNodo,
                            )
                    }) {
                        Text(text = "Click add value", color = AppColors.whiteNormal)
                    }

                }

            }
            Spacer(Modifier.height(20.dp))
        }

    }

}

Native implementation (iOS configs)

Project/iosApp/iosApp

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

class AppDelegate: NSObject, UIApplicationDelegate {

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


@main
struct iOSApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

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

}
import FirebaseDatabase
import ComposeApp

@objc public class SwiftFirebaseDataBaseRealTime: NSObject, FirebaseRealTimeDataBase {
    let myFireBaseDataBase = Database.database()
    
    @objc public func fetchDataNodo(nodoName: String,
                                    onSuccess: @escaping (String) -> Void,
                                    onError: @escaping (KotlinException) -> Void) {
        
        let reference = myFireBaseDataBase.reference(withPath: nodoName)
        reference.observe(.value) { snapshot in
            print("fetchDataNodo: Value snapshot is: \(snapshot)")
            onSuccess(snapshot.description)
        } withCancel: { error in
            print("fetchDataNodo: Cancelled with error: \(error.localizedDescription)")
            onError(error as! KotlinException)
        }
    }
    
    @objc public func putDataByNodoName(nodoName: String, keyNewNodo: String, valueForNodo: String) {
        
        let values: [String: String] = [
                    keyNewNodo: valueForNodo
                ]
                
        let reference = myFireBaseDataBase.reference(withPath: nodoName)
        reference.updateChildValues(values) { error, _ in
            if let error = error {
                print("putDataByNodoName: Fail \(error.localizedDescription)")
                // Se quiser lançar, pode usar uma completion ou delegate
            } else {
                print("putDataByNodoName: Success \(nodoName)")
            }
        }
    }
    
}

Extras

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

GitHub

Don’t forget habilitate write and reading in your firebase console.
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 *