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

2019-droidcon-nyc.pdf

 2019-droidcon-nyc.pdf

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

Jeremy Rempel

August 26, 2019
Tweet

More Decks by Jeremy Rempel

Other Decks in Technology

Transcript

  1. R&D

  2. What is this talk? • Why Multiplatform? • Introduction to

    Kotlin Native technology • Basic Android/iOS App • Comparison to other tech such as Flutter • Current state of Kotlin Native • Not a deep dive
  3. The Status Quo • 2 code bases (Swift/ObjC) + (Java/Kotlin).

    DRY • 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
  4. • 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
  5. What now? • Finding common ground and shared problems is

    the first step • Android and iOS developers 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
  6. 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 limit • Reuse one of the supported languages (Kotlin, Swift) Avoid introducing yet another language • Reuse existing skillsets, build tools, IDEs, libraries and tooling
  7. 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
  8. 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
  9. 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
  10. 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)
  11. 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
  12. 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
  13. 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)
  14. Kotlin Modules • Common: Kotlin code compiled to all platforms.

    Limited • iOS: Compiled to only iOS • Can use Objective C, Swift libs • Android: Compiled to only Android • Can use Android and Java libs
  15. 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 declaration of how to fulfill the request using the platform specific APIs
  16. 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
  17. 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)") }
  18. 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) } }
  19. App: Android View: Fragment Interactor: Business Logic Repository: Persistence (Sqlite,

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

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

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

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

    { takeFrom(“http://myserver") encodedPath = “/greeting/" } }
  24. 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()) } }
  25. 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`() { }
  26. 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/" } }
  27. Common Testing • Common unit tests are compiled and executed

    on each platform • Android tests execute on JVM, iOS tests use xrun • IDE support for unit testing is still incomplete • No mockk support yet, need to create your own Mocks, Stubs & Spys
  28. // view implemented native interface GreetingView { var isUpdating: Boolean

    fun onUpdate(data: Greeting) } MVP Contract: Kotlin // actions, presenter common interface GreetingActions { fun onRequestData() }
  29. Presenter: Kotlin } class GreetingPresenter( uiContext: CoroutineContext, private val useCase:

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

    launch(coroutineContext) { val result = useCase.getGreeting() view.onUpdate(result) }.invokeOnCompletion { view.isUpdating = false } } class GreetingPresenter( uiContext: CoroutineContext, private val useCase: GreetingUseCase, private val view: GreetingsView ) : CoroutinePresenter(uiContext),GreetingsActions {
  31. 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 }
  32. Sqlite Persistence: SqlDelight CREATE TABLE greeting ( name TEXT NOT

    NULL ); insertOrReplace: INSERT OR REPLACE INTO greeting(name) VALUES ?; select: SELECT * FROM greeting;
  33. 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
  34. ViewController: Swift override func viewDidLoad() { super.viewDidLoad() // Greeting is

    a Kotlin data class let greeting = Greeting( name: “Hello Droidcon" ) } import UIKit import lib class GreetingViewController: UIViewController { }
  35. Dependency Injection iOS let apiUrl:String = “https://myapi.com” let client:Ktor_client_coreHttpClient =

    DIKt.getHttpClient() let service: GreetingApi = DIKt.getService(client: client, apiUrl: apiUrl)
  36. Presenter: Swift override func viewDidLoad() { super.viewDidLoad() // obtain dependencies

    let view: GreetingsView = self let presenter = GreetingsPresenter( uiContext: uiContext, view: view, api: service) ) } } class GreetingController: UIViewController, GreetingsView {
  37. UI View Controller: Swift func loadData() { presenter.onRequestData() } override

    func onUpdate(response: Greeting) { updateUi(response) }
  38. Coroutine Context: Swift public class UI: Kotlinx_coroutines_coreCoroutineDispatcher { } override

    public func dispatch(context: KotlinCoroutineContext, block: Kotlinx_coroutines_coreRunnable) { } DispatchQueue.main.async { block.run() }
  39. Dependency Injection: Kotlin Dagger Module @Module class ServiceModule { }

    @Provides @Singleton fun providesHttp() = HttpClient() @Provides @Singleton fun providesGreetingApi(client: HttpClient, @Named("APIURL") apiUrl: String): GreetingApi { return GreetingService(client, apiUrl) } @Provides @Singleton fun providesActions(): GreetingActions = GreetingPresenter(…)
  40. Dependency Injection: Dagger Component @Component(modules = [ServiceModule::class]) { } @Singleton

    interface ContentFragmentComponent { fun inject(frag: GreetingFragment)
  41. 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() }
  42. 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 • Ktor client has open defect where cannot be created on non-main thread (fixed in 1.3.0, Github #1183) • Coroutine Worker (Ben Asher), Reaktive (Badoo)
  43. 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
  44. 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
  45. 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)
  46. 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
  47. 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)
  48. 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) }
  49. 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
  50. KN is the future, Is the future now? • Early

    adopter phase, its usable 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
  51. 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 an alternative to the Native SDKs • Flutter maximizes code sharing, KN maximizes native platform interop
  52. My Limited Experience w/ Flutter • Shared UI, 3rd language

    (Dart) • Integration with brownfield apps is not great (crashes, memory leaks, integration boilerplate, lifecycle issues) • Flutter excels for simple greenfield apps with common UI, small teams • Uncanny valley. Its very hard to provide a seamless experience in a brownfield app
  53. 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
  54. “Thanks for listening! I hope you learnt something. Feel free

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