Share
Embed
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
Transcript
79 slides
-
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 -
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 -
• 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 -
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 -
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 -
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 -
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 -
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 -
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) -
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 -
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 -
“So just some gradle magic and I can have iOS
app for free?“ -
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) -
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 -
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 -
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 -
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)") } -
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) } } -
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 -
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 -
val input = """ {"name": “Hello Droidcon”} """ data class
Greeting(val name: String) @Serializable val result: Greeting = Json.parse(Greeting.serializer(), input) -
Repository Http Ktor • Android engine provided by OkHttp+OkIO or
HttpUrlConnection • iOS engine provided by async NSURLSession -
val client = HttpClient { Json { serializer = KotlinxSerializer().apply
{ register(Greeting.serializer()) } } } -
suspend fun getGreeting(): Greeting { } return client.get { url
{ takeFrom(“http://myserver") encodedPath = “/greeting/" } } -
Repository Http Ktor Tests val mockResponse = """{"name": “Droidcon”}""" val
client = HttpClient(MockEngine) { } -
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()) } } -
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`() { } -
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/" } } -
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 -
// view implemented native interface GreetingView { var isUpdating: Boolean
fun onUpdate(data: Greeting) } MVP Contract: Kotlin // actions, presenter common interface GreetingActions { fun onRequestData() } -
Presenter: Kotlin } class GreetingPresenter( uiContext: CoroutineContext, private val useCase:
GreetingUseCase, private val view: GreetingsView ) : CoroutinePresenter(uiContext),GreetingsActions { -
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 { -
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 } -
Sqlite Persistence: SqlDelight CREATE TABLE greeting ( name TEXT NOT
NULL ); insertOrReplace: INSERT OR REPLACE INTO greeting(name) VALUES ?; select: SELECT * FROM greeting; -
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 -
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 { } -
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) -
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 { -
UI View Controller: Swift func loadData() { presenter.onRequestData() } override
func onUpdate(response: Greeting) { updateUi(response) } -
Coroutine Context: Swift public class UI: Kotlinx_coroutines_coreCoroutineDispatcher { } override
public func dispatch(context: KotlinCoroutineContext, block: Kotlinx_coroutines_coreRunnable) { } DispatchQueue.main.async { block.run() } -
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(…) -
Dependency Injection: Dagger Component @Component(modules = [ServiceModule::class]) { } @Singleton
interface ContentFragmentComponent { fun inject(frag: GreetingFragment) -
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() } -
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) -
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 -
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 -
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) -
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 -
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) -
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) } -
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 -
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 -
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 -
Flutter Can’t go as many places but *might get you
there faster -
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 -
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 -
“Thanks for listening! I hope you learnt something. Feel free
to contact me with any questions.” twitter @jeremyrempel http://jeremyrempel.com