$30 off During Our Annual Pro Sale. View Details »

All stacks Kotlin

All stacks Kotlin

Guillermo Orellana

June 07, 2019
Tweet

More Decks by Guillermo Orellana

Other Decks in Programming

Transcript

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

  2. None
  3. + =

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

  5. KOTLIN ALL STACKS

  6. Multiplatform Development

  7. None
  8. None
  9. None
  10. 1.2

  11. Kotlin Multiplatform

  12. "Traditional" Kotlin kotlinc

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

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

  15. KotlinJS kotlinc-js

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

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

    doValidations(newValue)
  18. Kotlin Native kotlinc-native

  19. val model = User().freeze()

  20. val model = User().freeze()

  21. Different toolchains • No JVM goodies • No annotation processor

    • No class file interaction
  22. Trying it out

  23. 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') {
  24. Common Module compilations.test.outputKinds("FRAMEWORK") } } jvm { } js {

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

    compileKotlinJs { kotlinOptions.metaInfo = true kotlinOptions.sourceMap = true kotlinOptions.moduleKind = "commonjs" kotlinOptions.main = "call" } }
  26. 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 {
  27. Common Module api Libs.kotlinTestCommon } } iosMain { dependencies {

    api Libs.kotlinxCoroutinesCoreNative api Libs.serializationRuntimeNative api Libs.ktorClientIos api Libs.ktorClientJsonNative api Libs.ktorClientLoggingNative } } jvmMain { dependencies {
  28. Multiplatform Module jvmMain androidMain iosMain jsMain commonMain jvmTest androidTest iosTest

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

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

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

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

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

    iosTest jsTest commonTest MyModule
  34. 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() }
  35. None
  36. Keynotedex

  37. None
  38. None
  39. Code sharing

  40. Where do you draw the line?

  41. •Good starting point •Shared DTOs = Repository Domain Logic Presenters

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

    UI Network
  43. •Works well if your UI is consistent across •Requires careful

    planning of API Repository Domain Logic Presenters View Binding UI Network
  44. •World of pain and fighting the frameworks •Your iOS teammates

    won't be happy •Just, why? Repository Domain Logic Presenters View Binding UI Network
  45. Repository Domain Logic Presenters View Binding UI View Binding UI

    React UI Network
  46. Android Client

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

  48. Job's done

  49. Common code

  50. Recommended setup

  51. apply plugin: "kotlin-multiplatform" apply plugin: "com.android.application" android { compileSdkVersion 28

    defaultConfig { applicationId "es.guillermoorellan minSdkVersion 21 targetSdkVersion 28 versionCode 1
  52. 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)
  53. Extra indirection

  54. common jvmMain commonMain android main

  55. commonMain expect class SessionStorage { fun put(value: String?) fun get():

    String? fun clear() }
  56. 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() }
  57. 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() }
  58. 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()
  59. iOS Client

  60. None
  61. None
  62. common.framework

  63. None
  64. None
  65. None
  66. cocoapods { summary = "Keynotedex common module" homepage = "https:

    //github.com/wiyarmir/keynotedex" }
  67. platform :ios, '12.1' target 'Keynotedex' do pod 'common', :path =>

    ' ../common' end
  68. None
  69. apply plugin: 'co.touchlab.kotlinxcodesync' xcode { projectPath = " ../ios/Keynotedex.xcodeproj" target

    = "Keynotedex" }
  70. None
  71. None
  72. None
  73. None
  74. Kotlin Native

  75. Coroutines

  76. 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 ->
  77. 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 ->
  78. kotlin.IllegalStateException: There is no event loop. Use runBlocking { ...

    } to start one. at 0 Keynotedex 0x000000010b170f05 kfun:kotlin.Exception.<init>(kotlin.String?)kotlin.Exception + 21 at 1 Keynotedex 0x000000010b170ec5 kfun:kotlin.RuntimeException.<init>(kotlin.String?)kotlin.RuntimeException + 21 at 2 Keynotedex 0x000000010b182925 kfun:kotlin.IllegalStateException.<init>(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- core@kotlin.coroutines.Continuation<#GENERIC>.(#GENERIC)Generic + 341 at 7 Keynotedex 0x000000010b266d91 kfun:kotlinx.coroutines.intrinsics.startCoroutineCancellable$kotlinx-coroutines- core@kotlin.coroutines.SuspendFunction1<#GENERIC,#GENERIC>. (#GENERIC;kotlin.coroutines.Continuation<#GENERIC>)Generic
  79. kotlin.IllegalStateException: There is no event loop. Use runBlocking { ...

    } to start one. at 0 Keynotedex 0x000000010b170f05 kfun:kotlin.Exception.<init>(kotlin.String?)kotli n.Exception + 21 at 1 Keynotedex 0x000000010b170ec5 kfun:kotlin.RuntimeException.<init>(kotlin.String ?)kotlin.RuntimeException + 21 at 2 Keynotedex 0x000000010b182925
  80. None
  81. 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 ->
  82. 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 }
  83. internal actual fun CustomMainScope(): CoroutineScope = MainScope()

  84. Serialization

  85. @Serializable data class SessionResponse( val session: Session )

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

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

  88. None
  89. What next?

  90. •Local storage with SQLDelight •Desktop apps

  91. https://github.com/wiyarmir/keynotedex

  92. https://keynotedex.wiyarmir.es/

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