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

Kotlin Multiplatform Architecture

Kotlin Multiplatform Architecture

Discussing building shared architecture apps with Kotlin, mostly native mobile (Android & iOS). Talk about libraries currently available, why Jetbrains has a different threading model, where the ecosystem is at and where I think it's going.

58d1281770fe55a05a96600244ec8341?s=128

Kevin Galligan

October 26, 2018
Tweet

Transcript

  1. Kotlin Multiplatform Architecture Kevin Galligan

  2. None
  3. Touchlab

  4. Community community!

  5. None
  6. SHARED ARHICTECTURE

  7. Mobile & Web Architecture, not UI

  8. Shared UI == Failure!

  9. Shared Loigc == Computers

  10. None
  11. Web is more difficult No SQL :(

  12. Advocate for new standards I want SQL again

  13. https://hacks.mozilla.org/

  14. mobile is MUCH simpler

  15. Why Kotlin?

  16. None
  17. Modern Language

  18. None
  19. Community Excitement

  20. Best Tools

  21. Smooth Interop

  22. Optional Sharing Low risk. No Big Decisions.

  23. “Everything Should Be Made as Simple as Possible, But Not

    Simpler” - Albert Einstein Maybe?
  24. Status

  25. Q3 Q2 Q4 Q1 Q2 2018 2019 0 .6 v0.7

    v0.8 v0.8.2 v0.9.3 IDE tooling! Coroutines?
  26. Q3 Q2 Q4 Q1 Q2 2018 2019 0 .6 v0.7

    v0.8 v0.8.2 v0.9.3 IDE tooling! Coroutines? K/N 1.0, Kotlin 1.3 Gradle 4.10+ Android Studio
  27. Q3 Q2 Q4 Q1 Q2 2018 2019 0 .6 v0.7

    v0.8 v0.8.2 v0.9.3 IDE tooling! Coroutines? K/N 1.0, Kotlin 1.3 Gradle 4.10+ Android Studio MT Coroutines! Other samples/libraries Production deployments
  28. Q3 Q2 Q4 Q1 Q2 2018 2019 0 .6 v0.7

    v0.8 v0.8.2 v0.9.3 IDE tooling! Coroutines? K/N 1.0, Kotlin 1.3 Gradle 4.10+ Android Studio MT Coroutines! Other samples/libraries Production deployments Paid license/debugger Reactive Library Big Production Apps Webassembly stuff?
  29. SHARED CODE FOR ANDROID & IOS

  30. Common

  31. Common mainThread?

  32. expect val mainThread:Boolean

  33. expect val mainThread:Boolean actual val mainThread: Boolean get() = Looper.myLooper()

    === Looper.getMainLooper()
  34. expect val mainThread:Boolean actual val mainThread: Boolean get() = Looper.myLooper()

    === Looper.getMainLooper() actual val mainThread: Boolean get() = NSThread.isMainThread()
  35. expect val mainThread:Boolean actual val mainThread: Boolean get() = Looper.myLooper()

    === Looper.getMainLooper() actual val mainThread: Boolean get() = NSThread.isMainThread() actual val mainThread: Boolean = true
  36. expect fun currentTimeMillis():Long expect fun <B> backgroundTask(backJob:()-> B, mainJob:(B) ->

    Unit) expect fun backgroundTask(backJob:()->Unit) expect fun networkBackgroundTask(backJob:()->Unit) expect fun initContext():NativeOpenHelperFactory expect fun <T> goFreeze(a:T):T expect fun <T> T.freeze2(): T expect fun simpleGet(url:String):String expect fun logException(t:Throwable) expect fun settingsFactory(): Settings.Factory expect fun createUuid():String
  37. expect class Date { fun toLongMillis():Long } expect class DateFormatHelper(format:String){

    fun toDate(s:String):Date fun format(d:Date):String }
  38. actual class Date(val date:java.util.Date) { actual fun toLongMillis(): Long =

    date.time } actual class DateFormatHelper actual constructor(format: String) { val dateFormatter = object : ThreadLocal<DateFormat>(){ override fun initialValue(): DateFormat = SimpleDateFormat(format) } actual fun toDate(s: String): Date = Date(dateFormatter.get()!!.parse(s)) actual fun format(d: Date): String = dateFormatter.get()!!.format(d.date) }
  39. fun initPlatformClient( staticFileLoader: (filePrefix: String, fileType: String) -> String?, analyticsCallback:

    (name: String, params: Map<String, Any>) -> Unit, clLogCallback: (s: String) -> Unit) {
  40. fun initPlatformClient( staticFileLoader: (filePrefix: String, fileType: String) -> String?, analyticsCallback:

    (name: String, params: Map<String, Any>) -> Unit, clLogCallback: (s: String) -> Unit) { AppContext.initPlatformClient ({filePrefix, fileType -> loadAsset("${filePrefix}.${fileType}")}, {name: String, params: Map<String, Any> -> val event = CustomEvent(name) //Loop Answers.getInstance().logCustom(event) }, { Log.w("MainApp", it) })
  41. let appContext = AppContext() appContext.doInitPlatformClient(staticFileLoader: loadAsset, analyticsCallback: analyticsCallback, clLogCallback: csLog)

    func loadAsset(filePrefix:String, fileType:String) -> String?{ do{ let bundleFile = Bundle.main.path(forResource: filePrefix, ofType: fileType) return try String(contentsOfFile: bundleFile!) } catch { return nil } }
  42. Common

  43. JVM Native Common

  44. JVM Native Common Framework

  45. JVM Native Common Framework

  46. JVM Native Common Android Stuff Framework iOS Stuff

  47. JVM Native Common

  48. JVM Native Common Ktor-JVM Ktor-Native Ktor

  49. More Info and Tutorials https://github.com/touchlab/KotlinMultiplatformStuff

  50. DROIDCON WITH KOTLIN MULTIPLATFORM

  51. Funky Code Testbed

  52. Funky Code Testbed Kotlin in 2014!

  53. Droidcon NYC & SF

  54. SQLite Knarch.db Android iOS

  55. SQLite Knarch.db SQLDelight Android iOS

  56. SQLite Knarch.db SQLDelight Logic! Reactive (LiveData) Android iOS

  57. SQLite Knarch.db SQLDelight Logic! Reactive (LiveData) Android iOS

  58. SQLite Knarch.db SQLDelight MP Settings Logic! Reactive (LiveData) Android iOS

  59. val evenLiveData:EventLiveData init { val query = goFreeze(AppContext.dbHelper. queryWrapper.sessionQueries. sessionById(sessionId))

    evenLiveData = EventLiveData(query) } fun shutDown(){ evenLiveData.removeListener() }
  60. val evenLiveData:EventLiveData init { val query = goFreeze(AppContext.dbHelper. queryWrapper.sessionQueries. sessionById(sessionId))

    evenLiveData = EventLiveData(query) } fun shutDown(){ evenLiveData.removeListener() }
  61. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    eventViewModel.eventModel.evenLiveData. observe(viewLifecycleOwner, Observer { dataRefresh(it) }) return initView(inflater, container) }
  62. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    eventViewModel.eventModel.evenLiveData. observe(viewLifecycleOwner, Observer { dataRefresh(it) }) return initView(inflater, container) }
  63. fun registerForChanges(proc:(sessionInfo:SessionInfo)->Unit){ eventObserver = object : Observer<SessionInfo>{ override fun onChanged(t:

    SessionInfo?){ if(t != null) proc(t) } } eventModel.evenLiveData.observeForever(eventObserver!!) }
  64. viewModel = EventViewModel(sessionId: sessionId) viewModel.registerForChanges(proc: updateUi)

  65. viewModel = EventViewModel(sessionId: sessionId) viewModel.registerForChanges(proc: updateUi) func updateUi(sessionInfo:SessionInfo) -> KotlinUnit{

    self.sessionInfo = sessionInfo styleButton() updateAllUi() return KotlinUnit() }
  66. Droidcon App Kotlin Multiplatform https://www.youtube.com/watch?v=YAeDK3Ei0Lk https://github.com/touchlab/DroidconKotlin/ Now with 0.9.3!

  67. KOTLINCONF WITH KOTLIN MULTIPLATFORM (OBV)

  68. Settings Android iOS Ktor

  69. Settings Android iOS Ktor DataRepository

  70. Settings Android iOS Ktor DataRepository SessionDetailsPresenter

  71. Settings Android iOS Ktor DataRepository SessionDetailsPresenter SessionDetailsView

  72. interface SessionDetailsView : BaseView { fun updateView(isFavorite: Boolean, session: SessionModel)

    fun setupRatingButtons(rating: SessionRating?) fun setRatingClickable(clickable: Boolean) } override fun updateView(isFavorite: Boolean, session: SessionModel) { collapsingToolbar.title = session.title speakersTextView.text = session.speakers.joinToString(separator = ", ") { it.fullName } timeTextView.text = session.timeString detailsTextView.text = listOfNotNull(session.roomText, session.category).joinToString(", ") descriptionTextView.text = session.descriptionText val online = context?.let { it.isConnected?.and(!it.isAirplaneModeOn) } ?: false for (button in listOf(votingButtonsLayout, favoriteButton)) { func updateView(isFavorite: Bool, session: SessionModel) { titleLabel.text = session.title let startsAt = session.startsAt let endsAt = session.endsAt if (startsAt != nil && endsAt != nil) { timeLabel.text = KotlinPair(first: startsAt, second: endsAt).toReadableString() } let image = UIImage(named: isFavorite ? "star_full" : "star_empty")! favoriteButton.image = image
  73. Settings Android iOS Ktor DataRepository SessionDetailsPresenter SessionDetailsView

  74. KotlinConf App https://github.com/JetBrains/kotlinconf-app

  75. Which?

  76. STATE

  77. 3 Ecosystems JVM, JS, and Native

  78. Kotlin/Native State Rules

  79. Rule #1 Live state belongs to 1 thread

  80. Rule #2 Frozen state can be shared by threads

  81. No threading primitives No “synchronized”, “volatile”, etc

  82. Runtime Safety Kotlin/Native can verify safe mutability

  83. JVM/JS? See Kotlinconf keynote

  84. Short term pain Tradeoff for future

  85. How does Kotlin know?!

  86. FROZEN!

  87. FROZEN!

  88. Runtime Designation AKA a flag

  89. all the code you’ve ever written not frozen frozen

  90. all the code you’ve ever written not frozen frozen

  91. One-way operation No unfreeze()

  92. Freezes everything

  93. class TalkExamples{ var justCountingStuff:Int = 0 init { backgroundCall {

    //do something justCountingStuff++ }.freeze() } }
  94. class TalkExamples{ var justCountingStuff:Int = 0 init { backgroundCall {

    //do something justCountingStuff++ }.freeze() } }
  95. Usually OK Data objects should be immutable

  96. Global state more difficult Service object, large memory state

  97. Passing State

  98. DetachedObjectGraph(TransferMode.SAFE) { ListData("asdf") }

  99. val data = ListData("asdf") DetachedObjectGraph(TransferMode.SAFE) { data }

  100. private val stateBox: AtomicReference<DetachedObjectGraph<Any>> = AtomicReference( DetachedObjectGraph(mode = TransferMode.SAFE, producer

    = { mutableListOf<E>() as Any }) ) private val lock = NSLock() internal fun withLockDetached(proc: (MutableList<E>) -> MutableList<E>) { lock.lock() try { stateBox.value = DetachedObjectGraph(mode = TransferMode.SAFE, producer = { val dataList = stateBox.value.attach() as MutableList<E> proc(dataList) as Any }) } finally { lock.unlock() } }
  101. Atomics! Mutable immutable

  102. AtomicInt/AtomicLong

  103. AtomicReference Update with frozen objects

  104. val lambdas = AtomicReference<PlatformLambdas?>(null) fun initPlatformClient( staticFileLoader: (filePrefix: String, fileType:

    String) -> String?, analyticsCallback: (name: String, params: Map<String, Any>) -> Unit, clLogCallback: (s: String) -> Unit) { lambdas.value = PlatformLambdas( staticFileLoader, analyticsCallback, clLogCallback).freeze() }
  105. data class FrozenData( val someCount:AtomicInt, val someString:String, val someOtherState:AtomicReference<OtherState> )

    data class OtherState( val otherCount:Int, val otherString:String )
  106. val frozenData = FrozenData( AtomicInt(1), "asdf", AtomicReference( OtherState( 1, "qwert")

    ) ).freeze()
  107. val frozenData = FrozenData( AtomicInt(1), "asdf", AtomicReference( OtherState( 1, "qwert")

    ) ).freeze()
  108. fun update(frozenData: FrozenData){ frozenData.someCount.increment() val otherState = frozenData.someOtherState.value val updated

    = otherState.copy(otherCount = otherState.otherCount+1) frozenData.someOtherState.value = updated.freeze() }
  109. Stately! v0.3.1~ish

  110. Multiplatform Definitions • freeze() method and frozen info • Atomics

    (Int, Long, Reference) • K/N state-related annotations (@ThreadLocal/ @SharedImmutable)
  111. Multithreaded Collections Built on atomics

  112. val cowList = frozenCopyOnWriteList<SampleData>() val sharedList = frozenLinkedList<SampleData>() val coiSharedList

    = frozenLinkedList<SampleData>(stableIterator = true) val sharedMap = frozenHashMap<String, SampleData>() val lruRemovedCount = AtomicInt(0) val lruCache = frozenLruCache<String, SampleData>(5) { lruRemovedCount.increment() }
  113. Why? If you have to ask…

  114. None
  115. Changing Architectures Coroutines, libraries, best practices

  116. THREADING

  117. Workers like Executor

  118. Passing State similar to DetachedObjectGraph

  119. Until Coroutines MT coroutines will largely replace Worker

  120. Coroutines and State? we’ll see

  121. LIBRARIES

  122. Jetbrains • Ktor • Kotlinx.Coroutines • Kotlinx.io • Atomic-fu

  123. Community • Sqldelight • Knarch.db • Multiplatform Settings • Stately

    • OKIO2 (developing)
  124. GETTING STARTED

  125. Intellij EAP Sample project templates!

  126. None
  127. Build Samples Conference apps, several others

  128. Kotlin/Native Docs Learn threads and state

  129. Join the Kotlin Slack

  130. Kotlinconf Videos list on KotlinMultiplatformStuff

  131. For Libraries? Check out Stately

  132. check out yours!

  133. None
  134. kevin@touchlab.co @kpgalligan

  135. kevin@touchlab.co @kpgalligan Join the team !