Slide 1

Slide 1 text

Hitchhiker's Guide to Kotlin Multiplatform Libraries Droidcon Berlin 2024 John O’Reilly (@joreilly), Software Engineer, Neat

Slide 2

Slide 2 text

@joreilly

Slide 3

Slide 3 text

@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 key libraries used within them.

Slide 4

Slide 4 text

@joreilly

Slide 5

Slide 5 text

@joreilly

Slide 6

Slide 6 text

@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) • ClimateTrace (https://github.com/joreilly/ClimateTraceKMP) • FantasyPremierLeague (https://github.com/joreilly/FantasyPremierLeague) • 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

Slide 7

Slide 7 text

@joreilly • PeopleInSpace • Koin, Ktor, Kotlinx Serialization, SQLDelight, KMP-NativeCoroutines • Confetti • Apollo Kotlin, Decompose, MultiplatformSettings, Coil • BikeShare • Realm, KMP-ObservableViewModel, Multiplatform Swift Package • ClimateTrace • Voyager, Molecule, Window Size Class, Compose on iOS, Jetpack Navigation • FantasyPremierLeague • Jetpack ViewModel, Jetpack Room, Jetpack DataStore, SKIE, KoalaPlot KMP/CMP Samples

Slide 8

Slide 8 text

@joreilly

Slide 9

Slide 9 text

@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 • KStore • Koin • Decompose • Molecule • SKIE Kotlin/Compose Multiplatform Libraries Used • Jetpack ViewModel • Jetpack Room • Jetpack DataStore • Jetpack Navigation • Jetpack Paging • KMP-NativeCoroutines • KMP-ObservableViewModel • Generative AI SDK • MultiplatformSettings • KMMBridge • Multiplatform Swift Package • Kermit Logging

Slide 10

Slide 10 text

@joreilly Kotlin Multiplatform Libraries by Year

Slide 11

Slide 11 text

@joreilly PeopleInSpace Confetti BikeShare ClimateTrace FantasyPremierLeague

Slide 12

Slide 12 text

@joreilly PeopleInSpace

Slide 13

Slide 13 text

@joreilly PeopleInSpace

Slide 14

Slide 14 text

@joreilly PeopleInSpace Shared KMP Code SwiftUI Clients Compose Clients Repository Server DB ViewModel ViewModel RemoteAPI (Ktor)

Slide 15

Slide 15 text

@joreilly PeopleInSpace Covering in this section: • Koin • Ktor • SQLDelight • KMP-NativeCoroutines Shared KMP Code SwiftUI Clients Compose Clients Repository Server DB ViewModel ViewModel RemoteAPI (Ktor)

Slide 16

Slide 16 text

@joreilly Koin https://github.com/InsertKoinIO/koin “A pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform” PeopleInSpace

Slide 17

Slide 17 text

@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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

@joreilly Koin actual fun platformModule() = module { single { AndroidSqliteDriver(PeopleInSpaceDatabase.Schema, get(), "peopleinspace.db") } single { Android.create() } } actual fun platformModule() = module { single { NativeSqliteDriver(PeopleInSpaceDatabase.Schema, "peopleinspace.db") } single { Darwin.create() } } Android iOS PeopleInSpace

Slide 20

Slide 20 text

@joreilly Ktor https://github.com/ktorio/ktor “Framework for quickly creating connected applications in Kotlin with minimal e ff ort” PeopleInSpace

Slide 21

Slide 21 text

@joreilly fun createJson() = Json { isLenient = true; ignoreUnknownKeys = true } fun createHttpClient(httpClientEngine: HttpClientEngine, json: Json) = HttpClient(httpClientEngine) { install(ContentNegotiation) { json(json) } } Ktor PeopleInSpace

Slide 22

Slide 22 text

@joreilly PeopleInSpace actual fun platformModule() = module { single { AndroidSqliteDriver(PeopleInSpaceDatabase.Schema, get(), "peopleinspace.db") } single { Android.create() } } actual fun platformModule() = module { single { NativeSqliteDriver(PeopleInSpaceDatabase.Schema, "peopleinspace.db") } single { Darwin.create() } } Android iOS Ktor

Slide 23

Slide 23 text

@joreilly @Serializable data class AstroResult(val message: String, val number: Int, val people: List) @Serializable data class Assignment(val craft: String, val name: String, var personImageUrl: String? = "", var personBio: String? = "") @Serializable data class IssPosition(val latitude: Double, val longitude: Double) Ktor PeopleInSpace

Slide 24

Slide 24 text

@joreilly class PeopleInSpaceApi(private val client: HttpClient) { suspend fun fetchPeople() = client.get(“$baseUrl/astros.json").body() suspend fun fetchISSPosition() = client.get("$baseUrl/iss-now.json").body() } Ktor PeopleInSpace

Slide 25

Slide 25 text

@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

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

@joreilly peopleInSpaceQueries.transaction { // delete existing entries peopleInSpaceQueries.deleteAll() // store results in DB result.people.forEach { peopleInSpaceQueries.insertItem( it.name, it.craft, it.personImageUrl, it.personBio ) } } SQLDelight - storing data PeopleInSpace

Slide 28

Slide 28 text

@joreilly peopleInSpaceQueries.selectAll( // map result to Assignment data class mapper = { name, craft, personImageUrl, personBio -> Assignment(name = name, craft = craft, personImageUrl = personImageUrl, personBio = personBio) } ) ?. asFlow().mapToList(Dispatchers.IO) SQLDelight - retrieving data PeopleInSpace

Slide 29

Slide 29 text

@joreilly ALTER TABLE People ADD COLUMN personImageUrl TEXT; ALTER TABLE People ADD COLUMN personBio TEXT; SQLDelight - migration PeopleInSpace

Slide 30

Slide 30 text

@joreilly KMP-NativeCoroutines https://github.com/rickclephas/KMP-NativeCoroutines “A library to use Kotlin Coroutines from Swift code in KMP apps” PeopleInSpace

Slide 31

Slide 31 text

@joreilly KMP-NativeCoroutines @NativeCoroutines override fun pollISSPosition(): Flow { ... } Kotlin shared code let sequence = asyncSequence(for: repository.pollISSPosition()) for try await data in sequence { self.issPosition = data } Swift PeopleInSpace

Slide 32

Slide 32 text

@joreilly PeopleInSpace Confetti BikeShare ClimateTrace FantasyPremierLeague

Slide 33

Slide 33 text

@joreilly Confetti

Slide 34

Slide 34 text

@joreilly Confetti

Slide 35

Slide 35 text

@joreilly Confetti

Slide 36

Slide 36 text

@joreilly Confetti SwiftUI Clients Compose Clients Server Cache Shared KMP/CMP Code Repository (Apollo) Decompose Components Shared Compose UI

Slide 37

Slide 37 text

@joreilly Confetti SwiftUI Clients Compose Clients Server Cache Shared KMP/CMP Code Repository (Apollo) Decompose Components Shared Compose UI Covering in this section: • Apollo Kotlin • Decompose • MultiplatformSettings • Coil

Slide 38

Slide 38 text

@joreilly Apollo Kotlin - GraphQL https://github.com/apollographql/apollo-kotlin “A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.” Confetti

Slide 39

Slide 39 text

@joreilly Apollo Kotlin val memoryFirstThenSqlCacheFactory = MemoryCacheFactory(maxSize).chain(sqlNormalizedCacheFactory) val apolloClient = ApolloClient.Builder() .serverUrl("https: / / confetti-app.dev/graphql") .addHttpHeader("conference", conference) .normalizedCache(memoryFirstThenSqlCacheFactory) .build() Confetti

Slide 40

Slide 40 text

@joreilly Queries.graphql query GetConferences{ conferences { id timezone days name timezone themeColor } } Apollo Kotlin - Read Conference List (query) Confetti

Slide 41

Slide 41 text

@joreilly Queries.graphql query GetConferences{ conferences { id timezone days name timezone themeColor } } Apollo Kotlin - Read Conference List (query) ConfettiRepository.kt val conferences: List = apolloClient .query(GetConferencesQuery()) .execute().data.conferences Confetti

Slide 42

Slide 42 text

@joreilly Apollo Kotlin - Add Bookmark (mutation) Confetti Bookmarks.graphql mutation AddBookmark($sessionId: String!) { addBookmark(sessionId: $sessionId) { sessionIds } }

Slide 43

Slide 43 text

@joreilly Apollo Kotlin - Add Bookmark (mutation) Confetti Bookmarks.graphql mutation AddBookmark($sessionId: String!) { addBookmark(sessionId: $sessionId) { sessionIds } } ConfettiRepository.kt apolloClient .mutation(AddBookmarkMutation(sessionId)) .execute()

Slide 44

Slide 44 text

@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

Slide 45

Slide 45 text

@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

Slide 46

Slide 46 text

@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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

@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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

@joreilly Decompose - HomeComponent ... private fun child(config: Config, componentContext: ComponentContext): Child = when (config) { Config.Speakers -> Child.Speakers( SpeakersComponent( componentContext = componentContext, conference = conference, onSpeakerSelected = onSpeakerSelected, ) ) ... } } fun onSpeakersTabClicked() { navigation.bringToFront(Config.Speakers) } Confetti

Slide 53

Slide 53 text

@joreilly Decompose - HomeComponent ... private fun child(config: Config, componentContext: ComponentContext): Child = when (config) { Config.Speakers -> Child.Speakers( SpeakersComponent( componentContext = componentContext, conference = conference, onSpeakerSelected = onSpeakerSelected, ) ) ... } } fun onSpeakersTabClicked() { navigation.bringToFront(Config.Speakers) } Confetti

Slide 54

Slide 54 text

@joreilly Decompose - Compose UI code Children( stack = component.stack ) { when (val child = it.instance) { is HomeComponent.Child.Speakers - > SpeakersView(child.component) ... } } Confetti

Slide 55

Slide 55 text

@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.Speakers: SpeakersView(child.component) ... } } Confetti

Slide 56

Slide 56 text

@joreilly Multiplatform Settings https://github.com/russhwolf/multiplatform-settings “A Kotlin library for Multiplatform apps, so that common code can persist key-value data” Confetti

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

@joreilly Multiplatform Settings Confetti androidMain singleOf( :: DataStoreSettings) { bind() } iosMain single { NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults).toFlowSettings() }

