Mastering Kotlin 2.4+ SwiftPM (Part 3): Implementing Firebase Authentication in KMP
Learn how to implement Firebase Authentication in a Kotlin Multiplatform (KMP) project, enabling you to share authentication logic between Android and iOS using SwiftPM.
AG Mobile Labs•10 min read•
AndroidiOSKMPFirebaseAuthAndroid StudioFirebaseSwift Package ManagerKotlin

In the previous article, we learned how to configure Firebase in our KMP project and make it available across our common code source sets.
In this part, we will build upon our setup to implement Firebase Authentication for both Android and iOS apps.
Initializing Firebase
To initialize Firebase for Android, we need to call the
FirebaseApp.initializeApp() method within the onCreate() method of our custom Application class.
Here, I created AuroraApplication inside the androidApp module and registered it in the androidApp/src/main/AndroidManifest.xml file.// androidApp/src/main/kotlin/com/ai/auroraai/AuroraApplication.kt
import android.app.Application
import com.google.firebase.FirebaseApp
class AuroraApplication: Application() {
override fun onCreate() {
super.onCreate()
FirebaseApp.initializeApp(this)
}
}<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".AuroraApplication"
<!-- Existing activity and other declarations -->
</application>
</manifest>For iOS, we can initialize Firebase by calling
FIRApp.configure() inside the MainViewController located in the iosMain source set.// shared/src/iosMain/kotlin/com/ai/auroraai/MainViewController.kt
fun MainViewController() = ComposeUIViewController {
FIRApp.configure()
App()
}As you may have noticed, SwiftPM significantly simplifies iOS Firebase configuration. There is no longer a need to write Swift code in the
iosApp module just to configure Firebase! This is a massive improvement over the older CocoaPods approach.Implementing Firebase Authentication
Now that we've set up Firebase and added the necessary dependencies, let's dive into implementing the authentication flow. We'll create a shared authentication interface named
AuthManager in the shared/commonMain source set, which we will then implement for each platform.// shared/src/commonMain/kotlin/com/ai/auroraai/domain/AuthManager.kt
interface AuthManager {
fun signIn(email: String, password: String, onResult: (Boolean, String?) -> Unit)
}This shared interface defines the contract for our authentication operations. In
androidMain, we provide the Android-specific implementation for this interface:// shared/src/androidMain/kotlin/com/ai/auroraai/data/AndroidAuthManager.kt
class AndroidAuthManager : AuthManager {
private val auth: FirebaseAuth
get() = FirebaseAuth.getInstance()
override fun signIn(
email: String,
password: String,
onResult: (Boolean, String?) -> Unit
) {
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
onResult(true, null)
} else {
onResult(false, task.exception?.localizedMessage ?: "Authentication failed")
}
}
}
}Similarly, we define the iOS implementation for
AuthManager in the shared/iosMain source set:// shared/src/iosMain/kotlin/com/ai/auroraai/data/IOSAuthManager.kt
class IosAuthManager : AuthManager {
@OptIn(ExperimentalForeignApi::class)
override fun signIn(
email: String,
password: String,
onResult: (Boolean, String?) -> Unit
) {
FIRAuth.auth().signInWithEmail(email, password = password) { result, error ->
if (error != null) {
onResult(false, error.localizedDescription)
} else {
onResult(true, null)
}
}
}
}While we could use
expect/actual for the AuthManager, using a standard Kotlin interface combined with dependency injection (DI) or factory patterns
is widely considered best practice for business logic and third-party services like Firebase for several reasons:Flexibility vs Rigidity
-
The
expect/actualapproach (Rigid): If you markexpect interface AuthManagerorexpect class AndroidAuthManager, you are telling the compiler: "There is a physical, platform-specific implementation that will be compiled directly into the binary for each target." The problem here is that you can't easily swap out the implementation for testing or mock data. If you are incommonMainwriting tests for your ViewModel, you are stuck using the actual native Firebase implementation, which requires a real device or simulator to run. -
The
interfaceapproach (Flexible): By using a standard commoninterface, your common code doesn't care where the implementation comes from.- It could come from
androidMain(using Firebase Android) - It could come from
iosMain(using Firebase iOS) - It could come from a
commonMainmock class used entirely for local preview screens or unit testing.
- It could come from
Cleaner Architecture (Dependency Injection)
When you use an interface, you pass the implementation into your common business logic at runtime. This decouples your code completely.
For example, your ViewModel in
commonMain can look like this:class LoginViewModel(private val authManager: AuthManager) {
fun handleLogin() {
authManager.signIn("user@test.com", "password") { success, error ->
// Update UI state completely in commonMain
}
}
}A good rule of thumb in modern KMP development is:
-
Use
expect/actualfor low-level platform APIs, system utilities, or hardware configurations (e.g., getting the device OS version, accessing file system paths, handling the platform Context object). -
Use
interfacefor business logic, network clients, databases, and third-party wrappers (like Firebase, Supabase, or Analytics).
To get the platform-specific implementations into common code, we can use a simple Factory pattern or a DI library like Koin.
Since we haven't configured Koin in our project yet, we will use a Simple Factory pattern for now:
Under
commonMain, we declare the createAuthManager expect function inside the AuthManagerFactory.kt file.// shared/src/commonMain/kotlin/com/ai/auroraai/data/AuthManagerFactory.kt
import com.ai.auroraai.domain.AuthManager
expect fun createAuthManager(): AuthManagerNext, we provide the
actual Android and iOS implementations under androidMain and iosMain.// shared/src/androidMain/kotlin/com/ai/auroraai/data/AuthManagerFactory.kt
import com.ai.auroraai.domain.AuthManager
actual fun createAuthManager(): AuthManager {
return AndroidAuthManager()
}// shared/src/iosMain/kotlin/com/ai/auroraai/data/AuthManagerFactory.kt
import com.ai.auroraai.domain.AuthManager
actual fun createAuthManager(): AuthManager {
return IosAuthManager()
}Create and Use the ViewModel in commonMain
Because
LoginViewModel lives entirely in commonMain, it only interacts with the AuthManager interface. It doesn't know (or care) whether it's executing Android Play Services or an iOS Swift Package under the hood.// shared/src/commonMain/kotlin/com/ai/auroraai/presentation/LoginViewModel.kt
package com.ai.auroraai.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ai.auroraai.domain.AuthManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class LoginViewModel(private val authManager: AuthManager): ViewModel() {
private val _uiState = MutableStateFlow<String>("Idle")
val uiState: StateFlow<String> = _uiState
fun login(email: String, password: String) {
_uiState.value = "Loading..."
viewModelScope.launch {
authManager.signIn(email, password) { success, errorMessage ->
if (success) {
_uiState.value = "Success! Logged in."
} else {
_uiState.value = "Error: ${errorMessage ?: "Unknown error"}"
}
}
}
}
}Since our project shares Compose UI across both platforms, we can instantiate
LoginViewModel in the shared module and use it on both platforms seamlessly.
In our case, we haven't created a custom UI for the login screen yet. To verify that our setup works correctly, we will create an instance of LoginViewModel in the App.kt file within commonMain. We'll also add a minimal UI to test if our Firebase implementation authenticates users successfully.// shared/src/commonMain/kotlin/com/ai/auroraai/presentation/App.kt
@Composable
@Preview
fun App() {
val loginViewModel = remember {
LoginViewModel(authManager = createAuthManager())
}
val loginState by loginViewModel.uiState.collectAsStateWithLifecycle()
MaterialTheme {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("Auth Status: $loginState")
Button(onClick = {
loginViewModel.login("example@gmail.com", "123456")
}) {
Text("Sign In")
}
}
}
}In the Firebase console, I have already added a test user with the email
I ran the app on an Android emulator and an iOS simulator. After clicking the Sign In button, it displays a successful authentication state, as shown in the screenshots below:
"example@gmail.com" and the password "123456".I ran the app on an Android emulator and an iOS simulator. After clicking the Sign In button, it displays a successful authentication state, as shown in the screenshots below:


Source Code
You can find the complete, working source code for this project—including the Firebase and SwiftPM configuration—on my GitHub repository:
👉 AuroraAi-kmp on GitHub
Conclusion & Next Steps
We have successfully implemented Firebase Authentication in our Kotlin Multiplatform project using the interface pattern! This approach not only keeps our
commonMain code clean and testable but also takes full advantage of Swift Package Manager on the iOS side, avoiding any unnecessary Swift bindings.In the next part of this series, we will look into adding Dependency Injection (like Koin) to properly inject our
AuthManager and scale our architecture, as well as building a beautiful, fully functional Login and Signup UI using Compose Multiplatform.Stay tuned! If you have any questions, suggestions, or run into issues setting this up, please feel free to open an issue on the GitHub repository.
Enjoyed this article?
Subscribe to my newsletter to get the latest articles on Android architecture, KMP, and mobile engineering straight to your inbox.