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

Building Multiplatform Mobile Apps in Kotlin

Building Multiplatform Mobile Apps in Kotlin

So you’ve seen the blog posts and conference talks about Kotlin Multiplatform. Maybe you’ve put together a hello world. You might have found the experience confusing, with lots of new configuration to learn, and many out-of-date examples.

At Touchlab, we created a template project called KaMPKit to ease startup time and get to prototyping real features faster. It’s given us a platform to prototype ideas quickly, and has been a model for integrating shared Kotlin code into real production apps. And best of all, you can use it too!

We’ll talk about the structure of KaMPKit, and the architectural choices we made in developing it. You’ll learn about the libraries involved and how you can use them. And we’ll talk through some actual examples of projects Touchlab has built from this template. With any luck, our production experience can help inform yours!

Russell Wolf

May 14, 2020
Tweet

More Decks by Russell Wolf

Other Decks in Technology

Transcript

  1. Quick KMP Overview • Compile platform-agnostic common code to multiple

    targets • JVM, JS, Native • Use platform-specific code to interact with platform APIs • Well-suited for mobile code- sharing
  2. Kotlin Native Concurrency • Mutable state must be confined to

    a single thread • Immutable state can be shared to multiple threads • freeze()
  3. kotlinx.coroutines https://github.com/Kotlin/kotlinx.coroutines scope.launch { dbHelper.selectAllItems().asFlow() .map { q #-> val

    itemList = q.executeAsList() ItemDataSummary( itemList.maxBy { it.name.length }, itemList) } .flowOn(Dispatchers.Default) .collect { summary #-> viewUpdate(summary) } } • Async operations • Suspend functions and Flows • Lambda callbacks for Swift • Multithreaded preview release • CoroutineScope refactors coming soon
  4. Koin https://github.com/InsertKoinIO/koin fun initKoin( appDeclaration: KoinAppDeclaration = {} ) =

    startKoin { appDeclaration() modules(platformModule, coreModule) } private val coreModule = module { single { DatabaseHelper(get()) } single<KtorApi> { DogApiImpl() } } expect val platformModule: Module • Dependency Management • Preview release • Main-threaded • Common and platform code • checkModules()
  5. Ktor https://github.com/ktorio/ktor class DogApiImpl : KtorApi { private val client

    = HttpClient { install(JsonFeature) { serializer = KotlinxSerializer() } } override suspend fun getJsonFromApi() = network { client.get<BreedResult> { dogs("api/breeds/list/all") } } private fun HttpRequestBuilder.dogs(path: String) { url { takeFrom("https:#//dog.ceo/") encodedPath = path } } } • Coroutine-aware Http client • JSON via kotlinx.serialization • Not multithread-safe
  6. SqlDelight https://github.com/cashapp/sqldelight CREATE TABLE Breed ( id INTEGER NOT NULL

    PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, favorite INTEGER NOT NULL DEFAULT 0 ); interface Breed { val id: Long val name: String val favorite: Long data class Impl( override val id: Long, override val name: String, override val favorite: Long ) : Breed } • Database • Generates models and queries from Sqlite statements • Platform-specific runtimes
  7. Multiplatform Settings https://github.com/russhwolf/multiplatform-settings settings .putLong(DB_TIMESTAMP_KEY, currentTimeMS) • Made by me!

    • Key/value storage • Update listeners (not used in KaMPKit) • Platform-specific delegates
  8. Stately https://github.com/touchlab/Stately/ val ref = GuardedStableRef(ktorJob) val listenerDisposableHandle = coroutineContext[Job]#!!

    .invokeOnCompletion(onCancelling = true) { val parentJob = ref.state #// ##... } • Concurrency management • Common definitions of K/N freeze APIs • Other threading primitives • Isostate (not in KaMPKit currently)
  9. Kermit https://github.com/touchlab/Kermit private val log: Kermit by inject { parametersOf("MainActivity")

    } log.v { "List submitted to adapter" } • New logging library from Touchlab • Immutable after configuration • Not yet merged into KaMPKit
  10. Gradle Stuff android() val onPhone = System.getenv("SDK_NAME") #?.startsWith("iphoneos") #?: false

    if (onPhone) { iosArm64("ios") } else { iosX64("ios") } sourceSets["commonMain"].dependencies {#/*##...#*/} sourceSets["commonTest"].dependencies {#/*##...#*/} sourceSets["androidMain"].dependencies {#/*##...#*/} sourceSets["androidTest"].dependencies {#/*##...#*/} sourceSets["iosMain"].dependencies {#/*##...#*/} sourceSets["iosTest"].dependencies {#/*##...#*/} • Targets and dependencies • Kotlin Scripts • Opt-in for Obj-C generics • Opt-in for ExperimentalCoroutinesApi
  11. Cocopods Gradle Plugin https://github.com/touchlab/KotlinCocoapods cocoapodsext { summary = "Common library

    for the KaMP starter kit" homepage = "https:#//github.com/touchlab/ KaMPStarter" framework { isStatic = false } } • Fork of Jetbrains plugin • Cocoapods for easier Xcode/iOS config • Expanded configurability • Podfile adjustments • Active development
  12. Multimodule SqlDelight • Added duplicate shared module • Extracted Sqlite

    definitions • Quick answer to "is this possible?"
  13. RxSwift/Coroutines interop func wrapObservable<T>(flowWrapper: FlowWrapper<T>) #-> Observable<T> { return Observable<T>.create

    { observer in let job: Kotlinx_coroutines_coreJob = flowWrapper.subscribe( scope: scope, onEach: { item in observer.on(.next(item)) }, onComplete: { observer.on(.completed) }, onThrow: { error in observer.on(.error(KotlinError(error))) } ) return Disposables.create { job.cancel(cause: nil) } } } class FlowWrapper<out T : Any>( private val flow: Flow<T> ) { fun subscribe( scope: CoroutineScope, onEach: (item: T) #-> Unit, onComplete: () #-> Unit, onThrow: (error: Throwable) #-> Unit ) = flow .onEach { onEach(it) } .catch { onThrow(it) } .onCompletion { onComplete() } .launchIn(scope) }
  14. Feature Module • Integrated KMP module into client codebase •

    Started from KaMPKit • Adjustments for client's build strategy • Updated KaMPKit with learnings