Slide 60

Slide 60 text

@joreilly Multiplatform Settings Confetti iosMain single { NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults).toFlowSettings() } androidMain singleOf( :: DataStoreSettings) { bind() }

Slide 61

Slide 61 text

@joreilly Coil https://github.com/coil-kt/coil “Image loading for Android and Compose Multiplatform.” Confetti

Slide 62

Slide 62 text

@joreilly Coil Confetti AsyncImage( model = speaker.photoUrl, contentDescription = speaker.name, contentScale = ContentScale.Crop, modifier = Modifier.size(64.dp) .clip(CircleShape) )

Slide 63

Slide 63 text

@joreilly PeopleInSpace Confetti BikeShare ClimateTrace FantasyPremierLeague

Slide 64

Slide 64 text

@joreilly BikeShare

Slide 65

Slide 65 text

@joreilly BikeShare

Slide 66

Slide 66 text

@joreilly BikeShare Shared KMPCode SwiftUI Clients Compose Clients Repository Server Realm DB View Models (KMP-ObservableViewModel) RemoteAPI (Ktor)

Slide 67

Slide 67 text

@joreilly Covering in this section: • Realm Kotlin • KMP-ObservableViewModel • Multiplatform Swift Package Shared KMPCode SwiftUI Clients Compose Clients Repository Server Realm DB View Models (KMP-ObservableViewModel) RemoteAPI (Ktor) BikeShare

