The Triumphs and Tribulations of Kotlin/Native In Practice - DroidCon Italy, Torino - April 2019

The Triumphs and Tribulations of Kotlin/Native In Practice - DroidCon Italy, Torino - April 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

April 04, 2019
Tweet

Transcript

  1. 1.

    THE TRIUMPHS AND TRIBULATIONS OF KOTLIN/NATIVE IN PRACTICE DROIDCON ITALY

    | TORINO | APRIL 2019 @DESIGNATEDNERD | BAKKENBAECK.NO | JUSTHUM.COM
  2. 2.
  3. 3.
  4. 4.
  5. 5.

    !

  6. 6.

    !

  7. 7.
  8. 8.
  9. 9.
  10. 10.
  11. 11.
  12. 12.
  13. 13.
  14. 14.
  15. 16.
  16. 17.
  17. 18.
  18. 19.
  19. 20.
  20. 22.
  21. 23.
  22. 24.
  23. 25.
  24. 26.
  25. 27.

    !

  26. 29.
  27. 30.
  28. 31.
  29. 32.
  30. 33.
  31. 34.
  32. 35.
  33. 36.
  34. 37.
  35. 38.
  36. 39.
  37. 40.
  38. 41.
  39. 42.
  40. 43.
  41. 44.
  42. 46.
  43. 47.
  44. 48.
  45. 49.
  46. 50.
  47. 51.

    ! "

  48. 52.
  49. 53.
  50. 55.
  51. 64.
  52. 69.

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

    projects targeting multiple platforms, // for example JVM + Native + JS apply plugin: 'kotlin-multiplatform'
  53. 70.
  54. 71.
  55. 72.
  56. 73.
  57. 74.
  58. 77.
  59. 78.
  60. 80.
  61. 81.
  62. 82.
  63. 86.
  64. 91.
  65. 92.
  66. 93.
  67. 94.
  68. 95.
  69. 96.
  70. 98.
  71. 99.
  72. 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") } }
  73. 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") } }
  74. 107.
  75. 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") }
  76. 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") }
  77. 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") }
  78. 115.
  79. 116.
  80. 117.
  81. 118.
  82. 119.
  83. 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() } } }
  84. 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() } } }
  85. 125.
  86. 126.
  87. 127.
  88. 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 } }
  89. 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 } }
  90. 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 } }
  91. 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 } }
  92. 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() } }
  93. 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() } }
  94. 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() } }
  95. 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 } }
  96. 140.
  97. 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"
  98. 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"
  99. 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"
  100. 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"
  101. 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"
  102. 150.
  103. 151.
  104. 152.
  105. 153.
  106. 154.
  107. 155.
  108. 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() }
  109. 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() } }
  110. 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() } }
  111. 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() } }
  112. 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() } }
  113. 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 } } }
  114. 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 } }
  115. 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) }
  116. 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) }
  117. 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) }
  118. 185.

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

    ) : T androidTest actual fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T ): T { return runBlocking { block() } }
  119. 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() } }
  120. 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) }
  121. 188.
  122. 190.
  123. 196.

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

    > ORMs for MySQL > One-way hashing for secure password storage
  124. 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
  125. 198.
  126. 199.
  127. 200.
  128. 201.
  129. 206.

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

    the Raspberry pi software working
  130. 207.

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

    the Raspberry pi software working > Get the raspberry pi actually unlocking the box
  131. 208.

    !"# 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
  132. 209.

    !"# 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
  133. 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 > Work on sharing localized strings > Maybe find someone who knows what they're doing to build it for real?
  134. 213.

    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
  135. 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 > it's still beta, You will run into a lot of brick walls
  136. 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 > Ktor and kotlinX serialization are a huge help, if you can get around what a mess coroutines can be on iOS
  137. 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 > This is experimental, but it's stupid amounts of fun
  138. 217.
  139. 218.

    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
  140. 219.

    LINKS! > My original Kotlin Native sandbox https://github.com/designatednerd/ KotlinNativeTest >

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