Slide 1

Slide 1 text

All-Stacks Kotlin Guillermo Orellana @wiyarmir June 2019 Kotliners Budapest

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

+ =

Slide 4

Slide 4 text

https://www.thinkgeek.com/product/e554/

Slide 5

Slide 5 text

KOTLIN ALL STACKS

Slide 6

Slide 6 text

Multiplatform Development

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

1.2

Slide 11

Slide 11 text

Kotlin Multiplatform

Slide 12

Slide 12 text

"Traditional" Kotlin kotlinc

Slide 13

Slide 13 text

startActivity( Intent( this, ProfileActivity ::class.java ) )

Slide 14

Slide 14 text

startActivity( Intent( this, ProfileActivity ::class.java ) )

Slide 15

Slide 15 text

KotlinJS kotlinc-js

Slide 16

Slide 16 text

val element: dynamic = findDOMNode(refs) val newValue: String = element.value doValidations(newValue)

Slide 17

Slide 17 text

val element: dynamic = findDOMNode(refs) val newValue: String = element.value doValidations(newValue)

Slide 18

Slide 18 text

Kotlin Native kotlinc-native

Slide 19

Slide 19 text

val model = User().freeze()

Slide 20

Slide 20 text

val model = User().freeze()

Slide 21

Slide 21 text

Different toolchains • No JVM goodies • No annotation processor • No class file interaction

Slide 22

Slide 22 text

Trying it out

Slide 23

Slide 23 text

Common Module apply plugin: 'kotlin-multiplatform' apply plugin: 'kotlinx-serialization' apply plugin: 'org.jetbrains.kotlin.native.cocoapods' apply plugin: 'co.touchlab.kotlinxcodesync' kotlin { targets { def iOSTarget = System.getenv('SDK_NAME') ?.startsWith("iphoneos") ? presets.iosArm64 : presets.iosX64 fromPreset(iOSTarget, 'ios') {

Slide 24

Slide 24 text

Common Module compilations.test.outputKinds("FRAMEWORK") } } jvm { } js { compileKotlinJs { kotlinOptions.metaInfo = true kotlinOptions.sourceMap = true kotlinOptions.moduleKind = "commonjs" kotlinOptions.main = "call" } }

Slide 25

Slide 25 text

Common Module compilations.test.outputKinds("FRAMEWORK") } } jvm { } js { compileKotlinJs { kotlinOptions.metaInfo = true kotlinOptions.sourceMap = true kotlinOptions.moduleKind = "commonjs" kotlinOptions.main = "call" } }

Slide 26

Slide 26 text