Slide 68

Slide 68 text

@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 BikeShare

Slide 69

Slide 69 text

@joreilly Realm Kotlin - Open Database val configuration = RealmConfiguration.create(schema = setOf(NetworkDb :: class)) val realm = Realm.open(configuration) BikeShare

Slide 70

Slide 70 text

@joreilly class NetworkDb: RealmObject { @PrimaryKey var id: String = "" var name: String = "" var city: String = "" var country: String = "" var latitude: Double = 0.0 var longitude: Double = 0.0 } Realm Kotlin BikeShare

Slide 71

Slide 71 text

@joreilly Realm Kotlin - storing data realm.write { networkList.forEach { networkDto -> copyToRealm(NetworkDb().apply { id = networkDto.id name = networkDto.name city = networkDto.location.city country = networkDto.location.country latitude = networkDto.location.latitude longitude = networkDto.location.longitude }, updatePolicy = UpdatePolicy.ALL) } } BikeShare

Slide 72

Slide 72 text

@joreilly Realm Kotlin - reading data BikeShare realm.query().asFlow()

Slide 73

Slide 73 text

@joreilly KMP-ObservableViewModel https://github.com/rickclephas/KMP-ObservableViewModel “Library to use AndroidX/Kotlin ViewModels with SwiftUI” BikeShare

