Pro Yearly is on sale from $80 to $50! »

The Triumphs and Tribulations of Kotlin/Native In Practice - GOTO Copenhagen, Denmark - November 2019

The Triumphs and Tribulations of Kotlin/Native In Practice - GOTO Copenhagen, Denmark - November 2019

The latest and greatest version of this talk!

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
PRO

November 18, 2019
Tweet

Transcript

  1. THE TRIUMPHS AND TRIBULATIONS OF KOTLIN/NATIVE IN PRACTICE GOTO COPENHAGEN

    | NOVEMBER 2019 ELLEN SHAPIRO | @DESIGNATEDNERD | APOLLOGRAPHQL.COM
  2. None
  3. None
  4. None
  5. !

  6. !

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

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

  22. None
  23. None
  24. None
  25. None
  26. None
  27. !

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

  29. None
  30. None
  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. PACKAGE THIEF VS. GLITTER BOMB TRAP HTTPS://WWW.YOUTUBE.COM/WATCH?V=XOXHDK-HWUO

  46. None
  47. None
  48. None
  49. None
  50. None
  51. ! "

  52. None
  53. None
  54. WITH WHAT SYSTEMS SHALL I BUILD IT?

  55. None
  56. Raspberry pi

  57. Raspberry pi Server

  58. Raspberry pi Server iOS app

  59. Raspberry pi Server iOS app Android App

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

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

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

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

  64. None
  65. NATIVE VS. MULTI-PLATFORM

  66. apply plugin: 'kotlin-multiplatform'

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

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

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

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

  76. PROTIP: USE GRADLE 5.4.1* enableFeaturePreview('GRADLE_METADATA') *as of November 18th, 2019

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

  80. None
  81. None
  82. None
  83. FUNCTIONS WITH PER-PLATFORM IMPLEMENTATIONS expect fun

  84. commonMain

  85. commonMain

  86. iosMain

  87. androidMain

  88. commonMain

  89. CALLING CODE FROM BOTH APPS!

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

  91. IOS: MODE

  92. None
  93. None
  94. None
  95. None
  96. None
  97. ! OBJECTIVE-C

  98. None
  99. None
  100. IOS/OBJC GOTCHAS

  101. SWIFT Array != KOTLIN Array

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

  103. SWIFT enum != KOTLIN enum class IN OBJC

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

    Saturday, Sunday; }
  105. 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") } }
  106. 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") } }
  107. None
  108. 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") }
  109. 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") }
  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. DECIDING WHAT TO CENTRALIZE

  112. WHAT DO I REALLY HATE BUILDING TWICE?

  113. TALKING TO THE SERVER

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

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

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

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

    = Dispatchers.Main
  123. 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() } } }
  124. 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() } } }
  125. None
  126. ! FREEZING

  127. None
  128. MUTATING VARIABLES RETURNING A NEW OBJECT

  129. AVOID object FROM IOS

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

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

    fetchTokenString(): String? }
  132. 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 } }
  133. 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 } }
  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. 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() } }
  137. 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() } }
  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. 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 } }
  140. None
  141. KotlinX Serialization

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

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

    String, val password: String )
  144. 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"
  145. 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"
  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. FROM CLIENT TO SERVER AND BACK!

  150. LOGIC

  151. MVC!

  152. MVVM!

  153. MVA!*

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

    VIEW FOR THE LOVE OF GOD
  155. MVP!

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

  157. MODEL: VIEW: PRESENTER:

  158. MODEL: data class VIEW: PRESENTER:

  159. MODEL: data class VIEW: interface PRESENTER:

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

  161. VIEW: interface

  162. IOS VIEW: ANDROID VIEW: TESTING VIEW:

  163. IOS VIEW: UIViewController ANDROID VIEW: TESTING VIEW:

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

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

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

    not apply if your name is jake wharton
  167. 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() }
  168. commonMain class LoginPresenter( val view: LoginView, storage: SecureStorage ): BaseCoroutinePresenter(storage)

    { // rest of presenter class }
  169. 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() } }
  170. 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() } }
  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. Android App

  173. Android App

  174. 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() } }
  175. 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 } } }
  176. TESTING YOUR COMMON CODE

  177. commonTest WORKS IF COROUTINES AREN'T INVOLVED!

  178. 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 } }
  179. @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) }
  180. @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) }
  181. @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) }
  182. runBlocking DOESN'T EXIST FOR ALL PLATFORMS

  183. JAVASCRIIIIIIIIPT!

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

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

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

    ) : T androidTest + iosTest actual fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T ): T { return runBlocking { block() } }
  187. @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) }
  188. None
  189. androidTest WILL ALSO RUN YOUR commonTest TESTS!

  190. None
  191. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT

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

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

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

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

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

    > ORMs for MySQL > One-way hashing for secure password storage
  197. 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
  198. None
  199. None
  200. None
  201. None
  202. DO WE HAVE TIME FOR A TERRIBLE IDEA?

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

  204. !"# NEXT STEPS

  205. None
  206. None
  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 design elements
  211. !"# NEXT STEPS > Build the prototype box > Get

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

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

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

    the last year
  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
  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
  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
  218. 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
  219. None
  220. 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
  221. LINKS! > My original Kotlin Native sandbox https://github.com/designatednerd/ KotlinNativeTest >

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