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

Building apps using Kotlin Native - Droidcon Vi...

Jeremy Rempel
September 19, 2019

Building apps using Kotlin Native - Droidcon Vienna 2019

Introduction to using Kotlin Native
When to use MPP
Building an app using KN
Status of tech
Comparison to Flutter

Jeremy Rempel

September 19, 2019
Tweet

More Decks by Jeremy Rempel

Other Decks in Technology

Transcript

  1. R&D

  2. The Status Quo • Different codebases solving the same problems

    • 2 teams with no common language or architecture • Accidental differences resulting in tech debt • The more the codebases diverge the harder it is to maintain and update • We’re spending a lot of time on boilerplate and architecture • Cross platform code is the history of computing
  3. • Code sharing is a solution; Figure out what problem

    you are attempting to solve • Evaluate your team, app and existing codebase • Code sharing doesn’t make sense for all projects • Avoid HDD and Whataboutisms. Do your own analysis Your App Multi Platform
  4. What now? • Finding shared problems and pain points is

    the first step • Android and iOS developers both need to understand basics and tradeoffs of each platform • VIPER, Clean Architecture, MVVM, MVP, MVI are all solving similar problems • We can’t share code until we start breaking down barriers
  5. My MPP Wish List • Avoid big decisions, incremental adoption,

    reduce risk • Integration with underlying platform should be default not exception • Choose the when and what to share. Empower not impose • Reuse one of the supported languages (Kotlin, Swift) Avoid introducing yet another language • Reuse existing skillsets, build tools, IDEs, libraries and tooling
  6. People don’t want to use your software, Its about the

    users! They want to go shopping, lose weight, laugh, be entertained, get smarter, spend time with loved ones, go home on time, sleep better, eat good food and be happy Your product is only as good as the experiences it enables people to have
  7. What is KN? • Compiler toolchain, backend LLVM • Compiles

    to different targets where virtual machines are not available such as iOS • One dependency: Kotlin stdlib • Memory management via reference counting and GC for cycles
  8. Interop with iOS • Kotlin Native is provided as an

    Objective-C binary with header • CocoaPod integration • Common Kotlin code can access iOS platform APIs such as Foundation and UIKit • Ability to utilize all existing abstractions (classes, interfaces, lambdas, exceptions) • Kotlin Native is a library not a framework
  9. fun main() = println("hello world”) ➜ kotlinc helloworld.kt Kotlin JVM

    ➜ java HelloworldKt hello world ➜ file HelloworldKt.class HelloworldKt.class: compiled Java class data, version 50.0 (Java 1.6)
  10. fun main() = println("hello world”) ➜ kotlinc-native helloworld.kt Kotlin Native

    ➜ ./helloworld.kexe hello world ➜ file helloworld.kexe helloworld.kexe: Mach-O 64-bit executable x86_64
  11. Compiling for iOS ➜ ./gradlew :lib:linkMainDebugFrameworkIos BUILD SUCCESSFUL in 9s

    fun main() = println("hello world”) • ➜ file main main: Mach-O 64-bit dynamically linked shared library x86_64 ➜ file Headers/main.h Headers/main.h: Objective-C source text, ASCII text, with very long lines
  12. Shared Common Common Module (Kotlin) Android libs (Kotlin+Java) Android App

    (Kotlin+Java) iOS App (Swift+ObjC) iOS Framework (Kotlin) Android AAR (Kotlin) MPP Libs (Kotlin)
  13. Bridging the Gap • Common code that can only use

    Kotlin stdlib is not useful enough on its own • Kotlin MPP introduces expect and actual • expect defined in the common module defines behavior such as fun log(message: String) • actual provides implementation on how to fulfill the request using the platform specific APIs • Alternatively: IoC pattern, Swift implementations
  14. Example: Logger • Timber is not available on Kotlin Native

    (yet) • Lets take build our own logger • Accept a Log Level (Debug, Info, Error, Fatal), String and optional exception
  15. iOS Actual import platform.Foundation.NSLog actual fun log(level: LogLevel, tag: String,

    message: String, error: Throwable) { NSLog("[$level]: ($tag), $message, $error") } actual fun log(level: LogLevel, tag: String, message: String) { NSLog("[$level]: ($tag)") }
  16. Android Actual import android.util.Log actual fun log(level: LogLevel, tag: String,

    message: String, error: Throwable) { when (level) { is LogLevel.DEBUG -> Log.d(tag, message, error) is LogLevel.INFO -> Log.i(tag, message, error) is LogLevel.WARN -> Log.w(tag, message, error) is LogLevel.ERROR -> Log.e(tag, message, error) } } actual fun log(level: LogLevel, tag: String, message: String) { when (level) { is LogLevel.DEBUG -> Log.d(tag, message) is LogLevel.INFO -> Log.i(tag, message) is LogLevel.WARN -> Log.w(tag, message) is LogLevel.ERROR -> Log.e(tag, message) } }
  17. App: Android View: Fragment Interactor: Business Logic Repository: Persistence (Sqlite,

    Shared Pref) API: Networking (Http, Serialization) Presenter DI: Dagger App: iOS View: UIViewController Common DI: Custom
  18. API Serialization JSON • Jetbrains provides 1st party library support

    via kotlinx.serialization • JSON, CBOR and Protobuf out of box • Annotation based on Kotlin data classes
  19. val input = """ {"name": “Hello Droidcon”} """ data class

    Greeting(val name: String) @Serializable val result: Greeting = Json.parse(Greeting.serializer(), input)
  20. Repository Http Ktor • Android engine provided by OkHttp+OkIO or

    HttpUrlConnection • iOS engine provided by async NSURLSession
  21. suspend fun getGreeting(): Greeting { } return client.get { url

    { takeFrom(“http://myserver") encodedPath = “/greeting/" } }
  22. val mockResponse = """{"name": “Droidcon”}""" val client = HttpClient(MockEngine) {

    } engine { addHandler { respond( mockResponse, HttpStatusCode.OK, headersOf( HttpHeaders.ContentType, “application/json" ) ) } } Json { serializer = KotlinxSerializer().apply { register(Greeting.serializer()) } }
  23. runBlockingTest { val service = Service(client, “http://myapi”) val actualResponse =

    service.getGreeting() val expectedResponse = Greeting(“Hello Droidcon”) assertEquals( actualResponse, expectedResponse ) } @Test fun `run http mock test`() { }
  24. Repository: Kotlin class GreetingService( private val client: HttpClient, private val

    serverEndPoint: String ) : GreetingApi { } override suspend fun getGreeting(): Greeting = client.get { url { takeFrom(serverEndPoint) encodedPath = "/greeting/" } }
  25. Common Testing • Common unit tests are compiled and executed

    on each platform • Android tests executed on JVM via junit, iOS tests are executed on simulator via xcrun • No mockk support yet for non JVM tests, need to create your own mocks manually
  26. iOS testing ➜ ./gradlew :lib:linkDebugTestIos BUILD SUCCESSFUL in 1s 2

    actionable tasks: 2 up-to-date ➜ xcrun simctl spawn 'iPhone 8' ./lib/build/bin/ios/debugTest/ test.kexe [==========] 4 tests from 3 test cases ran. (1 ms total) [ PASSED ] 4 tests.
  27. // view implemented native interface GreetingView { var isUpdating: Boolean

    fun onUpdate(data: Greeting) } MVP Contract: Kotlin // implemented by presenter in common interface GreetingActions { fun onRequestData() }
  28. Presenter: Kotlin } class GreetingPresenter( private val coroutineContext: CoroutineContext, private

    val useCase: GreetingUseCase, private val view: GreetingsView ) : GreetingsActions {
  29. Presenter: Kotlin } override fun onRequestData() { view.isUpdating = true

    launch(coroutineContext) { val result = useCase.getGreeting() view.onUpdate(result) }.invokeOnCompletion { view.isUpdating = false } } class GreetingPresenter( private val coroutineContext: CoroutineContext, private val useCase: GreetingUseCase, private val view: GreetingsView ) : GreetingsActions {
  30. Interactor: Kotlin class GreetingUseCase( private val api: GreetingApi private val

    db: GreetingQueries ) { } suspend fun getGreeting(): Greeting { // check cache first val cache = db.select() if (cache != null) return cache // get from remote val greeting = api.getGreeting() // transform val result = greeting.copy( name = greeting.name.trim().toUpperCase() ) // update cache db.insertOrReplace(result) return result }
  31. Sqlite Persistence: SqlDelight CREATE TABLE greeting ( name TEXT NOT

    NULL ); insertOrReplace: INSERT OR REPLACE INTO greeting(name) VALUES ?; select: SELECT * FROM greeting;
  32. iOS Integration Podfile myapp/iosApp on  master [$+] ➜ cat

    Podfile # Either use_frameworks! or use_modular_headers! must be specified. use_frameworks! platform :ios, '12.2' target 'ios-App' do pod 'lib', :path => '../lib' end
  33. // callback from Kotlin Presenter func onUpdate(response: Greeting) { updateUi(response)

    } } { , GreetingsView class GreetingViewController: UIViewController ViewController: Swift
  34. override func viewDidLoad() { super.viewDidLoad() // view is view controller

    let view: GreetingsView = self let presenter = GreetingsPresenter( uiContext: uiContext, view: view, api: service) ) } } { , GreetingsView class GreetingViewController: UIViewController presenter.onRequestData() ViewController: Swift
  35. Coroutine Context: Swift public class UI: Kotlinx_coroutines_coreCoroutineDispatcher { } override

    public func dispatch(context: KotlinCoroutineContext, block: Kotlinx_coroutines_coreRunnable) { } DispatchQueue.main.async { block.run() }
  36. class GreetingFragment() : Fragment(), GreetingView { } @Inject lateinit var

    actions: GreetingActions override var isUpdating: Boolean by Delegates.observable(false) { _, _, isLoading -> { loadingView.isGone = !isLoading content.isGone = isLoading } override fun onUpdate(response: Greeting) = updateUi(response) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) DaggerContentFragmentComponent.builder() .contentsPresenterModule(GreetingPresenterModule(this)) .build() .inject(this) actions.onRequestData() }
  37. Concurrency • The story is still evolving around concurrency •

    Simple in concept, difficult in practice • KN concurrency is not compatible with existing JVM based coroutines • Bridging the two worlds is challenging and can result in runtime errors • Coroutine Worker (Ben Asher), Reaktive (Badoo)
  38. Meet Worker • Workers are primitive built on top of

    threads • 2 basic rules: • All mutable state is owned by one thread • All shared state is immutable (frozen) • Immutable data can be shared across threads safety • Only available in Native module
  39. Freeze /** * Freezes object subgraph reachable from this object.

    Frozen objects can be freely * shared between threads/workers. * * @throws FreezingException if freezing is not possible * @return the object itself * @see ensureNeverFrozen */ public fun <T> T.freeze(): T
  40. val worker = Worker.start() val future = worker.execute( 2019-08-24 17:55:02.014

    test.kexe[16051:146036] isFrozen: true 2019-08-24 17:55:02.014 test.kexe[16051:146029] isFrozen: true , value: HELLO DROIDCON} TransferMode.SAFE, { "Hello Droidcon".freeze() }, // producer lambda { // bg thread NSLog("isFrozen: ${it.isFrozen}") it.toUpperCase() } ) future.consume { // main thread NSLog("isFrozen: ${it.isFrozen} , value: $it}") } worker.requestTermination(true)
  41. val input = "Fail Whale" val worker = Worker.start() val

    future = worker.execute( TransferMode.SAFE, { "Hello Droidcon”.freeze() }, // producer { } ) kotlin/worker/WorkerTest.kt: (43, 29): kotlin.native.concurrent.Worker.execute must take an unbound, non- capturing function or lambda // bg thread. this will fail it + input
  42. val hello1 = Greeting("Hello Droidcon").freeze() val future = worker.execute( TransferMode.SAFE,

    { ref }, { val hello2 = Greeting("Hello NYC").freeze() it.value = hello2 } ) future.consume { assertEquals("Hello NYC", ref.value.name) } val ref = AtomicReference(hello1) assertTrue(ref.isFrozen) assertEquals("Hello Droidcon", ref.value.name)
  43. val ptr = DetachedObjectGraph { Greeting("Hello Droidcon") }.asCPointer() val future

    = worker.execute(TransferMode.SAFE, { ptr.freeze() }, { // assign ownership to bg thread and mutate val greeting = DetachedObjectGraph<Greeting>(it).attach() greeting.name = "${greeting.name} + Mutate" }) future.consume { // attach to main thread val greeting = DetachedObjectGraph<Greeting>(ptr).attach() assertEquals("Hello Droidcon + Mutate", greeting.name) }
  44. MPP is (still) hard • Developing an app for multiple

    platforms will always be harder than a single platform. There is more surface area • Requires knowledge of Android, iOS and MPP tech • MPP solves some problems and introduces new problems • The community is still figuring out architecture and best practices • Just because things are hard doesn’t mean we shouldn’t do it
  45. KN is the future, Is the future now? • Early

    adopter phase, its usable and stable but it still has some rough edges • 1st party libs (ktor, serialization), compiler still in beta • Documentation is lacking • The IDE support is still a work in progress • Debugging native code is challenging • No multi threaded coroutines • Improving rapidly
  46. KN vs Flutter • Kotlin Native is a compiler technology.

    It provides a library for business logic and backend logic • Kotlin Native compliments the Native SDKs • Flutter is an alternative framework and language (Dart) • Flutter provides a complete solution that encompasses all layers (View, Logic etc) • Flutter maximizes code sharing, KN maximizes native platform interop
  47. Road to Adoption • Start small, avoid making big decisions

    • Prototype and experiment • Discuss with both iOS and Android teams • Be aware of the limitations • Plan your use cases, architecture and team, pick a feature-layer
  48. “Thanks for listening! I hope you learnt something. Feel free

    to contact me with any questions.” twitter @jeremyrempel http://jeremyrempel.com