Slide 74

Slide 74 text

@joreilly class CountriesViewModelShared : ViewModel() { private val cityBikesRepository: CityBikesRepository by inject() @NativeCoroutinesState val countryList = cityBikesRepository.groupedNetworkList.map { ... }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) } BikeShare KMP-ObservableViewModel

Slide 75

Slide 75 text

@joreilly struct ContentView : View { @StateViewModel var viewModel = CountriesViewModelShared() var body: some View { List { ForEach(viewModel.countryList) { country in CountryView(country: country) } } } } BikeShare KMP-ObservableViewModel - SwiftUI

Slide 76

Slide 76 text

@joreilly struct ContentView : View { @StateViewModel var viewModel = CountriesViewModelShared() var body: some View { List { ForEach(viewModel.countryList) { country in CountryView(country: country) } } } } BikeShare KMP-ObservableViewModel - SwiftUI

Slide 77

Slide 77 text

@joreilly @Composable fun CountryListScreen(countrySelected: (country: Country) -> Unit) { val viewModel = koinViewModel() val countryList by viewModel.countryList.collectAsState() LazyColumn { items(countryList) { country -> CountryView(country, countrySelected) } } } BikeShare KMP-ObservableViewModel - Compose

Slide 78

Slide 78 text

@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

Slide 79

Slide 79 text

@joreilly Multiplatform Swift Package BikeShare // build.gradle.kts multiplatformSwiftPackage { packageName("BikeShareKit") swiftToolsVersion("5.9") targetPlatforms { iOS { v("14") } macOS { v("12")} } }

Slide 80

Slide 80 text

@joreilly Multiplatform Swift Package BikeShare https://github.com/joreilly/BikeShareSwiftPackage

Slide 81

Slide 81 text

@joreilly BikeShare

Slide 82

Slide 82 text

@joreilly KMMBridge https://kmmbridge.touchlab.co/docs/

Slide 83

Slide 83 text

@joreilly PeopleInSpace Confetti BikeShare ClimateTrace FantasyPremierLeague

Slide 84

Slide 84 text

@joreilly ClimateTraceKMP

Slide 85

Slide 85 text

@joreilly ClimateTraceKMP

Slide 86

Slide 86 text

@joreilly ClimateTraceKMP Shared KMPCode SwiftUI/Compose Client Compose Clients Repository Server KMP-ObservableViewModel (Molecule) Shared Compose UI KStore DB RemoteAPI (Ktor)

Slide 87

Slide 87 text

@joreilly Covering in this section: • Voyager • Molecule • Window Size Class • Compose on iOS • Jetpack Navigation ClimateTraceKMP Shared KMPCode SwiftUI/Compose Client Compose Clients Repository Server Shared Compose UI KStore DB RemoteAPI (Ktor) KMP-ObservableViewModel (Molecule)

Slide 88

Slide 88 text

@joreilly Voyager https://github.com/adrielcafe/voyager “Compose on Warp Speed! A multiplatform navigation library built for, and seamlessly integrated with, Jetpack Compose” ClimateTrace

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

@joreilly Molecule https://github.com/cashapp/molecule “Build a StateFlow or Flow stream using Compose” ClimateTrace

Slide 94

Slide 94 text

@joreilly Molecule private val events = MutableSharedFlow(capacity) val viewState: StateFlow = scope.launchMolecule( .. ) { CountryDetailsPresenter(events) } fun setYear(year: String) { events.tryEmit(CountryDetailsEvents.SetYear(year)) } fun setCountry(country: Country) { events.tryEmit(CountryDetailsEvents.SetCountry(country)) } ClimateTrace

Slide 95

Slide 95 text

@joreilly Molecule @Composable fun CountryDetailsPresenter(events: Flow): CountryDetailsUIState { var uiState by remember { mutableStateOf(NoCountrySelected) } var selectedCountry by remember { mutableStateOf(null) } var selectedYear by remember { mutableStateOf("2022") } LaunchedEffect(Unit) { events.collect { event - > when (event) { is CountryDetailsEvents.SetCountry - > selectedCountry = event.country is CountryDetailsEvents.SetYear - > selectedYear = event.year } } } .. ClimateTrace

Slide 96

Slide 96 text

@joreilly Molecule .. LaunchedEffect(selectedCountry, selectedYear) { selectedCountry ?. let { country -> uiState = CountryDetailsUIState.Loading try { val countryEmissionInfo = repository.fetchCountryEmissionsInfo( .. ) val countryAssetEmissionsList = repository.fetchCountryAssetEmissionsInfo( .. ) uiState = CountryDetailsUIState.Success(country, selectedYear, countryEmissionInfo, countryAssetEmissionsList) } catch (e: Exception) { uiState = CountryDetailsUIState.Error("Error retrieving data from backend") } } } return uiState } ClimateTrace

