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

Compose Multiplatform to the City

Compose Multiplatform to the City

This talk walks through building scalable, secure, and accessible digital experiences using JetBrains Compose Multiplatform, focusing on real-world architecture and tooling decisions made while building KRAIL, a public transport app for Sydney.

From project structure to shared UI, networking with Ktor, database integration using SQLDelight, theming, and crash monitoring, this session showcases how Kotlin Multiplatform can power beautiful and reliable apps across Android, iOS, and desktop—all from a single codebase.

Whether you're new to Compose Multiplatform or scaling your shared UI strategy, this session blends practical code, architecture, and debugging insights with a strong emphasis on developer experience and user trust.

Avatar for Karan Sharma

Karan Sharma

June 07, 2025
Tweet

Other Decks in Programming

Transcript

  1. GOAL AGENDA • Project Structure • Networking • Database •

    Theming • Crash Monitoring • Analytics
  2. > composeApp Android iOS Shared > androidMain > commonMain >

    iosMain > src build.gradle.kts > iosApp > iosApp ContentView.swift Info.plist > iosApp.xcodeproj iOSApp.swift PROJECT STRUCTURE
  3. iOS @main struct iOSApp: App { init() { KoinAppKt.doInitKoin() }

    var body: some Scene { WindowGroup { ContentView() } } } > iosApp > iosApp ContentView.swift Info.plist > iosApp.xcodeproj iOSApp.swift > composeApp > androidMain > commonMain > iosMain > src Android Shared build.gradle.kts iosApp.swift PROJECT STRUCTURE
  4. struct ContentView: View { var body: some View { ComposeView().ignoresSafeArea()

    } } struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.MainViewController() } } ContentView.swift > iosApp > iosApp ContentView.swift Info.plist > iosApp.xcodeproj iOSApp.swift > composeApp > androidMain > commonMain > iosMain > src build.gradle.kts Android Shared MainViewController.kt import androidx.compose.ui.window.ComposeUIViewController fun MainViewController() = ComposeUIViewController { KrailApp() } MainViewController.kt iOS PROJECT STRUCTURE
  5. > iosApp > iosApp ContentView.swift Info.plist > iosApp.xcodeproj class MainApplication

    : Application() { override fun onCreate() { super.onCreate() initKoin { androidContext(this@MainApplication) } } } MainApplication.kt iOSApp.swift class MainActivity : ComponentActivity() { override fun onCreate( savedInstanceState: Bundle?, ) { super.onCreate(savedInstanceState) setContent { KrailApp() } } } MainActivity.kt > composeApp > androidMain > commonMain > iosMain > src build.gradle.kts MainApplication.kt MainActivity.kt Android Shared iOS PROJECT STRUCTURE
  6. > iosApp > iosApp ContentView.swift Info.plist > iosApp.xcodeproj @Composable fun

    KrailApp() { KrailTheme { NavHost() } } KrailApp.kt iOSApp.swift > composeApp > androidMain > commonMain > iosMain > src build.gradle.kts Android Shared KrailApp.kt iOS PROJECT STRUCTURE
  7. COMPOSE MULTIPLATFORM > androidMain > commonMain Java Bytecode Kotlin compiler

    Dalvik Executable (DEX) APK (DEX + resources) ANDROID
  8. COMPOSE MULTIPLATFORM > iosMain > commonMain LLVM IR Kotlin /

    Native compiler Native Machine Code (arm64 / x86) Framework or Static Library iOS
  9. 1. Source set contains Platform independent code. Can be used

    on any target platform (e.g., Android, iOS, JVM, JS). 2. Common functionalities that do not depend on platform-specific APIs. UI / Networking / Database commonMain > androidMain > commonMain > iosMain > src build.gradle.kts Shared
  10. 1. Source set contains iOS platform specific implementations and APIs.

    2. Interacts with iOS specific features e.g. UIKit, CoreData, or other iOS frameworks iosMain > androidMain > commonMain > iosMain > src build.gradle.kts iOS ALL THE WAY
  11. 1. Source set contains android platform specific implementations and APIs.

    2. Interacts with Android specific features. Context, WorkManager APIs etc. androidMain > androidMain > commonMain > iosMain > src build.gradle.kts Android
  12. NETWORKIN G Use KTOR HTTP Client to SEND requests &

    RECEIVE network responses import io.ktor.client.* val client = HttpClient()
  13. NETWORKING Platform Engine Android Android, OkHttp implementation("io.ktor:ktor-client-okhttp:$ktor") import io.ktor.client.engine.okhttp.* actual

    val client = HttpClient(OkHttp) Native Darwin iOS / macOS implementation("io.ktor:ktor-client-darwin:$ktor") import io.ktor.client.engine.darwin.* actual val client = HttpClient(Darwin) KTOR expect val httpClient: HttpClient implementation("io.ktor:ktor-client-core:$ktor")
  14. NETWORKING Platform Engine Android Android, OkHttp implementation("io.ktor:ktor-client-okhttp:$ktor") import io.ktor.client.engine.okhttp.* actual

    val client = HttpClient(OkHttp) Native Darwin iOS / macOS implementation("io.ktor:ktor-client-darwin:$ktor") import io.ktor.client.engine.darwin.* actual val client = HttpClient(Darwin) KTOR expect val httpClient: HttpClient commonMain implementation("io.ktor:ktor-client-core:$ktor") iosMain androidMain
  15. NETWORKING KTOR commonMain iosMain androidMain actual fun httpClient(): HttpClient {

    return HttpClient(Darwin) { expectSuccess = true install(ContentNegotiation) { json() } install(Logging) { LogLevel.BODY } } } actual fun httpClient(): HttpClient { return HttpClient(OkHttp) { install(ContentNegotiation) { protobuf() } install(Logging) { LogLevel.BODY } } } expect fun httpClient(): HttpClient
  16. NETWORKING KTOR commonMain val tripResponse: TripResponse = httpClient.get("$BASEURL/v1/trip") { url

    { parameters.append("PARAM", value) } }.body() @Serializable data class TripResponse( @SerialName("trips") val tripsList: List<Trip>? = null, --. )
  17. db-sqlAndroidDriver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" }

    androidMain iosMain db-sqlNativeDriver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } commonMain db-sqlRuntime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } SQLDELIGH T DATABASE
  18. import android.content.Context import app.cash.sqldelight.driver.android.AndroidSqliteDriver AndroidSqliteDriver( schema = Database.Schema, context =

    context, name = "DbName.db", ) androidMain import app.cash.sqldelight.driver.native.NativeSqliteDriver NativeSqliteDriver( schema = Database.Schema, name = "DbName.db", ) iosMain DATABASE SQLDELIGH T
  19. SQLDELIGH T -- Saved Trip Table -- CREATE TABLE SavedTrip

    ( tripId TEXT PRIMARY KEY, fromStopId TEXT NOT NULL, fromStopName TEXT NOT NULL, toStopId TEXT NOT NULL, toStopName TEXT NOT NULL, timestamp TEXT DEFAULT (datetime('now')) ); Database.sq DatabaseQueries.kt Database.kt (auto-gen) DATABASE
  20. -- Saved Trip Table -- CREATE TABLE SavedTrip ( tripId

    TEXT PRIMARY KEY, fromStopId TEXT NOT NULL, fromStopName TEXT NOT NULL, toStopId TEXT NOT NULL, toStopName TEXT NOT NULL, timestamp TEXT DEFAULT (datetime('now')) ); Database.sq insertOrReplaceTrip: INSERT OR REPLACE INTO SavedTrip( tripId, fromStopId, fromStopName, toStopId, toStopName, timestamp ) VALUES (?, ?, ?, ?, ?, datetime('now')); SUMMARY SQLDELIGH T private val db = Database(factory.createDriver()) private val query = db.databaseQueries query.insertOrReplaceTrip( tripId, fromStopId, fromStopName, toStopId, toStopName, ) commonMain
  21. COLOURS @Composable fun KrailTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable

    () -> Unit, ) { val krailColors = when { darkTheme -> KrailDarkColors else -> KrailLightColors } CompositionLocalProvider( LocalKrailColors provides krailColors, LocalKrailTypography provides krailTypography, content = content, ) } USER INTERFACE @Immutable data class KrailColors( val label: Color, val surface: Color, val onSurface: Color, val alert: Color, )
  22. TYPOGRAPHY @Composable fun KrailTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable

    () -> Unit, ) { val krailColors = when { darkTheme -> KrailDarkColors else -> KrailLightColors } CompositionLocalProvider( LocalKrailColors provides krailColors, LocalKrailTypography provides krailTypography, content = content, ) } USER INTERFACE @Immutable data class KrailTypography( val body: TextStyle, val title: TextStyle, val display: TextStyle, val headline: TextStyle, val label: TextStyle, val title: TextStyle, )
  23. THEMING @Composable fun KrailTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable

    () -> Unit, ) { val krailColors = when { darkTheme -> KrailDarkColors else -> KrailLightColors } CompositionLocalProvider( LocalKrailColors provides krailColors, LocalKrailTypography provides krailTypography, content = content, ) } USER INTERFACE
  24. import app.krail.ui.generated.resources.ic_star painterResource(Res.drawable.ic_star) RESOURCES / ICONS USER INTERFACE > src

    > main > res > drawable > src > commonMain > commonResources > drawable Multiplatform Android import app.krail.ui.R painterResource(R.drawable.ic_star) compose.resources { nameOfResClass = "MyRes" }
  25. POPULAR STOPS Central Town HallParramatt a Redfern West Ryde Epping

    Wynyard Blacktown Wentworthville Seven Hills Rouse Hill Waitara Campbelltow n Rosewood Burwood Villawood
  26. https://github.com/JetBrains/compose-multiplatform WHAT’S NEW IN COMPOSE MULTIPLATFORM iOS • A11y improvements

    • Drag and Drop • Predictive Back Handler • Deeplinking Across platforms • Variable fonts • Type-safe Navigation • Shared element transitions • Previews for commonMain Material3 adaptive and material3-window-size-class now in common code