Firebase Analytics in Compose Multiplatform without Cocoapods

23.09.2025

At Kittysplit, we’ve always aimed to make splitting group expenses as effortless as possible. For years, Kittysplit was a web first product. As our users took Kittysplit on trips, into shared apartments, and to events, it became clear that we needed mobile apps on both Android and iOS.

In this post, we’ll provide a hands-on guide how we wired Firebase Analytics into our Compose Multiplatform app on both Android and iOS without using Cocoapods (which is going to retire soon).

From SwiftUI to Compose Multiplatform

We embraced Kotlin Multiplatform to share business logic across platforms, initially pairing it with a native Android app and a SwiftUI app for iOS, each with its own Firebase Analytics integration.

But when we decided to fully commit to Compose Multiplatform and retire our SwiftUI app, we hit a wall: there’s no out of the box way to use Firebase Analytics in a Compose Multiplatform project. Also, Cocoapods is going to retire soon. Instead, we’ll use the Swift Package Manager dependencies directly with the wonderful spm4Kmp plugin made by François. We’re also going to use Koin to wire it all together.

Project setup

We’re going to add Firebase Analytics to a newly created Compose Multiplatform project, which we’re creating using the new project wizard in Android Studio. If you want to follow along, you can either check out the project code from GitHub or create a new project yourself.

To create a new CMP project in Android Studio, make sure you have the Kotlin Multiplatform plugin installed. You can then click File -> New -> New Project.... Under “Phone and Tablet” select “Kotlin Multiplatform”, enter a name and package for your app and click “Next”. On the next screen, make sure you have the Android and iOS boxes ticked and select “Share UI” as UI implementation. Click “Finish” to create the new project.

Dependencies

Our first step is to add the needed dependencies. To do this, we’re going to use the gradle version catalog. Locate the libs.versions.toml file and add the following lines:

[versions]
# ... other dependencies

koin = "4.1.1"
spm4kmp = "1.0.0-Beta04"
firebase-analytics = "22.5.0"
google-services = "4.4.3"

# spm
spm-firebase = "12.0.0"

[libraries]
# ... other libraries

koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebase-analytics" }

[plugins]
# ... other plugins

spm4kmp = { id = "io.github.frankois944.spmForKmp", version.ref = "spm4kmp"}
google-services = { id = "com.google.gms.google-services", version.ref = "google-services" }

./gradle/libs.versions.toml

You can now sync your project with the gradle files to pull in the dependencies. With this out of the way, we’re going to declare the dependencies in our build.gradle.kts files. We’ll start with the root build.gradle.kts file. Add this line:

plugins {
    // other plugins

    alias(libs.plugins.spm4kmp) apply false
    alias(libs.plugins.google.services) apply false
}

./build.gradle.kts

This just tells gradle to resolve the plugin, but not apply it. This is useful if you have a multi-module gradle project and only want to apply the plugin in a single submodule.

Now we’re going to set up the gradle.build.kts of our composeApp:

plugins {
    // other plugins

    alias(libs.plugins.spm4kmp)
    alias(libs.plugins.google.services)
}


kotlin {
    // ...

    listOf(
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "ComposeApp"
            binaryOption("bundleId", "com.kittysplit.firebase_analytics_cmp")
            isStatic = true
        }
        // This is the important part:
        iosTarget.compilations {
            val main by getting {
                cinterops.create("nativeIosShared")
            }
        }
    }

    sourceSets {
        named { it.lowercase().startsWith("ios") }.configureEach {
            languageSettings {
                optIn("kotlinx.cinterop.ExperimentalForeignApi")
            }
        }

        androidMain.dependencies {
            // ...
            implementation(libs.firebase.analytics.ktx)
        }

        commonMain.dependencies {
            // ...

            api(libs.koin.compose)
            api(libs.koin.core)
        }
    }

    swiftPackageConfig {
        create("nativeIosShared") {
            minIos = "16.0"
            dependency {
                remotePackageVersion(
                    url = uri("https://github.com/firebase/firebase-ios-sdk.git"),
                    products = {
                        add("FirebaseAnalytics", exportToKotlin = true)
                        add("FirebaseCore", exportToKotlin = true)
                    },
                    packageName = "firebase-ios-sdk",
                    version = libs.versions.spm.firebase.get(),
                )
            }
        }
    }
}

