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

Hitchhiker’s Guide to Kotlin Multiplatform Libr...

Hitchhiker’s Guide to Kotlin Multiplatform Libraries

This talk will use the various Kotlin Multiplatform (KMP) samples I've been working on over the last 5+ years to provide a guided tour of some of the libraries/patterns used within them.

It will include overview and examples of how the following KMP/CMP libraries are used in the samples:
- Persistence: SQLDelight, Realm, Apollo Kotlin, 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
- AI: Generative AI SDK
- Packaging: Multiplatform Swift Package
- Compose Multiplatform: Window Size Class, Coil, KoalaPlot, Compose on iOS

It will also cover examples from the various clients which were developed using:
- SwiftUI (on iOS, watchOS, and macOS)
- Jetpack Compose (on Android and Wear OS)
- Compose Multiplatform on Desktop, Web (using Wasm) and iOS (also includes examples of sharing Compose UI across multiple platforms).

John O'Reilly

July 03, 2024
Tweet

More Decks by John O'Reilly

Other Decks in Technology

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 key libraries 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) • 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
  3. @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
  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 • 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
  5. @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)
  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 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() … }
  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) } } Ktor PeopleInSpace
  10. @joreilly PeopleInSpace 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 Ktor
  11. @joreilly @Serializable data class AstroResult(val message: String, val number: Int,

    val people: List<Assignment>) @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
  12. @joreilly class PeopleInSpaceApi(private val client: HttpClient) { 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 result.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 = name, craft = craft, personImageUrl = personImageUrl, personBio = 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 SwiftUI Clients Compose Clients Server Cache Shared KMP/CMP

    Code Repository (Apollo) Decompose Components Shared Compose UI
  20. @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
  21. @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
  22. @joreilly Queries.graphql query GetConferences{ conferences { id timezone days name

    timezone themeColor } } Apollo Kotlin - Read Conference List (query) Confetti
  23. @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().data.conferences Confetti
  24. @joreilly Apollo Kotlin - Add Bookmark (mutation) Confetti Bookmarks.graphql mutation

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

    AddBookmark($sessionId: String!) { addBookmark(sessionId: $sessionId) { sessionIds } } ConfettiRepository.kt apolloClient .mutation(AddBookmarkMutation(sessionId)) .execute()
  26. @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
  27. @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
  28. @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
  29. @joreilly Decompose - Confetti Components Confetti Home Component Sessions Component

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

    Speakers Component Bookmarks Component Venue Component UI UI UI UI UI
  31. @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
  32. @joreilly Decompose - HomeComponent class HomeComponent( componentContext: ComponentContext, val conference:

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

    conference: String ) : ComponentContext by componentContext { private val navigation = StackNavigation<Config>() val stack: Value<ChildStack < * , Child >> = childStack( source = navigation, serializer = Config.serializer(), initialConfiguration = Config.Sessions, childFactory = :: child ) ... }
  34. @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
  35. @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
  36. @joreilly Decompose - Compose UI code Children( stack = component.stack

    ) { when (val child = it.instance) { is HomeComponent.Child.Speakers - > SpeakersView(child.component) ... } } Confetti
  37. @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
  38. @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
  39. @joreilly Multiplatform Settings Confetti 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) } }
  40. @joreilly Multiplatform Settings Confetti androidMain singleOf( :: DataStoreSettings) { bind<FlowSettings>()

    } iosMain single<FlowSettings> { NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults).toFlowSettings() }
  41. @joreilly Coil Confetti AsyncImage( model = speaker.photoUrl, contentDescription = speaker.name,

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

    Realm DB View Models (KMP-ObservableViewModel) RemoteAPI (Ktor)
  43. @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
  44. @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
  45. @joreilly Realm Kotlin - Open Database val configuration = RealmConfiguration.create(schema

    = setOf(NetworkDb :: class)) val realm = Realm.open(configuration) BikeShare
  46. @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
  47. @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
  48. @joreilly class CountriesViewModelShared : ViewModel() { private val cityBikesRepository: CityBikesRepository

    by inject() @NativeCoroutinesState val countryList = cityBikesRepository.groupedNetworkList.map { ... }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) } BikeShare KMP-ObservableViewModel
  49. @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
  50. @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
  51. @joreilly @Composable fun CountryListScreen(countrySelected: (country: Country) -> Unit) { val

    viewModel = koinViewModel<CountriesViewModelShared>() val countryList by viewModel.countryList.collectAsState() LazyColumn { items(countryList) { country -> CountryView(country, countrySelected) } } } BikeShare KMP-ObservableViewModel - Compose
  52. @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
  53. @joreilly Multiplatform Swift Package BikeShare // build.gradle.kts multiplatformSwiftPackage { packageName("BikeShareKit")

    swiftToolsVersion("5.9") targetPlatforms { iOS { v("14") } macOS { v("12")} } }
  54. @joreilly ClimateTraceKMP Shared KMPCode SwiftUI/Compose Client Compose Clients Repository Server

    KMP-ObservableViewModel (Molecule) Shared Compose UI KStore DB RemoteAPI (Ktor)
  55. @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)
  56. @joreilly Voyager https://github.com/adrielcafe/voyager “Compose on Warp Speed! A multiplatform navigation

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

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

    fun Content() { val navigator = LocalNavigator.currentOrThrow CountryListView( .. . ) { country - > navigator.push(CountryEmissionsScreen(country)) } } }
  59. @joreilly Voyager @Composable override fun Content() { val navigator =

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

    = LocalNavigator.currentOrThrow Scaffold( topBar = { CenterAlignedTopAppBar( title = { ... }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon(ArrowBack, contentDescription = "Back") } } ) } ) { ... } }
  61. @joreilly Molecule private val events = MutableSharedFlow<CountryDetailsEvents>(capacity) val viewState: StateFlow<CountryDetailsUIState>

    = scope.launchMolecule( .. ) { CountryDetailsPresenter(events) } fun setYear(year: String) { events.tryEmit(CountryDetailsEvents.SetYear(year)) } fun setCountry(country: Country) { events.tryEmit(CountryDetailsEvents.SetCountry(country)) } ClimateTrace
  62. @joreilly Molecule @Composable fun CountryDetailsPresenter(events: Flow<CountryDetailsEvents>): CountryDetailsUIState { var uiState

    by remember { mutableStateOf<CountryDetailsUIState>(NoCountrySelected) } var selectedCountry by remember { mutableStateOf<Country?>(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
  63. @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
  64. @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
  65. @joreilly Material 3 Window Size Class ClimateTrace val windowSizeClass =

    calculateWindowSizeClass() if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { Column(Modifier.fillMaxWidth()) { ... } } else { Row(Modifier.fillMaxHeight()) { ... } }
  66. @joreilly Compose on iOS - Kotlin iosMain fun CountryListViewController(onCountryClicked: (country:

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

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

    let onCountryClicked: (Country) - > Void func makeUIViewController(context: Context) - > UIViewController { MainViewControllerKt.CountryListViewController { country in onCountryClicked(country) } } ... } ClimateTrace
  69. @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) } }
  70. @joreilly FantasyPremierLeague Shared KMP/CMP Code SwiftUI Clients Compose Clients Repository

    Server Room DB View Models (Jetpack) Shared Compose UI RemoteAPI (Ktor)
  71. @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)
  72. @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.”
  73. @joreilly open class PlayerListViewModel : ViewModel(), KoinComponent { private val

    repository: FantasyPremierLeagueRepository by inject() val playerListUIState: StateFlow<PlayerListUIState> = { ... }.stateIn(viewModelScope, SharingStarted.Eagerly, PlayerListUIState.Loading) } Jetpack ViewModel FantasyPremierLeague
  74. @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
  75. @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 )
  76. @joreilly Jetpack Room - Database FantasyPremierLeague @Database(entities = [Team ::

    class, Player : : class, GameFixture :: class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun fantasyPremierLeagueDao(): FantasyPremierLeagueDao ... }
  77. @joreilly Jetpack Room - DAO FantasyPremierLeague @Dao interface FantasyPremierLeagueDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertPlayerList(playerList: List<Player>) @Query("SELECT * FROM Player") fun getPlayerListAsFlow(): Flow<List<Player >> @Query("SELECT * FROM Player WHERE id = :id") suspend fun getPlayer(id: Int): Player // other queries }
  78. @joreilly Jetpack Room - Repository FantasyPremierLeague class FantasyPremierLeagueRepository { private

    val database: AppDatabase by inject() suspend fun writeDataToDb( ... database.fantasyPremierLeagueDao().insertFixtureList(fixtureList) } fun getPlayers(): Flow<List<Player >> { return database.fantasyPremierLeagueDao().getPlayerListAsFlow() } suspend fun getPlayer(id: Int): Player { return database.fantasyPremierLeagueDao().getPlayer(id) } }
  79. @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
  80. @joreilly Jetpack 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 = ",") } } }
  81. @joreilly Jetpack 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 } )
  82. @joreilly Jetpack 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, ... ) requireNotNull(documentDirectory).path + "/fpl.preferences_pb" }
  83. @joreilly “SKIE is a tool for Kotlin Multiplatform development that

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

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

    SwiftUI .task { for await playerListUIState in viewModel.playerListUIState { self.playerListUIState = playerListUIState } } FantasyPremierLeague
  86. @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
  87. @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
  88. @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.”
  89. @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()) } } } ) } }
  90. @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