Slide 97

Slide 97 text

@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

Slide 98

Slide 98 text

@joreilly Material 3 Window Size Class ClimateTrace

Slide 99

Slide 99 text

@joreilly ClimateTrace

Slide 100

Slide 100 text

@joreilly Material 3 Window Size Class ClimateTrace val windowSizeClass = calculateWindowSizeClass() if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { Column(Modifier.fillMaxWidth()) { ... } } else { Row(Modifier.fillMaxHeight()) { ... } }

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

@joreilly Compose on iOS - SwiftUI struct CountryListViewShared: UIViewControllerRepresentable { let onCountryClicked: (Country) - > Void func makeUIViewController(context: Context) - > UIViewController { MainViewControllerKt.CountryListViewController { country in onCountryClicked(country) } } ... } ClimateTrace

Slide 104

Slide 104 text

@joreilly ClimateTrace Jetpack Navigation https://developer.android.com/guide/navigation

Slide 105

Slide 105 text

@joreilly Jetpack Navigation ClimateTrace val navController = rememberNavController() ... composable(route = "countryList") { CountryListView(countryList.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) } }

Slide 106

Slide 106 text

@joreilly PeopleInSpace Confetti BikeShare ClimateTrace FantasyPremierLeague

Slide 107

Slide 107 text

@joreilly FantasyPremierLeague

Slide 108

Slide 108 text

@joreilly FantasyPremierLeague

Slide 109

Slide 109 text

@joreilly FantasyPremierLeague Shared KMP/CMP Code SwiftUI Clients Compose Clients Repository Server Room DB View Models (Jetpack) Shared Compose UI RemoteAPI (Ktor)

Slide 110

Slide 110 text

@joreilly Shared KMP/CMP Code SwiftUI Clients Compose Clients Repository Server Room DB View Models (Jetpack) Shared Compose UI Covering in this section: • Jetpack ViewModel • Jetpack Roon • Jetpack DataStore • SKIE • KoalaPlot FantasyPremierLeague RemoteAPI (Ktor)

Slide 111

Slide 111 text

@joreilly Jetpack ❤ Kotlin Multiplatform!! FantasyPremierLeague

Slide 112

Slide 112 text

@joreilly Jetpack ❤ Kotlin Multiplatform!! FantasyPremierLeague

Slide 113

Slide 113 text

@joreilly Jetpack ViewModel https://developer.android.com/topic/libraries/architecture/viewmodel FantasyPremierLeague “The ViewModel class is a business logic or screen level state holder. It exposes state to the UI and encapsulates related business logic. Its principal advantage is that it caches state and persists it through con fi guration changes.”

Slide 114

Slide 114 text

@joreilly open class PlayerListViewModel : ViewModel(), KoinComponent { private val repository: FantasyPremierLeagueRepository by inject() val playerListUIState: StateFlow = { ... }.stateIn(viewModelScope, SharingStarted.Eagerly, PlayerListUIState.Loading) } Jetpack ViewModel FantasyPremierLeague

Slide 115

Slide 115 text

@joreilly “The Room persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.” Jetpack Room https://developer.android.com/kotlin/multiplatform/room FantasyPremierLeague

Slide 116

Slide 116 text

@joreilly Jetpack Room - Entity FantasyPremierLeague @Entity data class Player( @PrimaryKey val id: Int, val name: String, val team: String, val photoUrl: String, val points: Int, val currentPrice: Double, val goalsScored: Int, val assists: Int )

Slide 117

Slide 117 text

