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

Hitchhiker's Guide to Kotlin/Compose Multiplatf...

John O'Reilly
April 26, 2024
120

Hitchhiker's Guide to Kotlin/Compose Multiplatform Samples and Libraries (AndroidMakers 2024)

John O'Reilly

April 26, 2024
Tweet

Transcript

  1. @joreilly What we’ll cover The talk will use the various

    Kotlin/Compose Multiplatform samples I've been working on over the last 5+ years to provide a guided tour of a number of the libraries/patterns used within them.
  2. @joreilly • PeopleInSpace (https://github.com/joreilly/PeopleInSpace) • GalwayBus (https://github.com/joreilly/GalwayBus) • Confetti (https://github.com/joreilly/Confetti)

    • BikeShare (https://github.com/joreilly/BikeShare) • FantasyPremierLeague (https://github.com/joreilly/FantasyPremierLeague) • ClimateTrace (https://github.com/joreilly/ClimateTraceKMP) • GeminiKMP (https://github.com/joreilly/GeminiKMP) • MortyComposeKMM (https://github.com/joreilly/MortyComposeKMM) • StarWars (https://github.com/joreilly/StarWars) • WordMasterKMP (https://github.com/joreilly/WordMasterKMP) • Chip-8 (https://github.com/joreilly/chip-8) KMP/CMP Samples
  3. @joreilly • PeopleInSpace • Koin, Ktor, Kotlinx Serialization, SQLDelight, KMP-NativeCoroutines

    • Confetti • Apollo Kotlin, Decompose, MultiplatformSettings, Generative AI SDK , BuildKonfig, Markdown Renderer, Coil • BikeShare • KMM-ViewModel, Multiplatform Swift Package • FantasyPremierLeague • Jetpack ViewModel, Jetpack DataStore, Realm, SKIE, KoalaPlot • ClimateTrace • Voyager, Window Size Class, Compose on iOS, Jetpack Navigation KMP/CMP Samples
  4. @joreilly • Voyager • Koala Plot • Compose Treemap Chart

    • Material 3 Window Size Class • Compose ImageLoader • Coil • Markdown Renderer • File Picker • Material Kolor • Kotlinx Coroutines • Kotlinx Serialization • Kotlinx DateTime • Ktor • SQLDelight • Apollo Kotlin • Realm • Koin • Decompose • SKIE • KMMViewModel • Jetpack ViewModel • Jetpack DataStore • KMP-NativeCoroutines KMP Libraries CMP Libraries • Generative AI SDK • MultiplatformSettings • KMMBridge • Kermit Logging • Multiplatform Swift Package
  5. @joreilly PeopleInSpace Shared KMP Code SwiftUI Clients Compose Clients Repository

    Server DB • Koin • Ktor • SQLDelight • KMP-NativeCoroutines ViewModel ViewModel RemoteAPI
  6. @joreilly Koin expect fun platformModule(): Module fun commonModule() = module

    { singleOf(::createJson) singleOf(::createHttpClient) singleOf(::PeopleInSpaceApi) singleOf(::PeopleInSpaceRepository) } class PeopleInSpaceRepository : KoinComponent { private val peopleInSpaceApi: PeopleInSpaceApi by inject() … } PeopleInSpace
  7. @joreilly Koin expect fun platformModule(): Module fun commonModule() = module

    { singleOf(::createJson) singleOf(::createHttpClient) singleOf(::PeopleInSpaceApi) singleOf(::PeopleInSpaceRepository) } class PeopleInSpaceRepository : KoinComponent { private val peopleInSpaceApi: PeopleInSpaceApi by inject() … } PeopleInSpace
  8. @joreilly Koin actual fun platformModule() = module { single {

    AndroidSqliteDriver(PeopleInSpaceDatabase.Schema, get(), “peopleinspace.db") } single<HttpClientEngine> { Android.create() } } actual fun platformModule() = module { single {NativeSqliteDriver(PeopleInSpaceDatabase.Schema, “peopleinspace.db”) } single<HttpClientEngine> { Darwin.create() } } Android iOS PeopleInSpace
  9. @joreilly fun createJson() = Json { isLenient = true; ignoreUnknownKeys

    = true } fun createHttpClient(httpClientEngine: HttpClientEngine, json: Json) = HttpClient(httpClientEngine) { install(ContentNegotiation) { json(json) } install(Logging) { logger = Logger.DEFAULT level = LogLevel.INFO } } Ktor PeopleInSpace
  10. @joreilly actual fun platformModule() = module { single { AndroidSqliteDriver(PeopleInSpaceDatabase.Schema,

    get(), “peopleinspace.db") } single<HttpClientEngine> { Android.create() } } actual fun platformModule() = module { single {NativeSqliteDriver(PeopleInSpaceDatabase.Schema, “peopleinspace.db”) } single<HttpClientEngine> { Darwin.create() } } Android iOS PeopleInSpace
  11. @joreilly @Serializable data class AstroResult(val message: String, val number: Int,

    val people: List<Assignment>) @Serializable data class IssResponse(val message: String, val iss_position: IssPosition, val timestamp: Long) class PeopleInSpaceApi(val client: HttpClient, val baseUrl) { suspend fun fetchPeople() = client.get(“$baseUrl/astros.json").body<AstroResult>() suspend fun fetchISSPosition() = client.get(“$baseUrl/iss-now.json").body<IssResponse>() } Ktor PeopleInSpace
  12. @joreilly @Serializable data class AstroResult(val message: String, val number: Int,

    val people: List<Assignment>) @Serializable data class IssResponse(val message: String, val iss_position: IssPosition, val timestamp: Long) class PeopleInSpaceApi(val client: HttpClient, val baseUrl) { suspend fun fetchPeople() = client.get(“$baseUrl/astros.json").body<AstroResult>() suspend fun fetchISSPosition() = client.get(“$baseUrl/iss-now.json").body<IssResponse>() } Ktor PeopleInSpace
  13. @joreilly SQLDelight https://github.com/cashapp/sqldelight “SQLDelight generates typesafe Kotlin APIs from your

    SQL statements. It veri fi es your schema, statements, and migrations at compile-time” PeopleInSpace
  14. @joreilly CREATE TABLE People( name TEXT NOT NULL PRIMARY KEY,

    craft TEXT NOT NULL, personImageUrl TEXT, personBio TEXT ); insertItem: INSERT OR REPLACE INTO People(name, craft, personImageUrl, personBio) VALUES(?,?,?,?); selectAll: SELECT * FROM People; deleteAll: DELETE FROM People; SQLDelight PeopleInSpace
  15. @joreilly peopleInSpaceQueries.transaction { // delete existing entries peopleInSpaceQueries.deleteAll() // store

    results in DB people.forEach { peopleInSpaceQueries.insertItem( it.name, it.craft, it.personImageUrl, it.personBio ) } } SQLDelight - storing data PeopleInSpace
  16. @joreilly peopleInSpaceQueries.selectAll( // map result to Assignment data class mapper

    = { name, craft, personImageUrl, personBio -> Assignment(name, craft, personImageUrl, personBio) } ).asFlow().mapToList(Dispatchers.IO) SQLDelight - retrieving data PeopleInSpace
  17. @joreilly ALTER TABLE People ADD COLUMN personImageUrl TEXT; ALTER TABLE

    People ADD COLUMN personBio TEXT; SQLDelight - migration PeopleInSpace
  18. @joreilly KMP-NativeCoroutines @NativeCoroutines override fun pollISSPosition(): Flow<IssPosition> { ... }

    Kotlin shared code let sequence = asyncSequence(for: repository.pollISSPosition()) for try await data in sequence { self.issPosition = data } Swift PeopleInSpace
  19. @joreilly Confetti Shared KMP/CMP Code SwiftUI Clients Compose Clients Repository

    (Apollo) Server Cache Decompose Components Shared Compose UI • Apollo Kotlin • Decompose • MultiplatformSettings • Generative AI SDK • BuildKonfig • Markdown Renderer • Coil
  20. @joreilly Apollo Kotlin val sqlNormalizedCacheFactory = SqlNormalizedCacheFactory(getDatabaseName(conference, uid)) val memoryFirstThenSqlCacheFactory

    = MemoryCacheFactory(maxSize).chain(sqlNormalizedCacheFactory) ApolloClient.Builder() .serverUrl("https://confetti-app.dev/graphql") .addHttpHeader("conference", conference) .normalizedCache( memoryFirstThenSqlCacheFactory, writeToCacheAsynchronously = writeToCacheAsynchronously ) .build() Confetti
  21. @joreilly Queries.graphql query GetConferences{ conferences { id timezone days name

    timezone themeColor } } Apollo Kotlin - Read Conference List (query) ConfettiRepository.kt val conferences: List<Conference> = apolloClient .query(GetConferencesQuery()) .execute() Confetti
  22. @joreilly Apollo Kotlin - Add Bookmark (mutation) Confetti Bookmarks.graphql mutation

    AddBookmark($sessionId: String!) { addBookmark(sessionId: $sessionId) { sessionIds } } ConfettiRepository.kt apolloClient.mutation(AddBookmarkMutation(sessionId)) .tokenProvider(user) .execute()
  23. @joreilly Decompose https://github.com/arkivanov/Decompose “Decompose is a Kotlin Multiplatform library for

    breaking down your code into lifecycle-aware business logic components (aka BLoC), with routing functionality and pluggable UI” Confetti
  24. @joreilly Decompose - Confetti Components Confetti AppComponent Conference Component Conferences

    Component Home Component SessionDetails Component SpeakerDetails Component Settings Component Sessions Component Speakers Component Bookmarks Component Venue Component
  25. @joreilly Decompose - Confetti Components Confetti AppComponent Conference Component Conferences

    Component Home Component SessionDetails Component SpeakerDetails Component Settings Component Sessions Component Speakers Component Bookmarks Component Venue Component
  26. @joreilly Decompose - Confetti Components Confetti Home Component Sessions Component

    Speakers Component Bookmarks Component Venue Component
  27. @joreilly Decompose - Confetti Components Confetti Home Component Sessions Component

    Speakers Component Bookmarks Component Venue Component UI UI UI UI UI
  28. @joreilly Decompose - HomeComponent sealed class Child { class Sessions(val

    component: SessionsComponent) : Child() class Speakers(val component: SpeakersComponent) : Child() class Bookmarks(val component: BookmarksComponent) : Child() class Venue(val component: VenueComponent) : Child() } Confetti
  29. @joreilly Decompose - HomeComponent class HomeComponent( componentContext: ComponentContext, private val

    conference: String, ... ) : ComponentContext by componentContext { private val navigation = StackNavigation<Config>() val stack = childStack( source = navigation, serializer = Config.serializer(), initialConfiguration = Config.Sessions, childFactory = ::child ) ... Confetti
  30. @joreilly Decompose - HomeComponent class HomeComponent( componentContext: ComponentContext, private val

    conference: String, ... ) : ComponentContext by componentContext { private val navigation = StackNavigation<Config>() val stack = childStack( source = navigation, serializer = Config.serializer(), initialConfiguration = Config.Sessions, childFactory = ::child ) ... Confetti
  31. @joreilly Decompose - HomeComponent ... private fun child(config: Config, cx:

    ComponentContext): Child = when (config) { Config.Speakers -> Child.Speakers( SpeakersComponent(componentContext,conference, onSpeakerSelected = { navigation.push(Config.SpeakerDetails(speakerId = it)) } )) ... } } fun onSpeakersTabClicked() { navigation.bringToFront(Config.Speakers) } } Confetti
  32. @joreilly Decompose - SpeakersComponent class SpeakersComponent( componentContext: ComponentContext, conference: String,

    private val onSpeakerSelected: (id: String) -> Unit, ) : KoinComponent, ComponentContext by componentContext { private val coroutineScope = coroutineScope() private val repository: ConfettiRepository by inject() val uiState: Value<SpeakersUiState> = … fun onSpeakerClicked(id: String) { onSpeakerSelected(id) } } Confetti
  33. @joreilly Decompose - SpeakersComponent class SpeakersComponent( componentContext: ComponentContext, conference: String,

    private val onSpeakerSelected: (id: String) -> Unit, ) : KoinComponent, ComponentContext by componentContext { private val coroutineScope = coroutineScope() private val repository: ConfettiRepository by inject() val uiState: Value<SpeakersUiState> = … fun onSpeakerClicked(id: String) { onSpeakerSelected(id) } } Confetti
  34. @joreilly Decompose - Compose UI code Children( stack = component.stack,

    modifier = modifier, animation = stackAnimation(fade()), ) { when (val child = it.instance) { is HomeComponent.Child.Sessions -> SessionsRoute(component = child.component) is HomeComponent.Child.Speakers -> SpeakersRoute(component = child.component) ... } } Confetti
  35. @joreilly Decompose - SwiftUI UI code stack = StateValue(component.stack) var

    body: some View { ... let child = stack.active.instance switch child { case let child as HomeComponentChild.Sessions: SessionsView(child.component) case let child as HomeComponentChild.Speakers: SpeakersView(child.component) ... } } Confetti
  36. @joreilly Multiplatform Settings commonMain class AppSettings(val settings: FlowSettings) { ...

    suspend fun setConference(conference: String) { settings.putString(CONFERENCE_SETTING, conference) } fun getConferenceFlow(): Flow<String> { return settings.getStringFlow(CONFERENCE_SETTING, CONFERENCE_NOT_SET) } } Confetti
  37. @joreilly Multiplatform Settings commonMain class AppSettings(val settings: FlowSettings) { ...

    suspend fun setConference(conference: String) { settings.putString(CONFERENCE_SETTING, conference) } fun getConferenceFlow(): Flow<String> { return settings.getStringFlow(CONFERENCE_SETTING, CONFERENCE_NOT_SET) } } Confetti
  38. @joreilly Multiplatform Settings androidMain singleOf(::DataStoreSettings) { bind<FlowSettings>() } Confetti iosMain

    single<ObservableSettings> { NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) } single { get<ObservableSettings>().toFlowSettings() }
  39. @joreilly Multiplatform Settings Confetti androidMain singleOf(::DataStoreSettings) { bind<FlowSettings>() } iosMain

    single<ObservableSettings> { NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) } single { get<ObservableSettings>().toFlowSettings() }
  40. @joreilly Generative AI SDK for KMP https://github.com/PatilShreyas/generative-ai-kmp “The Google Generative

    AI client SDK for Kotlin Multiplatform enables developers to use Google's state- of-the-art generative AI models (like Gemini) to build AI- powered features and application” Confetti
  41. @joreilly BuildKon fi g https://github.com/yshrsmz/BuildKon fi g “BuildCon fi g

    for Kotlin Multiplatform Project” Confetti Markdown Renderer https://github.com/mikepenz/multiplatform-markdown-renderer “Markdown renderer for Kotlin Multiplatform Projects (Android, iOS, Desktop), using Compose.”
  42. @joreilly Generative AI SDK for KMP Confetti class GeminiApi {

    private val apiKey = BuildKonfig.GEMINI_API_KEY val generativeModel = GenerativeModel( modelName = "gemini-pro", apiKey = apiKey ) fun generateContent(prompt: String): Flow<GenerateContentResponse> { return generativeModel.generateContentStream(prompt) } ... }
  43. @joreilly Generative AI SDK for KMP Confetti var sessionsInfo =

    "Speakers,Session ID,Title,\n" sessions.forEach { session -> session.sessionDescription?.let { val speakers = session.speakers.joinToString(" ") { it.speakerDetails.name } sessionsInfo += "$speakers, ${session.id}, ${session.title},\n" } } val basePrompt = """ I would like to learn about $query. Which talks should I attend? Show me the session ids in the result as comma delimited list. Do not use markdown in the result. """ val prompt = "$basePrompt Base on the following CSV: $sessionsInfo}"
  44. @joreilly Generative AI SDK for KMP Confetti var sessionsInfo =

    "Speakers,Session ID,Title,\n" sessions.forEach { session -> session.sessionDescription?.let { val speakers = session.speakers.joinToString(" ") { it.speakerDetails.name } sessionsInfo += "$speakers, ${session.id}, ${session.title},\n" } } val basePrompt = """ I would like to learn about $query. Which talks should I attend? Show me the session ids in the result as comma delimited list. Do not use markdown in the result. """ val prompt = "$basePrompt Base on the following CSV: $sessionsInfo}"
  45. @joreilly Coil Confetti AsyncImage( model = speaker.photoUrl, contentDescription = speaker.name,

    contentScale = ContentScale.Crop, modifier = Modifier.size(64.dp) .clip(CircleShape) )
  46. @joreilly BikeShare Shared KMPCode SwiftUI Clients Compose Clients Repository Server

    Realm DB View Models (KMM-ViewModel) • KMM-ViewModel • Multiplatform Swift Package
  47. @joreilly KMM-ViewModel open class CountriesViewModelShared : KMMViewModel(), KoinComponent { private

    val cityBikesRepository: CityBikesRepository by inject() @NativeCoroutinesState val countryList = cityBikesRepository.groupedNetworkList.map { ... Country(countryCode, getCountryName(countryCode)) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) } BikeShare
  48. @joreilly KMM-ViewModel - iOS struct ContentView : View { @StateViewModel

    var viewModel = CountriesViewModelShared() var body: some View { List { ForEach(viewModel.countryList) { country in CountryView(country: country) } } } } BikeShare
  49. @joreilly KMM-ViewModel - iOS struct ContentView : View { @StateViewModel

    var viewModel = CountriesViewModelShared() var body: some View { List { ForEach(viewModel.countryList) { country in CountryView(country: country) } } } } BikeShare
  50. @joreilly KMM-ViewModel - Android @Composable fun CountryListScreen() { val viewModel

    = koinViewModel<CountriesViewModelShared>() val countryList by viewModel.countryList.collectAsState() LazyColumn { items(countryList) { country -> CountryView(country) } } } BikeShare
  51. @joreilly Multiplatform Swift Package https://github.com/luca992/multiplatform-swiftpackage “Gradle plugin that generates a

    Swift Package Manager manifest and an XCFramework to distribute a Kotlin Multiplatform library for Apple platforms” BikeShare
  52. @joreilly Multiplatform Swift Package BikeShare // build.gradle.kts multiplatformSwiftPackage { packageName("BikeShareKit")

    swiftToolsVersion("5.9") targetPlatforms { iOS { v("14") } macOS { v("12")} } }
  53. @joreilly FantasyPremierLeague Shared KMP/CMP Code SwiftUI Clients Compose Clients Repository

    Server Realm DB View Models (androidx) • Jetpack ViewModel • Jetpack DataStore • Realm • SKIE • KoalaPlot Shared Compose UI
  54. @joreilly class PlayerListViewModel : ViewModel(), KoinComponent { private val repository:

    FantasyPremierLeagueRepository by inject() val playerListUIState: StateFlow<PlayerListUIState> = ... } Jetpack ViewModel FantasyPremierLeague
  55. @joreilly “Jetpack DataStore is a data storage solution that allows

    you to store key-value pairs or typed objects with protocol bu ff ers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally.” DataStore https://developer.android.com/topic/libraries/architecture/datastore FantasyPremierLeague
  56. @joreilly DataStore FantasyPremierLeague class AppSettings(private val dataStore: DataStore<Preferences>) { val

    LEAGUES_SETTING = stringPreferencesKey("leagues") val leagues: Flow<List<String>> = dataStore.data.map { preferences -> getLeaguesSettingFromString(preferences[LEAGUES_SETTING]) } suspend fun updatesLeaguesSetting(leagues: List<String>) { dataStore.edit { preferences -> preferences[LEAGUES_SETTING] = leagues.joinToString(separator = ",") } } ... }
  57. @joreilly DataStore FantasyPremierLeague // androidMain actual fun platformModule() = module

    { ... single { dataStore(get())} } fun dataStore(context: Context): DataStore<Preferences> = createDataStore( producePath = { context.filesDir.resolve("fpl.preferences_pb").absolutePath } )
  58. @joreilly DataStore FantasyPremierLeague // iOSMain actual fun platformModule() = module

    { ... single { dataStore()} } fun dataStore(): DataStore<Preferences> = createDataStore( producePath = { val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory, inDomain = NSUserDomainMask, appropriateForURL = null, create = false, error = null, ) requireNotNull(documentDirectory).path + "/fpl.preferences_pb" } )
  59. @joreilly “Realm Kotlin makes persisting, querying, and syncing data as

    simple as working with objects in your data model, with idiomatic APIs that integrate directly with Coroutines and Flows.” Realm Kotlin https://github.com/realm/realm-kotlin FantasyPremierLeague
  60. @joreilly class FixtureDb: RealmObject { @PrimaryKey var id: Int =

    0 var kickoffTime: String? = "" var homeTeam: TeamDb? = null var awayTeam: TeamDb? = null var homeTeamScore: Int = 0 var awayTeamScore: Int = 0 var event: Int = 0 } Realm Kotlin FantasyPremierLeague
  61. @joreilly Realm Kotlin - storing data FantasyPremierLeague realm.write { ...

    val teams = query<TeamDb>().find().toList() // store fixtures fixtures.forEach { fixtureDto -> copyToRealm(FixtureDb().apply { id = fixtureDto.id kickoffTime = fixtureDto.kickoff_time ... homeTeam = teams.find { it.index == fixtureDto.team_h } awayTeam = teams.find { it.index == fixtureDto.team_a } }, updatePolicy = UpdatePolicy.ALL) } }
  62. @joreilly Realm Kotlin - storing data FantasyPremierLeague realm.write { ...

    val teams = query<TeamDb>().find().toList() // store fixtures fixtures.forEach { fixtureDto -> copyToRealm(FixtureDb().apply { id = fixtureDto.id kickoffTime = fixtureDto.kickoff_time ... homeTeam = teams.find { it.index == fixtureDto.team_h } awayTeam = teams.find { it.index == fixtureDto.team_a } }, updatePolicy = UpdatePolicy.ALL) } }
  63. @joreilly “SKIE is a tool for Kotlin Multiplatform development that

    enhances the Swift API published from Kotlin.” SKIE https://github.com/touchlab/SKIE FantasyPremierLeague
  64. @joreilly SKIE - Suspend Functions // Kotlin - LeaguesViewModel suspend

    fun getEventStatus(): List<EventStatusDto> { return repository.getEventStatus().status } // SwiftUI try await eventStatusList = viewModel.getEventStatus() FantasyPremierLeague
  65. @joreilly SKIE - Flows // Kotlin - PlayerListViewModel val playerListUIState:

    StateFlow<PlayerListUIState> // SwiftUI .task { for await playerListUIState in viewModel.playerListUIState { self.playerListUIState = playerListUIState } } FantasyPremierLeague
  66. @joreilly SKIE - Sealed Classes (Kotlin) sealed class PlayerListUIState {

    object Loading : PlayerListUIState() data class Error(val message: String) : PlayerListUIState() data class Success(val result: List<Player>) : PlayerListUIState() } FantasyPremierLeague
  67. @joreilly SKIE - Sealed Classes (Swift) switch onEnum(of: playerListUIState) {

    case .loading: ProgressView() .progressViewStyle(CircularProgressViewStyle()) case .error(let error): Text("Error: \(error)") case .success(let success): List(success.result) { player in PlayerView(player: player) } } FantasyPremierLeague
  68. @joreilly KoalaPlot https://github.com/KoalaPlot/koalaplot-core FantasyPremierLeague “Koala Plot is a Compose Multiplatform

    based charting and plotting library allowing you to build great looking interactive charts for Android, desktop, ios, and web using a single API and code base.”
  69. @joreilly KoalaPlot FantasyPremierLeague ChartLayout( title = { ChartTitle(title) }) {

    XYChart( ... ) { VerticalBarChart( series = listOf(barChartEntries.value), bar = { _, _, value -> DefaultVerticalBar( brush = SolidColor(Color.Blue), modifier = Modifier.fillMaxWidth(barWidth), ) { HoverSurface { Text(value.yMax.toString()) } } } ) } }
  70. @joreilly Shared KMPCode SwiftUI Clients Compose Clients Repository Server View

    Models (KMM-ViewModel) • Voyager • Window Size Class • Compose on iOS • Jetpack Navigation Shared Compose UI ClimateTraceKMP
  71. @joreilly Voyager https://github.com/adrielcafe/voyager “Compose on Warp Speed! A multiplatform navigation

    library built for, and seamlessly integrated with, Jetpack Compose” ClimateTrace
  72. @joreilly Voyager class CountryListScreen : Screen { @Composable override fun

    Content() { val navigator = LocalNavigator.currentOrThrow CountryListView(...) { country -> navigator.push(CountryEmissionsScreen(country)) } } } ClimateTrace
  73. @joreilly Voyager class CountryListScreen : Screen { @Composable override fun

    Content() { val navigator = LocalNavigator.currentOrThrow CountryListView(...) { country -> navigator.push(CountryEmissionsScreen(country)) } } } ClimateTrace
  74. @joreilly Voyager data class CountryEmissionsScreen(val country: Country) : Screen {

    @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow Scaffold( topBar = { CenterAlignedTopAppBar( title = {...}, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon(ArrowBack, contentDescription = "Back") } } ) } ) { ... } } } ClimateTrace
  75. @joreilly Voyager data class CountryEmissionsScreen(val country: Country) : Screen {

    @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow Scaffold( topBar = { CenterAlignedTopAppBar( title = {...}, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon(ArrowBack, contentDescription = "Back") } } ) } ) { ... } } } ClimateTrace
  76. @joreilly Material 3 Window Size Class https://github.com/chrisbanes/material3-windowsizeclass-multiplatform “set of opinionated

    breakpoints, the window size at which a layout needs to change to match available space, device conventions, and ergonomics” ClimateTrace
  77. @joreilly Material 3 Window Size Class ClimateTrace val windowSizeClass =

    calculateWindowSizeClass() if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { Column(Modifier.fillMaxWidth()) { Box(Modifier.height(300.dp).fillMaxWidth())) { CountryListView(…) } Spacer(modifier = Modifier.width(1.dp).fillMaxWidth()) CountryInfoDetailedView(...) } } else { Row(Modifier.fillMaxSize()) { Box(Modifier.width(250.dp).fillMaxHeight())) { CountryListView(...) } Spacer(modifier = Modifier.width(1.dp).fillMaxHeight()) CountryInfoDetailedView(...) } }
  78. @joreilly Material 3 Window Size Class ClimateTrace val windowSizeClass =

    calculateWindowSizeClass() if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { Column(Modifier.fillMaxWidth()) { Box(Modifier.height(300.dp).fillMaxWidth())) { CountryListView(…) } Spacer(modifier = Modifier.width(1.dp).fillMaxWidth()) CountryInfoDetailedView(...) } } else { Row(Modifier.fillMaxSize()) { Box(Modifier.width(250.dp).fillMaxHeight())) { CountryListView(...) } Spacer(modifier = Modifier.width(1.dp).fillMaxHeight()) CountryInfoDetailedView(...) } }
  79. @joreilly Compose on iOS - Kotlin iosMain fun CountryListViewController(onCountryClicked: (country:

    Country) -> Unit) = ComposeUIViewController { val viewModel = koinInject<ClimateTraceViewModel>() val countryList by viewModel.countryList.collectAsState() CountryListView(countryList) { onCountryClicked(it) } } ClimateTrace
  80. @joreilly Compose on iOS - Kotlin iosMain fun CountryListViewController(onCountryClicked: (country:

    Country) -> Unit) = ComposeUIViewController { val viewModel = koinInject<ClimateTraceViewModel>() val countryList by viewModel.countryList.collectAsState() CountryListView(countryList) { onCountryClicked(it) } } ClimateTrace
  81. @joreilly Compose on iOS - SwiftUI struct CountryListViewShared: UIViewControllerRepresentable {

    let onCountryClicked: (Country) -> Void func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.CountryListViewController { country in onCountryClicked(country) } } ... } ClimateTrace
  82. @joreilly Jetpack Navigation ClimateTrace val navController = rememberNavController() ... composable(route

    = "countryList") { CountryListView(countryList.value, selectedCountry.value) { country -> navController.navigate("details/${country.name}/${country.alpha3}") } } composable("details/{countryName}/{countryCode}",) { backStackEntry -> val countryName = backStackEntry.arguments?.getString("countryName") ?: "" val countryCode = backStackEntry.arguments?.getString("countryCode") ?: "" ,,, CountryInfoDetailedView(country, viewModel.year, countryEmissionInfo, countryAssetEmissions) }