Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

The Triumphs and Tribulations of Kotlin/Native ...

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

Ellen Shapiro

April 04, 2019
Tweet

More Decks by Ellen Shapiro

Other Decks in Technology

Transcript

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

    | TORINO | APRIL 2019 @DESIGNATEDNERD | BAKKENBAECK.NO | JUSTHUM.COM
  2. !

  3. !

  4. !

  5. ! "

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

    projects targeting multiple platforms, // for example JVM + Native + JS apply plugin: 'kotlin-multiplatform'
  7. 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") } }
  8. 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") } }
  9. 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") }
  10. 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") }
  11. 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") }
  12. 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() } } }
  13. 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() } } }
  14. 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 } }
  15. 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 } }
  16. 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 } }
  17. 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 } }
  18. 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() } }
  19. 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() } }
  20. 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() } }
  21. 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 } }
  22. commonMain val creds = UserCredentials("[email protected]", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"[email protected]","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "[email protected]" // updatedCreds.password is "guessme"
  23. commonMain val creds = UserCredentials("[email protected]", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"[email protected]","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "[email protected]" // updatedCreds.password is "guessme"
  24. commonMain val creds = UserCredentials("[email protected]", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"[email protected]","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "[email protected]" // updatedCreds.password is "guessme"
  25. commonMain val creds = UserCredentials("[email protected]", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"[email protected]","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "[email protected]" // updatedCreds.password is "guessme"
  26. commonMain val creds = UserCredentials("[email protected]", "guessme") val string = Json.stringify(UserCredentials.serializer(),

    creds) // String is '{"username":"[email protected]","password":"guessme"}' val updatedCreds = Json.parse(UserCredentials.serializer(), string) // updatedCreds.username is "[email protected]" // updatedCreds.password is "guessme"
  27. 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() }
  28. 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() } }
  29. 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() } }
  30. 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() } }
  31. 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() } }
  32. 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 } } }
  33. 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 } }
  34. @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 = "[email protected]" presenter.validateEmail() assertTrue(presenter.isCurrentInputValid()) assertNull(view.emailError) assertNull(view.passwordError) }
  35. @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) }
  36. @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) }
  37. commonTest expect fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T

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

    ) : T androidTest + iosTest actual fun <T> platformRunBlocking( block: suspend CoroutineScope.() -> T ): T { return runBlocking { block() } }
  39. @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) }
  40. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT > Docker > MySQL

    > ORMs for MySQL > One-way hashing for secure password storage
  41. 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
  42. !"# NEXT STEPS > Build the prototype box > Get

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

    the Raspberry pi software working > Get the raspberry pi actually unlocking the box
  44. !"# 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
  45. !"# 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
  46. !"# 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?
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. LINKS! > My original Kotlin Native sandbox https://github.com/designatednerd/ KotlinNativeTest >

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