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

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

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

    View full-size slide

  2. THE
    VIRTUAL MACHINE

    View full-size slide

  3. SHAMELESS PLUG!
    HTTPS://STORE.RAYWENDERLICH.COM
    PRODUCTS/KOTLIN-APPRENTICE

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. WITH WHAT SYSTEMS
    SHALL I BUILD IT?

    View full-size slide

  7. Raspberry pi

    View full-size slide

  8. Raspberry pi
    Server

    View full-size slide

  9. Raspberry pi
    Server
    iOS app

    View full-size slide

  10. Raspberry pi
    Server
    iOS app
    Android App

    View full-size slide

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

    View full-size slide

  12. Works With Kotlin?
    Raspberry pi ✅
    Server ✅
    iOS app ✅
    Android App ✅

    View full-size slide

  13. HTTP://BAKKENBAECK.NO

    View full-size slide

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

    View full-size slide

  15. NATIVE VS. MULTI-PLATFORM

    View full-size slide

  16. apply plugin: 'kotlin-multiplatform'

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  19. // For Native ONLY projects
    apply plugin: 'kotlin-native-platform'
    // For projects targeting multiple platforms,
    // for example JVM + Native + JS
    apply plugin: 'kotlin-multiplatform'

    View full-size slide

  20. PROTIP: USE GRADLE 5.1.1*
    enableFeaturePreview('GRADLE_METADATA')

    View full-size slide

  21. PROTIP: USE GRADLE 5.1.1*
    enableFeaturePreview('GRADLE_METADATA')
    *as of April 4, 2019

    View full-size slide

  22. By Jorje Lascar, https://www.flickr.com/photos/jlascar/4503951595

    View full-size slide

  23. FUNCTIONS WITH PER-PLATFORM IMPLEMENTATIONS
    expect fun

    View full-size slide

  24. CALLING CODE FROM
    BOTH APPS!

    View full-size slide

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

    View full-size slide

  26. !
    OBJECTIVE-C

    View full-size slide

  27. IOS/OBJC GOTCHAS

    View full-size slide

  28. SWIFT Array != KOTLIN Array

    View full-size slide

  29. SWIFT Array == KOTLIN List

    View full-size slide

  30. SWIFT enum != KOTLIN enum class IN OBJC

    View full-size slide

  31. KOTLIN
    enum class DayOfWeek {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday;
    }

    View full-size slide

  32. 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")
    }
    }

    View full-size slide

  33. 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")
    }
    }

    View full-size slide

  34. 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")
    }

    View full-size slide

  35. 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")
    }

    View full-size slide

  36. 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")
    }

    View full-size slide

  37. DECIDING WHAT TO
    CENTRALIZE

    View full-size slide

  38. WHAT DO I REALLY HATE
    BUILDING TWICE?

    View full-size slide

  39. TALKING TO THE SERVER

    View full-size slide

  40. JSON FROM NETWORK IN
    MODELS

    JSON OUT TO NETWORK

    View full-size slide

  41. IosClientEngine.kt
    https://github.com/ktorio/ktor/blob/master/
    ktor-client/ktor-client-ios/darwin/src/io/
    ktor/client/engine/ios/IosClientEngine.kt

    View full-size slide

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

    View full-size slide

  43. androidMain
    import kotlinx.coroutines.CoroutineDispatcher
    import kotlinx.coroutines.Dispatchers
    internal actual val ApplicationDispatcher: CoroutineDispatcher = Dispatchers.Main

    View full-size slide

  44. 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()
    }
    }
    }

    View full-size slide

  45. 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()
    }
    }
    }

    View full-size slide

  46. MUTATING VARIABLES
    RETURNING A NEW OBJECT

    View full-size slide

  47. AVOID object FROM IOS

    View full-size slide

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

    View full-size slide

  49. commonMain
    interface SecureStorage {
    fun storeTokenString(token: String)
    fun clearTokenString()
    fun fetchTokenString(): String?
    }

    View full-size slide

  50. 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
    }
    }

    View full-size slide

  51. 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
    }
    }

    View full-size slide

  52. 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
    }
    }

    View full-size slide

  53. 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
    }
    }

    View full-size slide

  54. 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()
    }
    }

    View full-size slide

  55. 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()
    }
    }

    View full-size slide

  56. 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()
    }
    }

    View full-size slide

  57. 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
    }
    }

    View full-size slide

  58. KotlinX Serialization

    View full-size slide

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

    View full-size slide

  60. commonMain
    apply plugin: 'kotlinx-serialization'
    @Serializable
    data class UserCredentials(
    val username: String,
    val password: String
    )

    View full-size slide

  61. 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"

    View full-size slide

  62. 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"

    View full-size slide

  63. 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"

    View full-size slide

  64. 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"

    View full-size slide

  65. 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"

    View full-size slide

  66. FROM CLIENT TO SERVER AND BACK!

    View full-size slide

  67. MVA!*
    * - ANYTHING OTHER THAN A MODEL OR A VIEW FOR THE LOVE OF GOD

    View full-size slide

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

    View full-size slide

  69. MODEL:
    VIEW:
    PRESENTER:

    View full-size slide

  70. MODEL: data class
    VIEW:
    PRESENTER:

    View full-size slide

  71. MODEL: data class
    VIEW: interface
    PRESENTER:

    View full-size slide

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

    View full-size slide

  73. VIEW: interface

    View full-size slide

  74. IOS VIEW:
    ANDROID VIEW:
    TESTING VIEW:

    View full-size slide

  75. IOS VIEW: UIViewController
    ANDROID VIEW:
    TESTING VIEW:

    View full-size slide

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

    View full-size slide

  77. IOS VIEW: UIViewController
    ANDROID VIEW: Fragment*
    TESTING VIEW:
    *May not apply if your name is jake wharton

    View full-size slide

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

    View full-size slide

  79. 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()
    }

    View full-size slide

  80. commonMain
    class LoginPresenter(
    val view: LoginView,
    storage: SecureStorage
    ): BaseCoroutinePresenter(storage) {
    // rest of presenter class
    }

    View full-size slide

  81. 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()
    }
    }

    View full-size slide

  82. 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()
    }
    }

    View full-size slide

  83. 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()
    }
    }

    View full-size slide

  84. 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()
    }
    }

    View full-size slide

  85. 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 }
    }
    }

    View full-size slide

  86. TESTING YOUR COMMON CODE

    View full-size slide

  87. commonTest
    WORKS IF COROUTINES AREN'T INVOLVED!

    View full-size slide

  88. 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
    }
    }

    View full-size slide

  89. @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)
    }

    View full-size slide

  90. @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)
    }

    View full-size slide

  91. @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)
    }

    View full-size slide

  92. runBlocking
    DOESN'T EXIST FOR ALL PLATFORMS

    View full-size slide

  93. JAVASCRIIIIIIIIPT!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  97. @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)
    }

    View full-size slide

  98. androidTest
    WILL ALSO RUN YOUR commonTest TESTS!

    View full-size slide

  99. THINGS YOU'LL LEARN DOING
    SERVER DEVELOPMENT

    View full-size slide

  100. THINGS YOU'LL LEARN DOING
    SERVER DEVELOPMENT
    (WHICH HAVE LITTLE TO NOTHING TO DO WITH KOTLIN)

    View full-size slide

  101. THINGS YOU'LL LEARN DOING SERVER DEVELOPMENT
    > Docker

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  105. 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

    View full-size slide

  106. DO WE HAVE TIME FOR
    A TERRIBLE IDEA?

    View full-size slide

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

    View full-size slide

  108. !"#
    NEXT STEPS

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  114. !"#
    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?

    View full-size slide

  115. OBLIGATORY SUMMARY SLIDE!

    View full-size slide

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

    View full-size slide

  117. 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

    View full-size slide

  118. 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

    View full-size slide

  119. 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

    View full-size slide

  120. 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

    View full-size slide

  121. 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

    View full-size slide

  122. LINKS!
    > My original Kotlin Native sandbox
    https://github.com/designatednerd/
    KotlinNativeTest
    > Package Thief vs. Glitter Bomb Trap
    https://youtube.com/watch?v=xoxhDk-hwuo

    View full-size slide