Slide 1

Slide 1 text

Hitchhiker's Guide to Kotlin/Compose Multiplatform Samples and Libraries 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 libraries/patterns 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) • 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

Slide 7

Slide 7 text

@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

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 • Koin • Decompose • SKIE • KMMViewModel • Jetpack ViewModel • Jetpack DataStore • KMP-NativeCoroutines KMP Libraries CMP Libraries • Generative AI SDK • MultiplatformSettings • KMMBridge • Kermit Logging • Multiplatform Swift Package

Slide 10

Slide 10 text

@joreilly https://blog.jetbrains.com/kotlin/2023/11/kotlin-multiplatform-stable/

Slide 11

Slide 11 text

@joreilly PeopleInSpace

Slide 12

Slide 12 text

@joreilly PeopleInSpace

Slide 13

Slide 13 text

@joreilly PeopleInSpace Shared KMP Code SwiftUI Clients Compose Clients Repository Server DB • Koin • Ktor • SQLDelight • KMP-NativeCoroutines ViewModel ViewModel RemoteAPI

Slide 14

Slide 14 text

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

Slide 15

Slide 15 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 16

Slide 16 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 17

Slide 17 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 18

Slide 18 text

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

Slide 19

Slide 19 text

@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

Slide 20

Slide 20 text

@joreilly 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 21

Slide 21 text

@joreilly @Serializable data class AstroResult(val message: String, val number: Int, val people: List) @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() suspend fun fetchISSPosition() = client.get(“$baseUrl/iss-now.json").body() } Ktor PeopleInSpace

Slide 22

Slide 22 text

@joreilly @Serializable data class AstroResult(val message: String, val number: Int, val people: List) @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() suspend fun fetchISSPosition() = client.get(“$baseUrl/iss-now.json").body() } Ktor PeopleInSpace

Slide 23

Slide 23 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 24

Slide 24 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 25

Slide 25 text

@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

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 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 30

Slide 30 text

@joreilly Confetti

Slide 31

Slide 31 text

@joreilly Confetti

Slide 32

Slide 32 text

@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

Slide 33

Slide 33 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 34

Slide 34 text

@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

Slide 35

Slide 35 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() Confetti

Slide 36

Slide 36 text

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

Slide 37

Slide 37 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 38

Slide 38 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 39

Slide 39 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 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 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 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

@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

Slide 46

Slide 46 text

@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 = … fun onSpeakerClicked(id: String) { onSpeakerSelected(id) } } Confetti

Slide 47

Slide 47 text

@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 = … fun onSpeakerClicked(id: String) { onSpeakerSelected(id) } } Confetti

Slide 48

Slide 48 text

@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

Slide 49

Slide 49 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.Sessions: SessionsView(child.component) case let child as HomeComponentChild.Speakers: SpeakersView(child.component) ... } } Confetti

Slide 50

Slide 50 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 51

Slide 51 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 52

Slide 52 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 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

@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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

@joreilly Generative AI SDK for KMP Confetti

Slide 58

Slide 58 text

@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 { return generativeModel.generateContentStream(prompt) } ... }

Slide 59

Slide 59 text

@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}"

Slide 60

Slide 60 text

@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}"

Slide 61

Slide 61 text

@joreilly Generative AI SDK for KMP Confetti

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

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 (KMM-ViewModel) • KMM-ViewModel • Multiplatform Swift Package

Slide 67

Slide 67 text

@joreilly KMM-ViewModel https://github.com/rickclephas/KMM-ViewModel “A library that allows you to share ViewModels between Android and iOS” BikeShare

Slide 68

Slide 68 text

@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

Slide 69

Slide 69 text

@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

Slide 70

Slide 70 text

@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

Slide 71

Slide 71 text

@joreilly KMM-ViewModel - Android @Composable fun CountryListScreen() { val viewModel = koinViewModel() val countryList by viewModel.countryList.collectAsState() LazyColumn { items(countryList) { country -> CountryView(country) } } } BikeShare

Slide 72

Slide 72 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 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

@joreilly BikeShare

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

@joreilly FantasyPremierLeague

Slide 78

Slide 78 text

@joreilly FantasyPremierLeague

Slide 79

Slide 79 text

@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

Slide 80

Slide 80 text

@joreilly Jetpack ViewModel https://developer.android.com/topic/libraries/architecture/viewmodel FantasyPremierLeague

Slide 81

Slide 81 text

@joreilly class PlayerListViewModel : ViewModel(), KoinComponent { private val repository: FantasyPremierLeagueRepository by inject() val playerListUIState: StateFlow = ... } Jetpack ViewModel FantasyPremierLeague

Slide 82

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

Slide 83

Slide 83 text

@joreilly 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 84

Slide 84 text

@joreilly 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 85

Slide 85 text

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

Slide 86

Slide 86 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 FantasyPremierLeague

Slide 87

Slide 87 text

@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

Slide 88

Slide 88 text

@joreilly Realm Kotlin - storing data FantasyPremierLeague realm.write { ... val teams = query().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) } }

Slide 89

Slide 89 text

@joreilly Realm Kotlin - storing data FantasyPremierLeague realm.write { ... val teams = query().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) } }

Slide 90

Slide 90 text

@joreilly Realm Kotlin - reading data FantasyPremierLeague realm.query().asFlow() .map { it.list } .collect { it: RealmResults -> ... } }

Slide 91

Slide 91 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 92

Slide 92 text

@joreilly SKIE FantasyPremierLeague

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 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 96

Slide 96 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 97

Slide 97 text

@joreilly SKIE - Sealed Classes (Swift) FantasyPremierLeague

Slide 98

Slide 98 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 99

Slide 99 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 100

Slide 100 text

@joreilly ClimateTraceKMP

Slide 101

Slide 101 text

@joreilly ClimateTraceKMP

Slide 102

Slide 102 text

@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

Slide 103

Slide 103 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 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

@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

Slide 107

Slide 107 text

@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

Slide 108

Slide 108 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 109

Slide 109 text

@joreilly Material 3 Window Size Class ClimateTrace

Slide 110

Slide 110 text

@joreilly ClimateTrace

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 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 116

Slide 116 text

@joreilly Jetpack Navigation ClimateTrace

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

@joreilly Thank you!

Slide 119

Slide 119 text

@joreilly Questions