@joreilly Jetpack Room - Database FantasyPremierLeague @Database(entities = [Team :: class, Player : : class, GameFixture :: class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun fantasyPremierLeagueDao(): FantasyPremierLeagueDao ... }

Slide 118

Slide 118 text

@joreilly Jetpack Room - DAO FantasyPremierLeague @Dao interface FantasyPremierLeagueDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertPlayerList(playerList: List) @Query("SELECT * FROM Player") fun getPlayerListAsFlow(): Flow> @Query("SELECT * FROM Player WHERE id = :id") suspend fun getPlayer(id: Int): Player // other queries }

Slide 119

Slide 119 text

@joreilly Jetpack Room - Repository FantasyPremierLeague class FantasyPremierLeagueRepository { private val database: AppDatabase by inject() suspend fun writeDataToDb( ... database.fantasyPremierLeagueDao().insertFixtureList(fixtureList) } fun getPlayers(): Flow> { return database.fantasyPremierLeagueDao().getPlayerListAsFlow() } suspend fun getPlayer(id: Int): Player { return database.fantasyPremierLeagueDao().getPlayer(id) } }

Slide 120

Slide 120 text

@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.” Jetpack DataStore https://developer.android.com/topic/libraries/architecture/datastore FantasyPremierLeague

Slide 121

Slide 121 text

@joreilly Jetpack DataStore FantasyPremierLeague class AppSettings(private val dataStore: DataStore) { val LEAGUES_SETTING = stringPreferencesKey("leagues") val leagues: Flow> = dataStore.data.map { preferences - > getLeaguesSettingFromString(preferences[LEAGUES_SETTING]) } suspend fun updatesLeaguesSetting(leagues: List) { dataStore.edit { preferences - > preferences[LEAGUES_SETTING] = leagues.joinToString(separator = ",") } } }

Slide 122

Slide 122 text

@joreilly Jetpack DataStore FantasyPremierLeague androidMain actual fun platformModule() = module { ... single { dataStore(get())} } fun dataStore(context: Context): DataStore = createDataStore( producePath = { context.filesDir.resolve("fpl.preferences_pb").absolutePath } )

Slide 123

Slide 123 text

@joreilly Jetpack DataStore FantasyPremierLeague iOSMain actual fun platformModule() = module { .. single { dataStore()} } fun dataStore(): DataStore = createDataStore( producePath = { val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory, inDomain = NSUserDomainMask, ... ) requireNotNull(documentDirectory).path + "/fpl.preferences_pb" }

Slide 124

Slide 124 text

@joreilly “SKIE is a tool for Kotlin Multiplatform development that enhances the Swift API published from Kotlin.” SKIE https://github.com/touchlab/SKIE FantasyPremierLeague

Slide 125

Slide 125 text

@joreilly SKIE FantasyPremierLeague

Slide 126

Slide 126 text

@joreilly SKIE - Suspend Functions Kotlin - LeaguesViewModel suspend fun getEventStatus(): List { return repository.getEventStatus().status } SwiftUI try await eventStatusList = viewModel.getEventStatus() FantasyPremierLeague

Slide 127

Slide 127 text

@joreilly SKIE - Flows Kotlin - PlayerListViewModel val playerListUIState: StateFlow SwiftUI .task { for await playerListUIState in viewModel.playerListUIState { self.playerListUIState = playerListUIState } } FantasyPremierLeague

Slide 128

Slide 128 text

@joreilly SKIE - Sealed Classes (Kotlin) sealed class PlayerListUIState { object Loading : PlayerListUIState() data class Error(val message: String) : PlayerListUIState() data class Success(val result: List) : PlayerListUIState() } FantasyPremierLeague

Slide 129

Slide 129 text

@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

Slide 130

Slide 130 text

@joreilly SKIE - Sealed Classes (Swift) FantasyPremierLeague

Slide 131

Slide 131 text

@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.”

Slide 132

Slide 132 text

@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()) } } } ) } }

Slide 133

Slide 133 text

@joreilly Summary - Persistence SQLDelight, Realm, Apollo Kotlin, Jetpack Room, Multiplatform Settings, Jetpack DataStore - Remote API requests Ktor, Apollo Kotlin - Dependency Injection Koin - ViewModel/Component sharing KMP-ObservableViewModel, Jetpack ViewModel, Decompose,Molecule - Navigation Voyager, Jetpack Navigation - Swift/Kotlin interop KMP-NativeCoroutines, SKIE - Packaging Multiplatform Swift Package, KMMBridge - Compose UI Multiplatform Window Size Class, Coil, KoalaPlot, Compose on iOS

Slide 134

Slide 134 text

@joreilly Questions