The Triumphs and Tribulations of Kotlin/Native In Practice - UIKonf, Berlin, Germany - May 2019

The Triumphs and Tribulations of Kotlin/Native In Practice - UIKonf, Berlin, Germany - May 2019

Newest iteration of my Kotlin/Native talk, now with updated Gradle recommendations!

Abstract:
The promise of write-once-run-everywhere has haunted native mobile developers since the first time someone whispered the words “phone gap”. But what if there was a way to have cross-platform development with a modern, type-safe language? JetBrains is trying to make this happen with Kotlin/Native, which compiles to LLVM bytecode, and can run on iOS, Android, and even the web. Learn more about some of the benefits of working with Kotlin/Native, some of the drawbacks, and a few of the (current) potential dealbreakers when it comes to using this exciting new technology in a real app.

Links:
- Main Project: https://github.com/bakkenbaeck/PorchPirateProtector
- Initial K/N Test: https://github.com/designatednerd/KotlinNativeTest

C4861b1dfdf3bbb21faec4a1acdf183d?s=128

Ellen Shapiro

May 28, 2019
Tweet

Transcript

  1. THE TRIUMPHS AND TRIBULATIONS OF KOTLIN/NATIVE IN PRACTICE UIKONF |

    BERLIN, GERMANY | MAY 2019 BY ELLEN SHAPIRO | @DESIGNATEDNERD | BAKKENBAECK.NO
  2. THE TRIUMPHS AND TRIBULATIONS OF KOTLIN/NATIVE IN PRACTICE UIKONF |

    BERLIN, GERMANY | MAY 2019 BY ELLEN SHAPIRO | @DESIGNATEDNERD | BAKKENBAECK.NO
  3. None
  4. None
  5. None
  6. !

  7. !

  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. THE VIRTUAL MACHINE

  17. None
  18. None
  19. None
  20. None
  21. None
  22. None
  23. SHAMELESS PLUG! HTTPS://STORE.RAYWENDERLICH.COM PRODUCTS/KOTLIN-APPRENTICE

  24. None
  25. None
  26. None
  27. None
  28. None
  29. !

  30. PACKAGE THIEF VS. GLITTER BOMB TRAP HTTPS://WWW.YOUTUBE.COM/WATCH?V=XOXHDK-HWUO

  31. None
  32. None
  33. None
  34. None
  35. None
  36. None
  37. None
  38. None
  39. None
  40. None
  41. None
  42. None
  43. None
  44. None
  45. None
  46. None
  47. PACKAGE THIEF VS. GLITTER BOMB TRAP HTTPS://WWW.YOUTUBE.COM/WATCH?V=XOXHDK-HWUO

  48. None
  49. None
  50. None
  51. None
  52. None
  53. ! "

  54. None
  55. None
  56. WITH WHAT SYSTEMS SHALL I BUILD IT?

  57. None
  58. Raspberry pi

  59. Raspberry pi Server

  60. Raspberry pi Server iOS app

  61. Raspberry pi Server iOS app Android App

  62. Works with Kotlin? Raspberry pi Server iOS app Android App

  63. Works With Kotlin? Raspberry pi ✅ Server ✅ iOS app

    ✅ Android App ✅
  64. HTTP://BAKKENBAECK.NO

  65. HTTPS://GITHUB.COM/BAKKENBAECK/ PORCH PIRATE PROTECTOR

  66. None
  67. NATIVE VS. MULTI-PLATFORM

  68. apply plugin: 'kotlin-multiplatform'

  69. apply plugin: 'kotlin-native-platform' apply plugin: 'kotlin-multiplatform'

  70. // For Native ONLY projects apply plugin: 'kotlin-native-platform' apply plugin:

    'kotlin-multiplatform'
  71. // For Native ONLY projects apply plugin: 'kotlin-native-platform' // For

    projects targeting multiple platforms, // for example JVM + Native + JS apply plugin: 'kotlin-multiplatform'
  72. None
  73. None
  74. None
  75. None
  76. None
  77. PROTIP: USE GRADLE 5.3.1* enableFeaturePreview('GRADLE_METADATA')

  78. PROTIP: USE GRADLE 5.3.1* enableFeaturePreview('GRADLE_METADATA') *as of May 28, 2019

  79. None
  80. None
  81. By Jorje Lascar, https://www.flickr.com/photos/jlascar/4503951595

  82. None
  83. None
  84. None
  85. FUNCTIONS WITH PER-PLATFORM IMPLEMENTATIONS expect fun

  86. commonMain

  87. commonMain

  88. iosMain

  89. androidMain

  90. commonMain

  91. CALLING CODE FROM BOTH APPS!

  92. ANDROID: EASY MODE build.gradle SomeFragment.kt

  93. IOS: MODE

  94. None
  95. None
  96. None
  97. None
  98. None
  99. ! OBJECTIVE-C

  100. None
  101. None
  102. IOS/OBJC GOTCHAS

  103. SWIFT Array != KOTLIN Array

  104. SWIFT Array<T> == KOTLIN List<T>

  105. SWIFT enum != KOTLIN enum class IN OBJC

  106. KOTLIN enum class DayOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday,

    Saturday, Sunday; }
  107. KOTLIN fun printForDay(day: DayOfWeek) { when (day) { DayOfWeek.Monday ->

    println("You can fall apart") DayOfWeek.Tuesday, DayOfWeek.Wednesday -> println("Break my heart") DayOfWeek.Thursday -> println("Doesn't even start") DayOfWeek.Friday -> println("I'm in love") DayOfWeek.Saturday -> println("Wait") DayOfWeek.Sunday -> println("Always comes too late") } }
  108. SWIFT (THEORETICALLY) func printForDay(day: DayOfWeek) { switch day { case

    .monday: print("You can fall apart") case .tuesday, .wednesday: print("Break my heart") case .thursday: print("Doesn't even start") case .friday: print("I'm in love") case .saturday: print("Wait") case .sunday: print("Always comes too late") } }
  109. None
  110. SWIFT (ACTUALLY) func printForDay(_ day: DayOfWeek) { switch day {

    case .monday: print("You can fall apart") case .tuesday, .wednesday: print("Break my heart") case .thursday: print("Doesn't even start") case .friday: print("I'm in love") case .saturday: print("Wait") case .sunday: print("Always comes too late") default: fatalError("Not a day") }
  111. SWIFT (ACTUALLY) func printForDay(_ day: DayOfWeek) { switch day {

    case .monday: print("You can fall apart") case .tuesday, .wednesday: print("Break my heart") case .thursday: print("Doesn't even start") case .friday: print("I'm in love") case .saturday: print("Wait") case .sunday: print("Always comes too late") default: fatalError("Not a day") }
  112. SWIFT (ACTUALLY) func printForDay(_ day: DayOfWeek) { switch day {

    case .monday: print("You can fall apart") case .tuesday, .wednesday: print("Break my heart") case .thursday: print("Doesn't even start") case .friday: print("I'm in love") case .saturday: print("Wait") case .sunday: print("Always comes too late") default: // <- ! ! ! fatalError("Not a day") }
  113. DECIDING WHAT TO CENTRALIZE

  114. WHAT DO I REALLY HATE BUILDING TWICE?

  115. TALKING TO THE SERVER

  116. JSON FROM NETWORK IN MODELS ⬅ JSON OUT TO NETWORK

  117. None
  118. None
  119. None
  120. None
  121. None
  122. IosClientEngine.kt https://github.com/ktorio/ktor/blob/master/ ktor-client/ktor-client-ios/darwin/src/io/ ktor/client/engine/ios/IosClientEngine.kt

  123. HttpClientJvmEngine.kt https://github.com/ktorio/ktor/blob/master/ ktor-client/ktor-client-core/jvm/src/io/ ktor/client/engine/HttpClientJvmEngine.kt

  124. androidMain import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers internal actual val ApplicationDispatcher: CoroutineDispatcher

    = Dispatchers.Main
  125. iosMain import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue import

    kotlin.coroutines.Runnable internal actual val ApplicationDispatcher: CoroutineDispatcher = NsQueueDispatcher() internal class NsQueueDispatcher: CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { val queue = dispatch_get_main_queue() dispatch_async(queue) { block.run() } } }
  126. iosMain import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue import

    kotlin.coroutines.Runnable internal actual val ApplicationDispatcher: CoroutineDispatcher = NsQueueDispatcher() internal class NSQueueDispatcher: CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { val queue = dispatch_get_main_queue() dispatch_async(queue) { block.run() } } }
  127. None
  128. ! FREEZING

  129. None
  130. MUTATING VARIABLES RETURNING A NEW OBJECT

  131. AVOID object FROM IOS

  132. USE INTERFACES TO MUTATE THINGS IN IOS INSTEAD OF K/N

  133. commonMain interface SecureStorage { fun storeTokenString(token: String) fun clearTokenString() fun

    fetchTokenString(): String? }
  134. iOS App @objc class Keychain: NSObject, SecureStorage { static let

    shared = Keychain() private override init() { super.init() } //TODO: Actually use keychain private var token: String? func storeTokenString(token: String) { self.token = token } func clearTokenString() { self.token = nil } func fetchTokenString() -> String? { return self.token } }
  135. iOS App @objc class Keychain: NSObject, SecureStorage { static let

    shared = Keychain() private override init() { super.init() } //TODO: Actually use keychain private var token: String? func storeTokenString(token: String) { self.token = token } func clearTokenString() { self.token = nil } func fetchTokenString() -> String? { return self.token } }
  136. iOS App @objc class Keychain: NSObject, SecureStorage { static let

    shared = Keychain() private override init() { super.init() } //TODO: Actually use keychain private var token: String? func storeTokenString(token: String) { self.token = token } func clearTokenString() { self.token = nil } func fetchTokenString() -> String? { return self.token } }
  137. iOS App @objc class Keychain: NSObject, SecureStorage { static let

    shared = Keychain() private override init() { super.init() } //TODO: Actually use keychain private var token: String? func storeTokenString(token: String) { self.token = token } func clearTokenString() { self.token = nil } func fetchTokenString() -> String? { return self.token } }
  138. commonMain object TokenManager { fun currentToken(storage: SecureStorage): UserToken? { storage.fetchTokenString()?.let

    { return UserToken(it) } ?: return null } fun storeToken(token: UserToken, storage: SecureStorage) { storage.storeTokenString(token.token) } fun clearToken(storage: SecureStorage) { storage.clearTokenString() } }
  139. commonMain object TokenManager { fun currentToken(storage: SecureStorage): UserToken? { storage.fetchTokenString()?.let

    { return UserToken(it) } ?: return null } fun storeToken(token: UserToken, storage: SecureStorage) { storage.storeTokenString(token.token) } fun clearToken(storage: SecureStorage) { storage.clearTokenString() } }
  140. commonMain object TokenManager { fun currentToken(storage: SecureStorage): UserToken? { storage.fetchTokenString()?.let

    { return UserToken(it) } ?: return null } fun storeToken(token: UserToken, storage: SecureStorage) { storage.storeTokenString(token.token) } fun clearToken(storage: SecureStorage) { storage.clearTokenString() } }
  141. iOS App @objc class Keychain: NSObject, SecureStorage { static let

    shared = Keychain() private override init() { super.init() } //TODO: Actually use keychain private var token: String? func storeTokenString(token: String) { self.token = token } func clearTokenString() { self.token = nil } func fetchTokenString() -> String? { return self.token } }
  142. None
  143. KotlinX Serialization

  144. commonMain data class UserCredentials( val username: String, val password: String

    )
  145. commonMain apply plugin: 'kotlinx-serialization' @Serializable data class UserCredentials( val username:

    String, val password: String )
  146. commonMain val creds = UserCredentials("lol@ha.no", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"lol@ha.no","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "lol@ha.no" // updatedCreds.password is "guessme"
  147. commonMain val creds = UserCredentials("lol@ha.no", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"lol@ha.no","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "lol@ha.no" // updatedCreds.password is "guessme"
  148. commonMain val creds = UserCredentials("lol@ha.no", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"lol@ha.no","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "lol@ha.no" // updatedCreds.password is "guessme"
  149. commonMain val creds = UserCredentials("lol@ha.no", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"lol@ha.no","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "lol@ha.no" // updatedCreds.password is "guessme"
  150. commonMain val creds = UserCredentials("lol@ha.no", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"lol@ha.no","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "lol@ha.no" // updatedCreds.password is "guessme"
  151. FROM CLIENT TO SERVER AND BACK!

  152. LOGIC

  153. MVC!

  154. MVVM!

  155. MVA!*

  156. MVA!* * - ANYTHING OTHER THAN A MODEL OR A

    VIEW FOR THE LOVE OF GOD
  157. MVP!

  158. HTTPS://GITHUB.COM/JETBRAINS/ KOTLINCONF-APP

  159. MODEL: VIEW: PRESENTER:

  160. MODEL: data class VIEW: PRESENTER:

  161. MODEL: data class VIEW: interface PRESENTER:

  162. MODEL: data class VIEW: interface PRESENTER: class

  163. VIEW: interface

  164. IOS VIEW: ANDROID VIEW: TESTING VIEW:

  165. IOS VIEW: UIViewController ANDROID VIEW: TESTING VIEW:

  166. IOS VIEW: UIViewController ANDROID VIEW: Fragment* TESTING VIEW:

  167. IOS VIEW: UIViewController ANDROID VIEW: Fragment* TESTING VIEW: *May not

    apply if your name is jake wharton
  168. IOS VIEW: UIViewController ANDROID VIEW: Fragment* TESTING VIEW: ¯\_(ϑ)_/¯ *May

    not apply if your name is jake wharton
  169. commonMain interface LoginView: IndefiniteLoadingIndicating { /// The text the user

    has input as their email address. var email: String? /// The text the user has input as their password. var password: String? fun emailErrorUpdated(toString: String?) fun passwordErrorUpdated(toString: String?) fun apiErrorUpdated(toString: String?) fun setSubmitButtonEnabled(enabled: Boolean) /// Called when login completes successfully. fun loginSucceeded() }
  170. commonMain class LoginPresenter( val view: LoginView, storage: SecureStorage ): BaseCoroutinePresenter(storage)

    { // rest of presenter class }
  171. suspend fun loginAsync(): Boolean { if (!isCurrentInputValid()) { return false

    } // If input is valid, these will not be null. val creds = UserCredentials(view.email!!, view.password!!) view.startLoadingIndicator() view.setSubmitButtonEnabled(false) apiError = null var success = false try { val token = api.login(creds) secureStorage.storeTokenString(token.token) view.loginSucceeded() success = true } catch (exception: Exception) { apiError = exception.message } view.stopLoadingIndicator() view.setSubmitButtonEnabled(true) return success } fun login() { launch { loginAsync() } }
  172. suspend fun loginAsync(): Boolean { if (!isCurrentInputValid()) { return false

    } // If input is valid, these will not be null. val creds = UserCredentials(view.email!!, view.password!!) view.startLoadingIndicator() view.setSubmitButtonEnabled(false) apiError = null var success = false try { val token = api.login(creds) secureStorage.storeTokenString(token.token) view.loginSucceeded() success = true } catch (exception: Exception) { apiError = exception.message } view.stopLoadingIndicator() view.setSubmitButtonEnabled(true) return success } fun login() { launch { loginAsync() } }
  173. suspend fun loginAsync(): Boolean { if (!isCurrentInputValid()) { return false

    } // If input is valid, these will not be null. val creds = UserCredentials(view.email!!, view.password!!) view.startLoadingIndicator() view.setSubmitButtonEnabled(false) apiError = null var success = false try { val token = api.login(creds) secureStorage.storeTokenString(token.token) view.loginSucceeded() success = true } catch (exception: Exception) { apiError = exception.message } view.stopLoadingIndicator() view.setSubmitButtonEnabled(true) return success } fun login() { launch { loginAsync() } }
  174. Android App

  175. Android App

  176. iOS App class LoginViewController: UIViewController { @IBOutlet private var emailTextInput:

    TextInputContainer! @IBOutlet private var passwordTextInput: TextInputContainer! @IBOutlet private var loginButton: UIButton! @IBOutlet private var apiErrorLabel: UILabel! @IBOutlet private var activityIndicator: UIActivityIndicatorView! private lazy var presenter = LoginPresenter(view: self, storage: Keychain.shared) @IBAction private func login() { self.presenter.login() } }
  177. extension LoginViewController: LoginView { func setSubmitButtonEnabled(enabled: Bool) { self.loginButton.isEnabled =

    enabled } func emailErrorUpdated(toString: String?) { self.emailTextInput.errorText = toString } func passwordErrorUpdated(toString: String?) { self.passwordTextInput.errorText = toString } func apiErrorUpdated(toString: String?) { self.apiErrorLabel.text = toString self.apiErrorLabel.isHidden = (toString == nil) } func loginSucceeded() { self.perform(segue: LoginSegue.loginSucceeded) } var email: String? { get { return self.emailTextInput.text } set(email) { self.emailTextInput.text = email } } var password: String? { get { return self.passwordTextInput.text } set(password) { self.passwordTextInput.text = password } } }
  178. TESTING YOUR COMMON CODE

  179. commonTest WORKS IF COROUTINES AREN'T INVOLVED!

  180. class TestLoginView: LoginView { override var email: String? = null

    override var password: String? = null var submitEnabled = true var submitWasDisabled = false override fun setSubmitButtonEnabled(enabled: Boolean) { submitEnabled = enabled if (!submitEnabled) { submitWasDisabled = true } } var emailError: String? = null override fun emailErrorUpdated(toString: String?) { emailError = toString } var passwordError: String? = null override fun passwordErrorUpdated(toString: String?) { passwordError = toString } var apiError: String? = null override fun apiErrorUpdated(toString: String?) { apiError = toString } var loginHasSucceeded = false override fun loginSucceeded() { loginHasSucceeded = true } }
  181. @Test fun validationSetsProperErrorsThenClearsThemAfterChangesMade() { val view = TestLoginView() val presenter

    = LoginPresenter(view, MockStorage()) view.password = "aaaaaa" presenter.validatePassword() assertFalse(presenter.isCurrentInputValid()) assertEquals(expectedEmailError.reason, view.emailError) assertNull(view.passwordError) view.email = "not@real.biz" presenter.validateEmail() assertTrue(presenter.isCurrentInputValid()) assertNull(view.emailError) assertNull(view.passwordError) }
  182. @Test fun attemptingToLoginWithoutChangesTriggersErrorsAndFails() = runBlocking { val view = TestLoginView()

    val presenter = LoginPresenter(view, MockStorage()) val expectedEmailError = ValidationResult.Invalid.WasNull("email") val expectedPasswordError = ValidationResult.Invalid.WasNull("password") val result = presenter.loginAsync() assertFalse(result) assertFalse(presenter.isCurrentInputValid()) // Actual attempt to login should never have been made since // the input was invalid assertFalse(view.submitWasDisabled) assertTrue(view.submitEnabled) assertNull(view.apiError) assertFalse(view.loginHasSucceeded) assertEquals(expectedEmailError.reason, view.emailError) assertEquals(expectedPasswordError.reason, view.passwordError) }
  183. @Test fun attemptingToLoginWithoutChangesTriggersErrorsAndFails() = runBlocking { val view = TestLoginView()

    val presenter = LoginPresenter(view, MockStorage()) val expectedEmailError = ValidationResult.Invalid.WasNull("email") val expectedPasswordError = ValidationResult.Invalid.WasNull("password") val result = presenter.loginAsync() assertFalse(result) assertFalse(presenter.isCurrentInputValid()) // Actual attempt to login should never have been made since // the input was invalid assertFalse(view.submitWasDisabled) assertTrue(view.submitEnabled) assertNull(view.apiError) assertFalse(view.loginHasSucceeded) assertEquals(expectedEmailError.reason, view.emailError) assertEquals(expectedPasswordError.reason, view.passwordError) }
  184. runBlocking DOESN'T EXIST FOR ALL PLATFORMS

  185. JAVASCRIIIIIIIIPT!

  186. commonTest expect fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T

    ) : T
  187. commonTest expect fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T

    ) : T androidTest actual fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T ): T { return runBlocking { block() } }
  188. commonTest expect fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T

    ) : T androidTest + iosTest actual fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T ): T { return runBlocking { block() } }
  189. @Test fun attemptingToLoginWithoutChangesTriggersErrorsAndFails() = platformRunBlocking { val view = TestLoginView()

    val presenter = LoginPresenter(view, MockStorage()) val expectedEmailError = ValidationResult.Invalid.WasNull("email") val expectedPasswordError = ValidationResult.Invalid.WasNull("password") val result = presenter.loginAsync() assertFalse(result) assertFalse(presenter.isCurrentInputValid()) // Actual attempt to login should never have been made since // the input was invalid assertFalse(view.submitWasDisabled) assertTrue(view.submitEnabled) assertNull(view.apiError) assertFalse(view.loginHasSucceeded) assertEquals(expectedEmailError.reason, view.emailError) assertEquals(expectedPasswordError.reason, view.passwordError) }
  190. None
  191. androidTest WILL ALSO RUN YOUR commonTest TESTS!

  192. None
  193. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT

  194. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT (WHICH HAVE LITTLE TO

    NOTHING TO DO WITH KOTLIN)
  195. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT > Docker

  196. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT > Docker > MySQL

  197. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT > Docker > MySQL

    > ORMs for MySQL
  198. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT > Docker > MySQL

    > ORMs for MySQL > One-way hashing for secure password storage
  199. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT > Docker > MySQL

    > ORMs for MySQL > One-way hashing for secure password storage > Your computer's exact melting point
  200. None
  201. None
  202. None
  203. None
  204. DO WE HAVE TIME FOR A TERRIBLE IDEA?

  205. AND NOW, A TERRIBLE IDEA: A LIVE DEMO!

  206. !"# NEXT STEPS

  207. !"# NEXT STEPS > Build the prototype box

  208. !"# NEXT STEPS > Build the prototype box > Get

    the Raspberry pi software working
  209. !"# NEXT STEPS > Build the prototype box > Get

    the Raspberry pi software working > Get the raspberry pi actually unlocking the box
  210. !"# NEXT STEPS > Build the prototype box > Get

    the Raspberry pi software working > Get the raspberry pi actually unlocking the box > Work on sharing localized strings
  211. !"# NEXT STEPS > Build the prototype box > Get

    the Raspberry pi software working > Get the raspberry pi actually unlocking the box > Work on sharing localized strings > Maybe find someone who knows what they're doing to build it for real?
  212. OBLIGATORY SUMMARY SLIDE!

  213. OBLIGATORY SUMMARY SLIDE! > Kotlin/Native has made great strides in

    the last year
  214. OBLIGATORY SUMMARY SLIDE! > Kotlin/Native has made great strides in

    the last year > Share logic across iOS, android, and server without killing performance or sacrificing type/null safety
  215. OBLIGATORY SUMMARY SLIDE! > Kotlin/Native has made great strides in

    the last year > Share logic across iOS, android, and server without killing performance or sacrificing type/null safety > it's still beta, You will run into a lot of brick walls
  216. OBLIGATORY SUMMARY SLIDE! > Kotlin/Native has made great strides in

    the last year > Share logic across iOS, android, and server without killing performance or sacrificing type/null safety > it's still beta, You will run into a lot of brick walls > Ktor and kotlinX serialization are a huge help, if you can get around what a mess coroutines can be on iOS
  217. OBLIGATORY SUMMARY SLIDE! > Kotlin/Native has made great strides in

    the last year > Share logic across iOS, android, and server without killing performance or sacrificing type/null safety > it's still beta, You will run into a lot of brick walls > Ktor and kotlinX serialization are a huge help, if you can get around what a mess coroutines can be on iOS > This is experimental, but it's stupid amounts of fun
  218. None
  219. LINKS! > Kotlin MPP for iOS + Android tutorial https://kotlinlang.org/docs/tutorials/

    native/mpp-ios-android.html > KotlinConf App https://github.com/JetBrains/kotlinconf- app > KotlinLang Slack https://slack.kotlinlang.org
  220. LINKS! > My original Kotlin Native sandbox https://github.com/designatednerd/ KotlinNativeTest >

    Package Thief vs. Glitter Bomb Trap https://youtube.com/watch?v=xoxhDk-hwuo