./composeApp/build.gradle.kts

That’s all dependency juggling. We added koin as “regular” multiplatform dependency of commonMain. For the Android implementation of Firebase Analytics, we added the Android library to androidMain. To be able to use the iOS Firebase Analytics in our iosMain implementation, we used spm4Kmp to declare the dependency. Notice the exportToKotlin = true, this tells spm4Kmp to export the dependency to kotlin, which means we can use it directly in iosMain code.

When you sync the project again, you’ll notice a new folder: ./composeApp/exportedNativeIosShared. This is the generated swift package which we’re going to add as a dependency to the iOS app next. Open XCode and navigate to the project dependencies:

XCode XCode: iosApp -> PROJECT (iosApp) -> Package Dependencies

Click on the little “+” sign and then on “Add Local…”. Now select the aforementioned folder and click “Add package”. Once done, you’ll see all dependencies of Firebase Analytics and Firebase itself in your package dependencies on the lower left of your XCode window.

Firebase setup

To function properly, you need to register your Android and iOS Apps with Firebase. We’re not going to explain this in detail in this blogpost. Essentially, you need to provide a GoogleService-Info.plist file on iOS and a google-services.json file on Android. You can find an in depth guide for Android here. The guide for iOS is located here. Please note that you only need to follow the steps to obtain the aforementioned files, all other parts are covered in this blog post.

Implementation

This completes the boring part. We’re going to look at the implementation next. Our goal is to create a simple interface that allows us to track events to Firebase Analytics from view models, but also from Composables.

We’re starting by creating a simple interface inside commonMain, which we’re going to implement in androidMain and iosMain:

package com.kittysplit.firebase_analytics_cmp.analytics

interface Analytics {
    fun init()
    fun logEvent(name: String, params: Map<String, Any>?)
}

./composeApp/src/commonMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/Analytics.kt

For the Android implementation, we’re going to use the kotlin Firebase SDK in androidMain:

package com.kittysplit.firebase_analytics_cmp.analytics

import androidx.core.os.bundleOf
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase

class AndroidAnalytics : Analytics {
    private lateinit var firebaseAnalytics: FirebaseAnalytics

    override fun init() {
        firebaseAnalytics = Firebase.analytics
    }

    override fun logEvent(
        name: String,
        params: Map<String, Any>?
    ) {
        val args = params
            ?.map { it.key to it.value }
            ?.toTypedArray()
            ?.let { bundleOf(*it) }

        firebaseAnalytics.logEvent(name, args)
    }
}

./composeApp/src/androidMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/AndroidAnalytics.kt

As you can see, the implementation is straight forward. All we have to do is to convert our parameter map to a Bundle, which is often used in Android to pass data around.

On to the iOS implementation. We’re going to use the native iOS Firebase Analytics SDK in iosMain:

package com.kittysplit.firebase_analytics_cmp.analytics

import FirebaseAnalytics.FIRAnalytics
import FirebaseCore.FIRApp

class AppleAnalytics : Analytics {

    private val firAnalytics: FIRAnalytics.Companion = FIRAnalytics

    override fun init() {
        FIRApp.configure()
    }

    override fun logEvent(
        name: String,
        params: Map<String, String>?
    ) {
        firAnalytics.logEventWithName(name, params as Map<Any?, *>?)
    }
}

./composeApp/src/iosMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/AppleAnalytics.kt

Dependency injection

All that is left to do now is to wire it all up using koin. We’ll have to create a module that provides our AppleAnalytics if we’re running on iOS and the AndroidAnalytics implementation if we’re running on Android. That’s where kotlin’s easy to use [expect/actual declarations] https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-expect-actual.html come in.

In commonMain, we’re only declaring our module as an expect function, which is comparable to an interface where the implementation are provided per platform:

package com.kittysplit.firebase_analytics_cmp.analytics

import org.koin.core.module.Module

expect fun analyticsModule(): Module

./composeApp/src/commonMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/AnalyticsModule.kt