Common Module sourceSets { commonMain { dependencies { api Libs.kotlinStdlibCommon api Libs.kotlinxCoroutinesCoreCommon api Libs.serializationRuntimeCommon api Libs.ktorClientCore api Libs.ktorClientAuth api Libs.ktorClientJson api Libs.ktorClientLogging } } commonTest { dependencies {

Slide 27

Slide 27 text

Common Module api Libs.kotlinTestCommon } } iosMain { dependencies { api Libs.kotlinxCoroutinesCoreNative api Libs.serializationRuntimeNative api Libs.ktorClientIos api Libs.ktorClientJsonNative api Libs.ktorClientLoggingNative } } jvmMain { dependencies {

Slide 28

Slide 28 text

Multiplatform Module jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest jsTest commonTest MyModule

Slide 29

Slide 29 text

Compile JVM jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest jsTest commonTest MyModule

Slide 30

Slide 30 text

Compile Android jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest jsTest commonTest MyModule

Slide 31

Slide 31 text

Compile iOS jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest jsTest commonTest MyModule

Slide 32

Slide 32 text

Compile JS jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest jsTest commonTest MyModule

Slide 33

Slide 33 text

Run JVM tests jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest jsTest commonTest MyModule

Slide 34

Slide 34 text

Platform specific code actual class Storage actual constructor(val foo: String) { actual fun save() { println("Save") } actual fun load() { println("Load") } } Native Common expect class Storage { fun save() fun load() }

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

Keynotedex

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

Code sharing

Slide 40

Slide 40 text

Where do you draw the line?

Slide 41

Slide 41 text

•Good starting point •Shared DTOs = Repository Domain Logic Presenters View Binding UI Network

Slide 42

Slide 42 text

•Real power of sharing Repository Domain Logic Presenters View Binding UI Network

Slide 43

Slide 43 text

•Works well if your UI is consistent across •Requires careful planning of API Repository Domain Logic Presenters View Binding UI Network

Slide 44

Slide 44 text

•World of pain and fighting the frameworks •Your iOS teammates won't be happy •Just, why? Repository Domain Logic Presenters View Binding UI Network

Slide 45

Slide 45 text

Repository Domain Logic Presenters View Binding UI View Binding UI React UI Network

Slide 46

Slide 46 text

Android Client

Slide 47

Slide 47 text

dependencies { implementation project(':common') } Android Client

Slide 48

Slide 48 text

Job's done

Slide 49

Slide 49 text

Common code

Slide 50

Slide 50 text

Recommended setup

Slide 51

Slide 51 text

apply plugin: "kotlin-multiplatform" apply plugin: "com.android.application" android { compileSdkVersion 28 defaultConfig { applicationId "es.guillermoorellan minSdkVersion 21 targetSdkVersion 28 versionCode 1

Slide 52

Slide 52 text

org.gradle.api.ProjectConfigurationException: A problem occurred configuring project ':common' ...(150 lines of trace) Caused by: java.lang.RuntimeException: SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable. ...(150 lines of trace)

Slide 53

Slide 53 text

Extra indirection

Slide 54

Slide 54 text

common jvmMain commonMain android main

Slide 55

Slide 55 text

commonMain expect class SessionStorage { fun put(value: String?) fun get(): String? fun clear() }

Slide 56

Slide 56 text

jvmMain actual class SessionStorage { var proxy: SessionStorageProxy = object : SessionStorageProxy { override fun put(value: String?): Unit = TODO() override fun get(): String? = TODO() override fun clear(): Unit = TODO() } actual fun put(value: String?): Unit = proxy.put(value) actual fun get(): String? = proxy.get() actual fun clear(): Unit = proxy.clear() } interface SessionStorageProxy { fun put(value: String?) fun get(): String? fun clear() }

Slide 57

Slide 57 text

jvmMain override fun clear(): Unit = TODO() } actual fun put(value: String?): Unit = proxy.put(value) actual fun get(): String? = proxy.get() actual fun clear(): Unit = proxy.clear() } interface SessionStorageProxy { fun put(value: String?) fun get(): String? fun clear() }

Slide 58

Slide 58 text

main class AndroidSessionStorage( applicationContext: Context, private val prefs: SharedPreferences = applicationContext.getSharedPreferences( "prefs", Context.MODE_PRIVATE ) ) : SessionStorageProxy { @SuppressLint("ApplySharedPref") override fun put(value: String?) { prefs.edit() .putString(KEY_SESSION, value) .commit()

Slide 59

Slide 59 text

iOS Client

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

common.framework

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

cocoapods { summary = "Keynotedex common module" homepage = "https: //github.com/wiyarmir/keynotedex" }

Slide 67

Slide 67 text

platform :ios, '12.1' target 'Keynotedex' do pod 'common', :path => ' ../common' end

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

apply plugin: 'co.touchlab.kotlinxcodesync' xcode { projectPath = " ../ios/Keynotedex.xcodeproj" target = "Keynotedex" }

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

Kotlin Native

Slide 75

Slide 75 text

Coroutines

Slide 76

Slide 76 text

class ProfilePresenter( private val view: ProfileView, private val getUserProfile: GetUserProfile, private val mainScope: CoroutineScope = MainScope() ) : CoroutineScope by mainScope { init { loadSessions() } private lateinit var _state: ProfileViewState private var state: ProfileViewState get() = _state set(value) = when (value) { is ProfileViewState.Loading -> view.showLoading() is ProfileViewState.Content ->

Slide 77

Slide 77 text

class ProfilePresenter( private val view: ProfileView, private val getUserProfile: GetUserProfile, private val mainScope: CoroutineScope = MainScope() ) : CoroutineScope by mainScope { init { loadSessions() } private lateinit var _state: ProfileViewState private var state: ProfileViewState get() = _state set(value) = when (value) { is ProfileViewState.Loading -> view.showLoading() is ProfileViewState.Content ->

Slide 78

Slide 78 text

kotlin.IllegalStateException: There is no event loop. Use runBlocking { ... } to start one. at 0 Keynotedex 0x000000010b170f05 kfun:kotlin.Exception.(kotlin.String?)kotlin.Exception + 21 at 1 Keynotedex 0x000000010b170ec5 kfun:kotlin.RuntimeException.(kotlin.String?)kotlin.RuntimeException + 21 at 2 Keynotedex 0x000000010b182925 kfun:kotlin.IllegalStateException.(kotlin.String?)kotlin.IllegalStateException + 21 at 3 Keynotedex 0x000000010b19ff56 kfun:kotlinx.coroutines.takeEventLoop#internal + 294 at 4 Keynotedex 0x000000010b19fdd6 kfun:kotlinx.coroutines.DefaultExecutor.dispatch(kotlin.coroutines.CoroutineContext;kotlinx. coroutines.Runnable) + 86 at 5 Keynotedex 0x000000010b2bd004 kfun:kotlinx.coroutines.NativeMainDispatcher.dispatch#internal + 116 at 6 Keynotedex 0x000000010b19b705 kfun:kotlinx.coroutines.resumeCancellable$kotlinx-coroutines- [email protected]<#GENERIC>.(#GENERIC)Generic + 341 at 7 Keynotedex 0x000000010b266d91 kfun:kotlinx.coroutines.intrinsics.startCoroutineCancellable$kotlinx-coroutines- [email protected]<#GENERIC,#GENERIC>. (#GENERIC;kotlin.coroutines.Continuation<#GENERIC>)Generic

Slide 79

Slide 79 text

kotlin.IllegalStateException: There is no event loop. Use runBlocking { ... } to start one. at 0 Keynotedex 0x000000010b170f05 kfun:kotlin.Exception.(kotlin.String?)kotli n.Exception + 21 at 1 Keynotedex 0x000000010b170ec5 kfun:kotlin.RuntimeException.(kotlin.String ?)kotlin.RuntimeException + 21 at 2 Keynotedex 0x000000010b182925

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

class ProfilePresenter( private val view: ProfileView, private val getUserProfile: GetUserProfile, private val mainScope: CoroutineScope = CustomMainScope() ) : CoroutineScope by mainScope { init { loadSessions() } private lateinit var _state: ProfileViewState private var state: ProfileViewState get() = _state set(value) = when (value) { is ProfileViewState.Loading -> view.showLoading() is ProfileViewState.Content ->

Slide 82

Slide 82 text

internal actual fun CustomMainScope(): CoroutineScope = CustomMainScopeImpl() internal class CustomMainScopeImpl : CoroutineScope { private val dispatcher = MainDispatcher() private val job = Job() private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> println("${throwable.message}: ${throwable.cause}") } override val coroutineContext: CoroutineContext get() = dispatcher + job + exceptionHandler }

Slide 83

Slide 83 text

internal actual fun CustomMainScope(): CoroutineScope = MainScope()

Slide 84

Slide 84 text

Serialization

Slide 85

Slide 85 text

@Serializable data class SessionResponse( val session: Session )

Slide 86

Slide 86 text

call.respond( JSON.stringify(SessionResponse()) ) Serialise

Slide 87

Slide 87 text

val sessionResponse: SessionResponse = JSON.parse(responseText) Deserialise

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

What next?

Slide 90

Slide 90 text

•Local storage with SQLDelight •Desktop apps

Slide 91

Slide 91 text

https://github.com/wiyarmir/keynotedex

Slide 92

Slide 92 text

https://keynotedex.wiyarmir.es/

Slide 93

Slide 93 text

Slides: https://speakerdeck.com/wiyarmir/all-stacks-kotlin Code: github.com/wiyarmir/keynotedex Contact: twitter.com/@wiyarmir