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

Sharing Kotlin Code across platforms

Avatar for Kurt Renzo Acosta Kurt Renzo Acosta
September 29, 2019
71

Sharing Kotlin Code across platforms

Avatar for Kurt Renzo Acosta

Kurt Renzo Acosta

September 29, 2019

Transcript

  1. Native Kotlin Multiplatform Cross-platform Code sharing None Partial Full Platform

    targets Specific Any Depends on framework Performance Native Native Depends on framework Development Cost Highest Higher High Access to platform APIs Yes Yes Depends on framework Time to market Slowest Average Fastest
  2. Desktop Domain Entities, Use Cases / Interactors Data Repositories, HTTP

    Clients, Cache iOS Web Android Common Presentation View Models, Presenters, Controllers Presentation Presentation Presentation Presentation
  3. // common.kt expect fun platformName(): String // actual-android.kt actual fun

    platformName(): String = "Android" // actual-ios.kt actual fun platformName(): String = "iOS" // actual-jvm.kt actual fun platformName(): String = "JVM"
  4. // common.kt expect fun platformName(): String // actual-android.kt actual fun

    platformName(): String = "Android" // actual-ios.kt actual fun platformName(): String = "iOS" // actual-jvm.kt actual fun platformName(): String = "JVM"
  5. // remote-source.kt expect class RemoteSource { fun fetchFromServer(): List<Item> }

    // android-remote-source.kt actual class RemoteSource { fun fetchFromServer(): List<Item> { // Retrofit } } // ios-remote-source.kt actual class RemoteSource { fun fetchFromServer(): List<Item> { // AlamoFire / NSURLSession } }
  6. // remote-source.kt expect class RemoteSource { fun fetchFromServer(): List<Item> }

    // android-remote-source.kt actual class RemoteSource { fun fetchFromServer(): List<Item> { // Retrofit } } // ios-remote-source.kt actual class RemoteSource { fun fetchFromServer(): List<Item> { // AlamoFire / NSURLSession } }
  7. apply plugin: "org.jetbrains.kotlin.multiplatform" kotlin { targets { ... } sourceSets

    { iosMain { dependencies { ... } } commonMain { dependencies { ... } } } }
  8. Multiplatform Libraries • HTTP Clients ◦ Ktor • JSON Serialization

    ◦ Kotlin Serialization • Storage ◦ SQLDelight • Dependency Injection ◦ Kodein • Asynchronous Code ◦ Kotlin Coroutines
  9. private val client = HttpClient(clientEngine) client.post("https://my.api.url/path/get") client.get<String>("https://my.api.url/path/post") client.call { method

    = HttpMethod.Post, url = "https://my.api.url/path/get", body = PostRequestBody(...) } client.request<String> { method = HttpMethod.Post, url = "myurl" }
  10. private val client = HttpClient(clientEngine) client.post("https://my.api.url/path/get") client.get<String>("https://my.api.url/path/post") client.call { method

    = HttpMethod.Post, url = "https://my.api.url/path/get", body = PostRequestBody(...) } client.request<String> { method = HttpMethod.Post, url = "myurl" }
  11. private val client = HttpClient(clientEngine) client.post("https://my.api.url/path/get") client.get<String>("https://my.api.url/path/post") client.call { method

    = HttpMethod.Post, url = "https://my.api.url/path/get", body = PostRequestBody(...) } client.request<String> { method = HttpMethod.Post, url = "myurl" }
  12. private val client = HttpClient(clientEngine) client.post("https://my.api.url/path/get") client.get<String>("https://my.api.url/path/post") client.call { method

    = HttpMethod.Post, url = "https://my.api.url/path/get", body = PostRequestBody(...) } client.request<String> { method = HttpMethod.Post, url = "myurl" }
  13. @Serializable data class Data(val a: Int, val b: String =

    "42") val json = Json(JsonConfiguration.Stable) // Serialization val jsonData = json.stringify(Data.serializer(), Data(42)) val jsonList = json.stringify( Data.serializer().list, listOf(Data(42)) ) // Deserialization val data = json.parse(Data.serializer(), """{"a":42}""")
  14. @Serializable data class Data(val a: Int, val b: String =

    "42") val json = Json(JsonConfiguration.Stable) // Serialization val jsonData = json.stringify(Data.serializer(), Data(42)) val jsonList = json.stringify( Data.serializer().list, listOf(Data(42)) ) // Deserialization val data = json.parse(Data.serializer(), """{"a":42}""")
  15. @Serializable data class Data(val a: Int, val b: String =

    "42") val json = Json(JsonConfiguration.Stable) // Serialization val jsonData = json.stringify(Data.serializer(), Data(42)) val jsonList = json.stringify( Data.serializer().list, listOf(Data(42)) ) // Deserialization val data = json.parse(Data.serializer(), """{"a":42}""")
  16. @Serializable data class Data(val a: Int, val b: String =

    "42") val json = Json(JsonConfiguration.Stable) // Serialization val jsonData = json.stringify(Data.serializer(), Data(42)) val jsonList = json.stringify( Data.serializer().list, listOf(Data(42)) ) // Deserialization val data = json.parse(Data.serializer(), """{"a":42}""")
  17. @Serializable data class Data(val a: Int, val b: String =

    "42") val json = Json(JsonConfiguration.Stable) // Serialization val jsonData = json.stringify(Data.serializer(), Data(42)) val jsonList = json.stringify( Data.serializer().list, listOf(Data(42)) ) // Deserialization val data = json.parse(Data.serializer(), """{"a":42}""")
  18. CREATE TABLE MyObject ( id INTEGER NOT NULL, myText TEXT

    NOT NULL, myOtherText TEXT NOT NULL ); getAllMyObjects: SELECT * FROM MyObject; insetMyObject: INSERT INTO MyObject (id, myText, myOtherText) VALUES ?;
  19. CREATE TABLE MyObject ( id INTEGER NOT NULL, myText TEXT

    NOT NULL, myOtherText TEXT NOT NULL ); getAllMyObjects: SELECT * FROM MyObject; insetMyObject: INSERT INTO MyObject (id, myText, myOtherText) VALUES ?;
  20. CREATE TABLE MyObject ( id INTEGER NOT NULL, myText TEXT

    NOT NULL, myOtherText TEXT NOT NULL ); getAllMyObjects: SELECT * FROM MyObject; insetMyObject: INSERT INTO MyObject (id, myText, myOtherText) VALUES ?;
  21. // Generated by SQLDelight fun getAllMyObjects(): Query<MyObject> fun insertMyObject(myObject: MyObject)

    // Repository.kt val myDb = AndroidSqliteDriver( MyDatabase.Schema, context, "mydb.db" ) // NativeSqliteDriver for Native fun getAllMyObjects(): List<MyObject> { return myDb.getAllMyObjects().executeAsList() }
  22. // Generated by SQLDelight fun getAllMyObjects(): Query<MyObject> fun insertMyObject(myObject: MyObject)

    // Repository.kt val myDb = AndroidSqliteDriver( MyDatabase.Schema, context, "mydb.db" ) // NativeSqliteDriver for Native fun getAllMyObjects(): List<MyObject> { return myDb.getAllMyObjects().executeAsList() }
  23. // Generated by SQLDelight fun getAllMyObjects(): Query<MyObject> fun insertMyObject(myObject: MyObject)

    // Repository.kt val myDb = AndroidSqliteDriver( MyDatabase.Schema, context, "mydb.db" ) // NativeSqliteDriver for Native fun getAllMyObjects(): List<MyObject> { return myDb.getAllMyObjects().executeAsList() }
  24. kotlin { targets { // iOS target platform depending on

    Xcode environment def iosPreset = System.getenv("SDK_NAME")?.startsWith("iphoneos") ? presets.iosArm64 : presets.iosX64 fromPreset(iosPreset, 'ios') { binaries { framework("Common") { if (iosPreset == presets.iosX64) { embedBitcode("disable") } } } } android("android") } sourceSets { … } }
  25. kotlin { targets { // iOS target platform depending on

    Xcode environment def iosPreset = System.getenv("SDK_NAME")?.startsWith("iphoneos") ? presets.iosArm64 : presets.iosX64 fromPreset(iosPreset, 'ios') { binaries { framework("Common") { if (iosPreset == presets.iosX64) { embedBitcode("disable") } } } } android("android") } sourceSets { … } }
  26. kotlin { targets { // iOS target platform depending on

    Xcode environment def iosPreset = System.getenv("SDK_NAME")?.startsWith("iphoneos") ? presets.iosArm64 : presets.iosX64 fromPreset(iosPreset, 'ios') { binaries { framework("Common") { if (iosPreset == presets.iosX64) { embedBitcode("disable") } } } } android("android") } sourceSets { … } }
  27. kotlin { targets { // iOS target platform depending on

    Xcode environment def iosPreset = System.getenv("SDK_NAME")?.startsWith("iphoneos") ? presets.iosArm64 : presets.iosX64 fromPreset(iosPreset, 'ios') { binaries { framework("Common") { if (iosPreset == presets.iosX64) { embedBitcode("disable") } } } } android("android") } sourceSets { … } }
  28. android { … } dependencies { … } kotlin {

    targets { … } sourceSets { iosMain { dependencies { … } } commonMain { dependencies { … } } } }
  29. android { … } dependencies { … } kotlin {

    targets { … } sourceSets { iosMain { dependencies { … } } commonMain { dependencies { … } } } }
  30. class GetJokes( private val jokesRepository: JokesRepository ) { suspend operator

    fun invoke() = jokesRepository.getJokes() } interface JokesRepository { suspend fun getJokes(): List<Joke> } Common Code - Domain
  31. class GetJokes( private val jokesRepository: JokesRepository ) { suspend operator

    fun invoke() = jokesRepository.getJokes() } interface JokesRepository { suspend fun getJokes(): List<Joke> } Common Code - Domain
  32. class GetJokes( private val jokesRepository: JokesRepository ) { suspend operator

    fun invoke() = jokesRepository.getJokes() } interface JokesRepository { suspend fun getJokes(): List<Joke> } Common Code - Domain
  33. class JokesRepositoryImpl( private val jokesRemoteSource: JokesRemoteSource, private val jokesLocalSource: JokesLocalSource

    ) : JokesRepository { override suspend fun getJokes(): List<Joke> { try { fetchJokes() } catch (exception: Exception) { println("Repository Error: ${exception.message}") } finally { return jokesLocalSource.getJokes() } } private suspend fun fetchJokes() { val jokes = jokesRemoteSource.getJokes() jokesLocalSource.saveJokes(jokes) } } Common Code - Data - Jokes Repository
  34. class JokesRepositoryImpl( private val jokesRemoteSource: JokesRemoteSource, private val jokesLocalSource: JokesLocalSource

    ) : JokesRepository { override suspend fun getJokes(): List<Joke> { try { fetchJokes() } catch (exception: Exception) { println("Repository Error: ${exception.message}") } finally { return jokesLocalSource.getJokes() } } private suspend fun fetchJokes() { val jokes = jokesRemoteSource.getJokes() jokesLocalSource.saveJokes(jokes) } } Common Code - Data - Jokes Repository
  35. class JokesRepositoryImpl( private val jokesRemoteSource: JokesRemoteSource, private val jokesLocalSource: JokesLocalSource

    ) : JokesRepository { override suspend fun getJokes(): List<Joke> { try { fetchJokes() } catch (exception: Exception) { println("Repository Error: ${exception.message}") } finally { return jokesLocalSource.getJokes() } } private suspend fun fetchJokes() { val jokes = jokesRemoteSource.getJokes() jokesLocalSource.saveJokes(jokes) } } Common Code - Data - Jokes Repository
  36. class JokesRepositoryImpl( private val jokesRemoteSource: JokesRemoteSource, private val jokesLocalSource: JokesLocalSource

    ) : JokesRepository { override suspend fun getJokes(): List<Joke> { try { fetchJokes() } catch (exception: Exception) { println("Repository Error: ${exception.message}") } finally { return jokesLocalSource.getJokes() } } private suspend fun fetchJokes() { val jokes = jokesRemoteSource.getJokes() jokesLocalSource.saveJokes(jokes) } } Common Code - Data - Jokes Repository
  37. class JokesRepositoryImpl( private val jokesRemoteSource: JokesRemoteSource, private val jokesLocalSource: JokesLocalSource

    ) : JokesRepository { override suspend fun getJokes(): List<Joke> { try { fetchJokes() } catch (exception: Exception) { println("Repository Error: ${exception.message}") } finally { return jokesLocalSource.getJokes() } } private suspend fun fetchJokes() { val jokes = jokesRemoteSource.getJokes() jokesLocalSource.saveJokes(jokes) } } Common Code - Data - Jokes Repository
  38. class JokesRepositoryImpl( private val jokesRemoteSource: JokesRemoteSource, private val jokesLocalSource: JokesLocalSource

    ) : JokesRepository { override suspend fun getJokes(): List<Joke> { try { fetchJokes() } catch (exception: Exception) { println("Repository Error: ${exception.message}") } finally { return jokesLocalSource.getJokes() } } private suspend fun fetchJokes() { val jokes = jokesRemoteSource.getJokes() jokesLocalSource.saveJokes(jokes) } } Common Code - Data - Jokes Repository
  39. expect val engine: HttpClientEngine class JokesRemoteSource(clientEngine: HttpClientEngine) { private val

    client = HttpClient(clientEngine) { install(JsonFeature) { serializer = KotlinxSerializer().apply { register(Joke.serializer().list) } } } suspend fun getJokes(): List<Joke> = client.get( “https://official-joke-api.appspot.com/jokes/ten" ) } Common Code - Data - Jokes Remote Source
  40. expect val engine: HttpClientEngine class JokesRemoteSource(clientEngine: HttpClientEngine) { private val

    client = HttpClient(clientEngine) { install(JsonFeature) { serializer = KotlinxSerializer().apply { register(Joke.serializer().list) } } } suspend fun getJokes(): List<Joke> = client.get( “https://official-joke-api.appspot.com/jokes/ten" ) } Common Code - Data - Jokes Remote Source
  41. expect val engine: HttpClientEngine // Android actual val engine by

    lazy { OkHttp.create { val networkInterceptor = HttpLoggingInterceptor() .apply { level = HttpLoggingInterceptor.Level.BODY } addNetworkInterceptor(networkInterceptor) } } // iOS import io.ktor.client.engine.ios.Ios actual val engine by lazy { Ios.create() } Common Code - Data - Jokes Remote Source
  42. expect val engine: HttpClientEngine // Android actual val engine by

    lazy { OkHttp.create { val networkInterceptor = HttpLoggingInterceptor() .apply { level = HttpLoggingInterceptor.Level.BODY } addNetworkInterceptor(networkInterceptor) } } // iOS import io.ktor.client.engine.ios.Ios actual val engine by lazy { Ios.create() } Common Code - Data - Jokes Remote Source
  43. expect val engine: HttpClientEngine // Android actual val engine by

    lazy { OkHttp.create { val networkInterceptor = HttpLoggingInterceptor() .apply { level = HttpLoggingInterceptor.Level.BODY } addNetworkInterceptor(networkInterceptor) } } // iOS import io.ktor.client.engine.ios.Ios actual val engine by lazy { Ios.create() } Common Code - Data - Jokes Remote Source
  44. class JokesRemoteSource(clientEngine: HttpClientEngine) { private val client = HttpClient(clientEngine) {

    install(JsonFeature) { serializer = KotlinxSerializer().apply { register(Joke.serializer().list) } } } suspend fun getJokes(): List<Joke> = client.get( “https://official-joke-api.appspot.com/jokes/ten" ) } Common Code - Data - Jokes Remote Source
  45. class JokesRemoteSource(clientEngine: HttpClientEngine) { private val client = HttpClient(clientEngine) {

    install(JsonFeature) { serializer = KotlinxSerializer().apply { register(Joke.serializer().list) } } } suspend fun getJokes(): List<Joke> = client.get( “https://official-joke-api.appspot.com/jokes/ten" ) } Common Code - Data - Jokes Remote Source
  46. class JokesRemoteSource(clientEngine: HttpClientEngine) { private val client = HttpClient(clientEngine) {

    install(JsonFeature) { serializer = KotlinxSerializer().apply { register(Joke.serializer().list) } } } suspend fun getJokes(): List<Joke> = client.get( “https://official-joke-api.appspot.com/jokes/ten" ) } Common Code - Data - Jokes Remote Source
  47. class JokesLocalSource { var driver: SqlDriver? = JokesDatabaseDriver.jokesDatabaseDriver private val

    db by lazy { val driver = requireNotNull(driver) { "No SqlDriver found!" } JokesDatabase.invoke(driver).jokeQueries } fun getJokes(): List<Joke> { return db.getAllJokes().executeAsList() } fun saveJokes(jokes: List<Joke>) { jokes.forEach { db.insertJoke(it) } } } Common Code - Data - Jokes Local Source
  48. class JokesLocalSource { var driver: SqlDriver? = JokesDatabaseDriver.jokesDatabaseDriver private val

    db by lazy { val driver = requireNotNull(driver) { "No SqlDriver found!" } JokesDatabase.invoke(driver).jokeQueries } fun getJokes(): List<Joke> { return db.getAllJokes().executeAsList() } fun saveJokes(jokes: List<Joke>) { jokes.forEach { db.insertJoke(it) } } } Common Code - Data - Jokes Local Source
  49. expect object JokesDatabaseDriver { var jokesDatabaseDriver: SqlDriver? } // Android

    actual object JokesDatabaseDriver { actual var jokesDatabaseDriver: SqlDriver? = null fun createDriver(context: Context) { jokesDatabaseDriver = AndroidSqliteDriver(JokesDatabase.Schema, context, "jokes.db") } } // iOS actual object JokesDatabaseDriver { actual var jokesDatabaseDriver: SqlDriver? = NativeSqliteDriver(JokesDatabase.Schema, "jokes.db") } Common Code - Data - Jokes Local Source
  50. expect object JokesDatabaseDriver { var jokesDatabaseDriver: SqlDriver? } // Android

    actual object JokesDatabaseDriver { actual var jokesDatabaseDriver: SqlDriver? = null fun createDriver(context: Context) { jokesDatabaseDriver = AndroidSqliteDriver(JokesDatabase.Schema, context, "jokes.db") } } // iOS actual object JokesDatabaseDriver { actual var jokesDatabaseDriver: SqlDriver? = NativeSqliteDriver(JokesDatabase.Schema, "jokes.db") } Common Code - Data - Jokes Local Source
  51. expect object JokesDatabaseDriver { var jokesDatabaseDriver: SqlDriver? } // Android

    actual object JokesDatabaseDriver { actual var jokesDatabaseDriver: SqlDriver? = null fun createDriver(context: Context) { jokesDatabaseDriver = AndroidSqliteDriver(JokesDatabase.Schema, context, "jokes.db") } } // iOS actual object JokesDatabaseDriver { actual var jokesDatabaseDriver: SqlDriver? = NativeSqliteDriver(JokesDatabase.Schema, "jokes.db") } Common Code - Data - Jokes Local Source
  52. class JokesLocalSource { var driver: SqlDriver? = JokesDatabaseDriver.jokesDatabaseDriver private val

    db by lazy { val driver = requireNotNull(driver) { "No SqlDriver found!" } JokesDatabase.invoke(driver).jokeQueries } fun getJokes(): List<Joke> { return db.getAllJokes().executeAsList() } fun saveJokes(jokes: List<Joke>) { jokes.forEach { db.insertJoke(it) } } } Common Code - Data - Jokes Local Source
  53. class JokesLocalSource { var driver: SqlDriver? = JokesDatabaseDriver.jokesDatabaseDriver private val

    db by lazy { val driver = requireNotNull(driver) { "No SqlDriver found!" } JokesDatabase.invoke(driver).jokeQueries } fun getJokes(): List<Joke> { return db.getAllJokes().executeAsList() } fun saveJokes(jokes: List<Joke>) { jokes.forEach { db.insertJoke(it) } } } Common Code - Data - Jokes Local Source
  54. CREATE TABLE JokeDb ( id INTEGER NOT NULL, setup TEXT

    NOT NULL, punchline TEXT NOT NULL, type TEXT NOT NULL ); getAllJokes: SELECT * FROM JokeDb; insertJoke: INSERT INTO JokeDb (id, setup, punchline, type) VALUES ?; Common Code - Data - Jokes Local Source
  55. @ThreadLocal object ServiceLocator { val jokesLocalSource = JokesLocalSource() val jokesRemoteSource

    = JokesRemoteSource(engine) val jokesRepository: JokesRepository = JokesRepositoryImpl(jokesRemoteSource, jokesLocalSource) val getJokes = GetJokes(jokesRepository) } Common Code - Dependency Injection
  56. @ThreadLocal object ServiceLocator { val jokesLocalSource = JokesLocalSource() val jokesRemoteSource

    = JokesRemoteSource(engine) val jokesRepository: JokesRepository = JokesRepositoryImpl(jokesRemoteSource, jokesLocalSource) val getJokes = GetJokes(jokesRepository) } Common Code - Dependency Injection
  57. class JokesViewModel( private val getJokes: GetJokes ) : ViewModel() {

    private val _jokes = MutableLiveData<List<Joke>>() val jokes: LiveData<List<Joke>> = _jokes init { viewModelScope.launch { _jokes.value = getJokes() } } } Android Code - Presentation
  58. class JokesViewModel( private val getJokes: GetJokes ) : ViewModel() {

    private val _jokes = MutableLiveData<List<Joke>>() val jokes: LiveData<List<Joke>> = _jokes init { viewModelScope.launch { _jokes.value = getJokes() } } } Android Code - Presentation
  59. class JokesFragment : Fragment(R.layout.fragment_jokes) { private val viewModel: JokesViewModel by

    viewModels { JokesViewModelFactory(ServiceLocator.getJokes) } private val jokesAdapter by lazy { JokesListAdapter() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // Assign RecyclerView to Adapter viewModel.jokes.observe(viewLifecycleOwner, Observer { jokesAdapter.submitList(it) }) } } Android Code - Presentation
  60. class JokesPresenter( private val jokesView: JokesView, private val getJokes: GetJokes

    ) { val presenterJob = SupervisorJob() val presenterScope: CoroutineScope = CoroutineScope(defaultDispatcher + presenterJob) fun getJokes() { presenterScope.launch { val jokes = getJokes.invoke() jokesView.showJokes(jokes) } } } iOS Code - Presentation
  61. class JokesPresenter( private val jokesView: JokesView, private val getJokes: GetJokes

    ) { val presenterJob = SupervisorJob() val presenterScope: CoroutineScope = CoroutineScope(defaultDispatcher + presenterJob) fun getJokes() { presenterScope.launch { val jokes = getJokes.invoke() jokesView.showJokes(jokes) } } } iOS Code - Presentation
  62. class ViewController: UITableViewController, JokesView { // MARK: Properties var jokes

    = [Joke]() // MARK: JokesView func showJokes(jokes: [Joke]) { self.jokes += jokes tableView.reloadData() } override func viewDidLoad() { let presenter = JokesPresenter.init( jokesView: self, getJokes: ServiceLocator.init().getJokes ) presenter.getJokes() } } iOS Code - Presentation
  63. class ViewController: UITableViewController, JokesView { // MARK: Properties var jokes

    = [Joke]() // MARK: JokesView func showJokes(jokes: [Joke]) { self.jokes += jokes tableView.reloadData() } override func viewDidLoad() { let presenter = JokesPresenter.init( jokesView: self, getJokes: ServiceLocator.init().getJokes ) presenter.getJokes() } } iOS Code - Presentation
  64. Gotchas • Tedious to set up • Lack of libraries

    in Multiplatform projects • IDE Support • Kotlin/Native still in Beta • Multiplatform is still experimental