Next, we’re providing the implementations for Android:

package com.kittysplit.firebase_analytics_cmp.analytics

import org.koin.core.module.Module
import org.koin.dsl.module

actual fun analyticsModule(): Module = module {
    single<Analytics> {
        val analytics = AndroidAnalytics()
        analytics.init()
        analytics
    }
}

./composeApp/src/androidMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/AnalyticsModule.android.kt

And iOS:

package com.kittysplit.firebase_analytics_cmp.analytics

import org.koin.core.module.Module
import org.koin.dsl.module


actual fun analyticsModule(): Module = module {
    single<Analytics> {
        val analytics = AppleAnalytics()
        analytics.init()
        analytics
    }
}

./composeApp/src/iosMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/AnalyticsModule.ios.kt

It’s important to call init on our implementations, to make sure they’re ready to go when they’re injected. Now we have to make sure that koin knows about the module we’ve just created. We’re going to do that by starting koin when our application starts. On Android, we can easily do that in the MainActivity:

package com.kittysplit.firebase_analytics_cmp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.kittysplit.firebase_analytics_cmp.analytics.analyticsModule
import org.koin.compose.KoinApplication

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)

        setContent {
            KoinApplication(application = {
                modules(analyticsModule())
            }) {
                App()
            }
        }
    }
}

./composeApp/src/androidMain/kotlin/com/kittysplit/firebase_analytics_cmp/MainActivity.kt

To make it work on iOS, we need to start the koin application in the MainViewController:

package com.kittysplit.firebase_analytics_cmp

import androidx.compose.ui.window.ComposeUIViewController
import com.kittysplit.firebase_analytics_cmp.analytics.analyticsModule
import org.koin.compose.KoinApplication

fun MainViewController() = ComposeUIViewController {
    KoinApplication(application = {
        modules(analyticsModule())
    }) { App() }
}

./composeApp/src/iosMain/kotlin/com/kittysplit/firebase_analytics_cmp/MainViewController.kt

Now we’re ready to inject our analytics implementation anywhere we need it. To test it, we’re going to use it to track the tap on the “Click me!” button from the example:

package com.kittysplit.firebase_analytics_cmp

// ... imports
import com.kittysplit.firebase_analytics_cmp.analytics.Analytics
import org.koin.compose.koinInject

@Composable
fun App(analytics: Analytics = koinInject()) {
    MaterialTheme {
        var showContent by remember { mutableStateOf(false) }
        Column(
            modifier = Modifier
                .background(MaterialTheme.colorScheme.primaryContainer)
                .safeContentPadding()
                .fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Button(onClick = {
                analytics.logEvent("button_clicked", null)
                showContent = !showContent
            }) {
                Text("Click me!")
            }
            AnimatedVisibility(showContent) {
                val greeting = remember { Greeting().greet() }
                Column(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    Image(painterResource(Res.drawable.compose_multiplatform), null)
                    Text("Compose: $greeting")
                }
            }
        }
    }
}

./composeApp/src/commonMain/kotlin/com/kittysplit/firebase_analytics_cmp/App.kt

That’s it! You’re now ready to track events from anywhere in your Android Compose app. There’s one little thing we need to fix. You might have noticed that your preview is not showing any longer. That’s because koin is not initialized. Luckily, there’s a fix for that: we’re going to provide a fake instance of our Analytics for previews:

package com.kittysplit.firebase_analytics_cmp.analytics

import org.koin.dsl.module

fun noopAnalyticsModule() = module {
    single<Analytics> {
        object : Analytics {
            override fun init() {}

            override fun logEvent(name: String, params: Map<String, String>?) {}
        }
    }
}

./composeApp/src/commonMain/kotlin/com/kittysplit/firebase_analytics_cmp/analytics/NoopAnalyticsModule.kt

Now all we have to do is wrap our previews with a little helper:

@Preview
@Composable
fun AppPreview() {
    KoinApplicationPreview(application = { modules(noopAnalyticsModule()) }) {
        App()
    }
}

Now you should see the preview again.

We hope this little guide helps you in your multiplatform endeavours. We had a lot of fun implementing the Kittysplit apps in Compose Multiplatform.