Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

The rollercoaster of releasing an Android, iOS,...

The rollercoaster of releasing an Android, iOS, and macOS app with Kotlin Multiplatform | Berlindroid November

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, and macOS 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.

Marco Gomiero

November 27, 2024
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

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

    Expert for Kotlin The rollercoaster of releasing an Android, iOS, and macOS 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 • Sign the app • Provisioning profiles • Upload

    to TestFlight • All handled by Xcode (or manually on the CI) iOS
  4. @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
  5. @marcogomiero.com macOS - App Store • Sign the app •

    Provisioning profiles • Upload to TestFlight
  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 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) }
  8. @marcogomiero.com val logger = Logger( config = StaticConfig( logWriterList =

    listOf( platformLogWriter(), crashReportingLogWriter(), ), ), tag = "FeedFlow", )
  9. @marcogomiero.com val logger = Logger( config = StaticConfig( logWriterList =

    listOf( platformLogWriter(), crashReportingLogWriter(), ), ), tag = "FeedFlow", )
  10. @marcogomiero.com val logger = Logger( config = StaticConfig( logWriterList =

    listOf( platformLogWriter(), crashReportingLogWriter(), ), ), tag = "FeedFlow", )
  11. @marcogomiero.com internal expect fun crashReportingLogWriter(): LogWriter actual fun crashReportingLogWriter(): LogWriter

    = CrashlyticsLogWriter() actual fun crashReportingLogWriter(): LogWriter = SentryLogWriter()
  12. @marcogomiero.com class SentryLogWriter : LogWriter() { override fun isLoggable(tag: String,

    severity: Severity): Boolean { // .. } override fun log( severity: Severity, message: String, tag: String, throwable: Throwable? ) { // .. } }
  13. @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() } }
  14. @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)
  15. @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! }
  16. @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! }
  17. @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 } } }
  18. @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 } } }
  19. @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 } } }
  20. @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 } } }
  21. @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! }
  22. @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,
  23. @marcogomiero.com Conclusions • Start from things you already know •

    Go incrementally • Exploring other planets enriches your vision
  24. @marcogomiero.com Thank you! Marco Gomiero Senior Android Developer @ Airalo

    Google Developer Expert for Kotlin > Twitter: @marcoGomier > Github: prof18 > Website: marcogomiero.com > Mastodon: androiddev.social/@marcogom > BlueSky: @marcogomiero.com
  25. 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/