Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The rollercoaster of releasing an Android, iOS ...

The rollercoaster of releasing an Android, iOS and desktop app with Kotlin Multiplatform | droidcon Lisbon

With the rise of Kotlin Multiplatform, the possibility of expanding to multiple platforms has increased, especially for Android Developers. It's easier than before to build for other platforms.

But how to release your app to multiple platforms?

In this talk, I will share all the things I've learned around distributing FeedFlow, an Android, iOS, macOS, Windows, and Linux app built with Kotlin Multiplatform, coming from an Android development background.

We will cover the deployment of the binary, automating everything with CI, crash reporting, logging, internationalization, and all you need to know to successfully distribute your KMP app.

Avatar for Marco Gomiero

Marco Gomiero

September 04, 2025
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. Marco Gomiero marcogomiero.com Senior Android Engineer @ Airalo Google Developer

    Expert for Android The rollercoaster of releasing an Android, iOS, and desktop app with Kotlin Multiplatform
  2. marcogomiero.com • Sign the app • Provisioning profiles • Upload

    to TestFlight • All handled by Xcode (or manually on the CI) iOS
  3. marcogomiero.com Notarization $ xcrun notarytool submit $BINARY_PATH --apple-id $APPLE_ID_NOTARIZATION --password

    $NOTARIZATION_PWD --team-id $APPSTORE_TEAM_ID --wait $ xcrun stapler staple $BINARY_PATH
  4. marcogomiero.com macOS - App Store • Sign the app •

    Provisioning profiles • Upload to TestFlight
  5. marcogomiero.com compose { desktop { application { macOS { minimumSystemVersion

    = "12.0" } } } } } https://github.com/JetBrains/compose-multiplatform/pull/4271 Fixed with Compose 1.6.10
  6. marcogomiero.com Native libraries on JVM • Native libraries are embedded

    in the dependencies jar • When needed, they are extracted and loaded
  7. marcogomiero.com Native libraries on JVM • Cannot load “random” native

    library • Signing and embedding in the binary is the way
  8. marcogomiero.com val isSandboxed = System.getenv("APP_SANDBOX_CONTAINER_ID") != null if (isSandboxed) {

    val resourcesPath = System.getProperty("compose.application.resources.dir") // sqlite-jdbc System.setProperty("org.sqlite.lib.path", resourcesPath) } Load native libraries
  9. marcogomiero.com val isSandboxed = System.getenv("APP_SANDBOX_CONTAINER_ID") != null if (isSandboxed) {

    val resourcesPath = System.getProperty("compose.application.resources.dir") // sqlite-jdbc System.setProperty("org.sqlite.lib.path", resourcesPath) } Load native libraries
  10. marcogomiero.com val isSandboxed = System.getenv("APP_SANDBOX_CONTAINER_ID") != null if (isSandboxed) {

    val resourcesPath = System.getProperty("compose.application.resources.dir") // sqlite-jdbc System.setProperty("org.sqlite.lib.path", resourcesPath) } Load native libraries
  11. marcogomiero.com • Sign the app • Buy a certificate or

    self-sign and distribute one • Distribute the app Windows - “Manual” Distribution
  12. marcogomiero.com Windows - “Manual” Distribution • Sign the app •

    Buy a certificate or self-sign and distribute one • Distribute the app
  13. marcogomiero.com Windows - “Manual” Distribution • Sign the app •

    Buy a certificate or self-sign and distribute one • Distribute the app
  14. marcogomiero.com Windows - Microsoft Store https://learn.microsoft.com/en-us/windows/apps/publish/?tabs=individual%2Cmsix-pwa-getting-started • MSI or EXE

    • MSIX or PWA - > You handle binary storage and signing - > Microsoft handles binary storage and signing
  15. marcogomiero.com • Universal stores (Flathub, Snap Store, etc) • Distribution-specific

    (Ubuntu Software Center, GNOME Software, KDE Discover, etc) Linux - Stores
  16. marcogomiero.com . ├── bin │ └── feedflow ├── jre │

    ├── bin │ ├── conf │ ├── lib │ └── release ├── lib │ └── feedflow.jar ├── manifest.json └── share ├── app-info ├── applications │ └── com.prof18.feedflow.desktop ├── icons └── metainfo └── com.prof18.feedflow.metainfo.xml ~/.local/share/flatpak/app/com.prof18.feedflow/current/active/files/
  17. marcogomiero.com . ├── bin │ └── feedflow ├── jre │

    ├── bin │ ├── conf │ ├── lib │ └── release ├── lib │ └── feedflow.jar ├── manifest.json └── share ├── app-info ├── applications │ └── com.prof18.feedflow.desktop ├── icons └── metainfo └── com.prof18.feedflow.metainfo.xml ~/.local/share/flatpak/app/com.prof18.feedflow/current/active/files/
  18. marcogomiero.com . ├── bin │ └── feedflow ├── jre │

    ├── bin │ ├── conf │ ├── lib │ └── release ├── lib │ └── feedflow.jar ├── manifest.json └── share ├── app-info ├── applications │ └── com.prof18.feedflow.desktop ├── icons └── metainfo └── com.prof18.feedflow.metainfo.xml ~/.local/share/flatpak/app/com.prof18.feedflow/current/active/files/
  19. marcogomiero.com . ├── bin │ └── feedflow ├── jre │

    ├── bin │ ├── conf │ ├── lib │ └── release ├── lib │ └── feedflow.jar ├── manifest.json └── share ├── app-info ├── applications │ └── com.prof18.feedflow.desktop ├── icons └── metainfo └── com.prof18.feedflow.metainfo.xml ~/.local/share/flatpak/app/com.prof18.feedflow/current/active/files/
  20. app-id: com.prof18.feedflow runtime: org.freedesktop.Platform runtime-version: '24.08' sdk: org.freedesktop.Sdk sdk-extensions: -

    org.freedesktop.Sdk.Extension.openjdk21 command: feedflow finish-args: - --share=network - --share=ipc - --socket=x11 - --device=dri - --filesystem=xdg-documents:ro - --filesystem=xdg-download:rw modules: - name: openjdk buildsystem: simple build-commands: - /usr/lib/sdk/openjdk21/install.sh - name: feedflow buildsystem: simple build-options: append-path: "/usr/lib/sdk/openjdk21/bin" build-commands: - ./.scripts/flatpak-build-setup.sh - ./.scripts/disable-android-for-flatpak.sh - ./gradlew desktopApp:packageReleaseUberJarForCurrentOS --no-daemon --offline -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=2g" -Dorg.gradle.daemon=true -Dorg.gradle.parallel=false --no-configuration-cache - JAR_FILE=$(find desktopApp/build/compose/jars/ -name "*.jar" -type f | head -1) && install -Dm755 "$JAR_FILE" /app/lib/feedflow.jar - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.desktop /app/share/applications/com.prof18.feedflow.desktop - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.metainfo.xml /app/share/metainfo/com.prof18.feedflow.metainfo.xml - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.png /app/share/icons/hicolor/512x512/apps/com.prof18.feedflow.png - mkdir -p /app/bin - install -Dm755 desktopApp/packaging/flatpak/feedflow.sh /app/bin/feedflow sources: - type: git url: https://github.com/prof18/feed-flow tag: 1.3.3-linux - type: file url: "https://services.gradle.org/distributions/gradle-8.14.3-bin.zip" sha256: "bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531" dest: "gradle/wrapper" dest-filename: "gradle-bin.zip" - flatpak-sources.json - flatpak-sources-convention.json - flatpak-sources-manual.json - flatpak-sources-root.json Flatpak: how to package https://github.com/ fl athub/com.prof18.feed fl ow/blob/master/com.prof18.feed fl ow.yml
  21. system: simple -commands: usr/lib/sdk/openjdk21/install.sh feedflow system: simple -options: end-path: "/usr/lib/sdk/openjdk21/bin"

    build-commands: - ./.scripts/flatpak-build-setup.sh - ./.scripts/disable-android-for-flatpak.sh - ./gradlew desktopApp:packageReleaseUberJarForCurrentOS --no-daemon -- - JAR_FILE=$(find desktopApp/build/compose/jars/ -name "*.jar" -type f - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.deskt - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.metai - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.png / - mkdir -p /app/bin - install -Dm755 desktopApp/packaging/flatpak/feedflow.sh /app/bin/feed es: ype: git rl: https://github.com/prof18/feed-flow ag: 1.3.3-linux ype: file rl: "https://services.gradle.org/distributions/gradle-8.14.3-bin.zip" ha256: "bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531" est: "gradle/wrapper" est-filename: "gradle-bin.zip" latpak-sources.json latpak-sources-convention.json latpak-sources-manual.json latpak-sources-root.json
  22. system: simple -commands: usr/lib/sdk/openjdk21/install.sh feedflow system: simple -options: end-path: "/usr/lib/sdk/openjdk21/bin"

    build-commands: - ./.scripts/flatpak-build-setup.sh - ./.scripts/disable-android-for-flatpak.sh - ./gradlew desktopApp:packageReleaseUberJarForCurrentOS --no-daemon -- - JAR_FILE=$(find desktopApp/build/compose/jars/ -name "*.jar" -type f - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.deskt - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.metai - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.png / - mkdir -p /app/bin - install -Dm755 desktopApp/packaging/flatpak/feedflow.sh /app/bin/feed es: ype: git rl: https://github.com/prof18/feed-flow ag: 1.3.3-linux ype: file rl: "https://services.gradle.org/distributions/gradle-8.14.3-bin.zip" ha256: "bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531" est: "gradle/wrapper" est-filename: "gradle-bin.zip" latpak-sources.json latpak-sources-convention.json latpak-sources-manual.json latpak-sources-root.json
  23. system: simple -commands: usr/lib/sdk/openjdk21/install.sh feedflow system: simple -options: end-path: "/usr/lib/sdk/openjdk21/bin"

    build-commands: - ./.scripts/flatpak-build-setup.sh - ./.scripts/disable-android-for-flatpak.sh - ./gradlew desktopApp:packageReleaseUberJarForCurrentOS --no-daemon -- - JAR_FILE=$(find desktopApp/build/compose/jars/ -name "*.jar" -type f - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.deskt - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.metai - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.png / - mkdir -p /app/bin - install -Dm755 desktopApp/packaging/flatpak/feedflow.sh /app/bin/feed es: ype: git rl: https://github.com/prof18/feed-flow ag: 1.3.3-linux ype: file rl: "https://services.gradle.org/distributions/gradle-8.14.3-bin.zip" ha256: "bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531" est: "gradle/wrapper" est-filename: "gradle-bin.zip" latpak-sources.json latpak-sources-convention.json latpak-sources-manual.json latpak-sources-root.json
  24. append-path: "/usr/lib/sdk/openjdk21/bin" build-commands: - ./.scripts/flatpak-build-setup.sh - ./.scripts/disable-android-for-flatpak.sh - ./gradlew desktopApp:packageReleaseUberJarForCurrentOS

    --no-daemon --offline -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=2g" -Dorg.gradle.daemon=true -Dorg.gradle.parallel=false --no-configuration-cach - JAR_FILE=$(find desktopApp/build/compose/jars/ -name "*.jar" -type f | head -1) && install -Dm755 "$JAR_FILE" /app/lib/feedflow.jar - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.desktop /app/share/applications/com.prof18.feedflow.desktop - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.metainfo.xml /app/share/metainfo/com.prof18.feedflow.metainfo.xml - install -Dm644 desktopApp/packaging/flatpak/com.prof18.feedflow.png /app/share/icons/hicolor/512x512/apps/com.prof18.feedflow.png - mkdir -p /app/bin - install -Dm755 desktopApp/packaging/flatpak/feedflow.sh /app/bin/feedflow sources: - type: git url: https://github.com/prof18/feed-flow tag: 1.3.3-linux - type: file url: "https://services.gradle.org/distributions/gradle-8.14.3-bin.zip" sha256: "bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531" dest: "gradle/wrapper" dest-filename: "gradle-bin.zip" - flatpak-sources.json - flatpak-sources-convention.json - flatpak-sources-manual.json - flatpak-sources-root.json
  25. [ { "type": "file", "url": "https://dl.google.com/dl/android/maven2/androidx/annotation/annotation-jvm/1.9.1/annotation-jvm-1.9.1.jar", "sha512": "ee8cceeb09d0231f6de4015f078e8cb0805de6faf383a9653d5f3763c43bb137e5346c2b177972b1f70d2f648f6f32047051c0f3 "dest": "./offline-repository/androidx/annotation/annotation-jvm/1.9.1",

    "dest-filename": "annotation-jvm-1.9.1.jar" }, { "type": "file", "url": "https://dl.google.com/dl/android/maven2/androidx/annotation/annotation-jvm/1.9.1/annotation-jvm-1.9.1.modul "sha512": "7137d5297d7f9049996609eb7b82600aad85a461e46ab8fbb812e7946f399bbcffb81a8b425d2312c225f0b7660ebc245b40e43d "dest": "./offline-repository/androidx/annotation/annotation-jvm/1.9.1", "dest-filename": "annotation-jvm-1.9.1.module" } ]
  26. marcogomiero.com val logger = Logger( config = StaticConfig( logWriterList =

    listOf( platformLogWriter(), crashReportingLogWriter(), ), ), tag = "FeedFlow", )
  27. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) FeedFlowTheme { val lyricist = rememberFeedFlowStrings() ProvideFeedFlowStrings(lyricist) { MyAppEntrypoint() } }
  28. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) FeedFlowTheme { val lyricist = rememberFeedFlowStrings() ProvideFeedFlowStrings(lyricist) { MyAppEntrypoint() } } Text(text = LocalFeedFlowStrings.current.feedUrl)
  29. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) func startKoin() { let languageCode = Locale.current.language.languageCode?.identifier let regionCode = Locale.current.region?.identifier let koinApplication = doInitKoinIos( languageCode: languageCode, regionCode: regionCode ) _feedFlowStrings = KotlinDependencies.shared.getFeedFlowStrings() } private var _feedFlowStrings: FeedFlowStrings? var feedFlowStrings: FeedFlowStrings { return _feedFlowStrings! }
  30. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) func startKoin() { let languageCode = Locale.current.language.languageCode?.identifier let regionCode = Locale.current.region?.identifier let koinApplication = doInitKoinIos( languageCode: languageCode, regionCode: regionCode ) _feedFlowStrings = KotlinDependencies.shared.getFeedFlowStrings() } private var _feedFlowStrings: FeedFlowStrings? var feedFlowStrings: FeedFlowStrings { return _feedFlowStrings! }
  31. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) func startKoin() { let languageCode = Locale.current.language.languageCode?.identifier let regionCode = Locale.current.region?.identifier let koinApplication = doInitKoinIos( languageCode: languageCode, single<FeedFlowStrings> { when { languageCode == null -> EnFeedFlowStrings regionCode == null -> feedFlowStrings[languageCode] ?: EnFeedFlowStrings else -> { val locale = "${languageCode}_$regionCode" feedFlowStrings[locale] ?: feedFlowStrings[languageCode] ?: EnFeedFlowStrings } } }
  32. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) func startKoin() { let languageCode = Locale.current.language.languageCode?.identifier let regionCode = Locale.current.region?.identifier let koinApplication = doInitKoinIos( languageCode: languageCode, regionCode: regionCode ) _feedFlowStrings = KotlinDependencies.shared.getFeedFlowStrings() } private var _feedFlowStrings: FeedFlowStrings? var feedFlowStrings: FeedFlowStrings { return _feedFlowStrings! }
  33. marcogomiero.com val feedFlowStrings: Map<String, FeedFlowStrings> = mapOf( Locales.It to ItFeedFlowStrings,

    Locales.En to EnFeedFlowStrings, Locales.De to DeFeedFlowStrings, ) Text(feedFlowStrings.searchHintTitle) func startKoin() { let languageCode = Locale.current.language.languageCode?.identifier let regionCode = Locale.current.region?.identifier let koinApplication = doInitKoinIos( languageCode: languageCode,
  34. marcogomiero.com Conclusions • Start from things you already know •

    Go incrementally • Exploring other planets enriches your vision
  35. marcogomiero.com Thank you! Marco Gomiero > Website: marcogomiero.com > Bluesky:

    @marcogomiero.com > Twitter: @marcoGomier > Mastodon: androiddev.social/@marcogom > Github: prof18 Senior Android Engineer @ Airalo Google Developer Expert for Android
  36. Bibliography / Useful Links • https: / / feedflow.dev/ •

    https: / / developer.apple.com/documentation/security/notarizing_macos_software_before_distribution • https: / / help.apple.com/itc/transporteruserguide/en.lproj/static.html#apd70774093eddb4 • https: / / developer.apple.com/documentation/security/app_sandbox/protecting_user_data_with_app_sandbox • https: / / github.com/JetBrains/compose-multiplatform/blob/master/tutorials/Native_distributions_and_local_execution/README.md#adding-files-to-packaged-application • https: / / slack-chats.kotlinlang.org/t/16371916/hey-there-wave-i-m-trying-to-publish-my-app-on-the-mac-app-s#0be57312-7e43-4d30-add9-1cd5a0b7421b • https: / / firebase.google.com/products/crashlytics • https: / / docs.sentry.io/platforms/java/ • https: / / github.com/touchlab/CrashKiOS • https: / / github.com/touchlab/Kermit • https: / / github.com/adrielcafe/lyricist • https: / / www.jetbrains.com/help/kotlin-multiplatform-dev/compose-images-resources.html#strings • https: / / www.marcogomiero.com/posts/2024/kmp-ci-android • https: / / www.marcogomiero.com/posts/2024/kmp-ci-ios • https: / / www.marcogomiero.com/posts/2024/kmp-ci-macos-github-releases • https: / / www.marcogomiero.com/posts/2024/kmp-ci-macos-appstore • https: / / www.marcogomiero.com/posts/2024/compose-macos-app-store/ • https: / / learn.microsoft.com/en-us/windows/apps/publish/?tabs=individual%2Cmsix-pwa-getting-started • https: / / learn.microsoft.com/en-us/windows/msix/overview • https: / / docs.flatpak.org/en/latest/building-introduction.html