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

Dynamic Code With Zipline

Jesse Wilson
September 01, 2022

Dynamic Code With Zipline

Video: [coming soon]

As products grow, teams tend to move business logic to the backend. Keeping clients dumb avoids duplication and allows behavior changes without app releases. But it comes with significant downsides: limited interactivity, difficult development, and inflexible APIs.

Zipline is a new library from Cash App that takes a new approach. Instead of moving logic to the backend, Zipline runs dynamic Kotlin/JS code in your Android and iOS apps. It lets you ship behavior changes without an app release!

This talk advises when to use dynamic code and how to adopt it in your apps. It also goes deep on Zipline internals:

- Interface bridging
- Coroutines & Flows
- Fast launches
- Debugging features

If your apps are getting dumber, or you’re using server-driven UI, don’t miss this talk for a great alternative.

Jesse Wilson

September 01, 2022
Tweet

More Decks by Jesse Wilson

Other Decks in Technology

Transcript

  1. @jakewha on
    @jessewilson
    https://github.com/cashapp/zipline/
    Dynamic Code
    with Zipline

    View Slide

  2. Dynamic Code

    View Slide

  3. SEPT
    2022
    DEC
    2022
    MAR
    2023
    JUNE
    2023
    SEPT
    2023

    View Slide

  4. CHANGE
    PRICINGe
    SEPT
    2022
    DEC
    2022
    MAR
    2023
    JUNE
    2023
    SEPT
    2023

    View Slide

  5. CHANGE
    PRICINGe
    BECOMEe
    AD-FUNDED
    SEPT
    2022
    DEC
    2022
    MAR
    2023
    JUNE
    2023
    SEPT
    2023

    View Slide

  6. CHANGE
    PRICINGe
    LAUNCHe
    ‘STORIES’
    PIVOT
    TO VIDEOe
    BECOMEe
    AD-FUNDED
    ACQUIRED
    BY GOOGLE
    NEW PRIVACY
    LAWe
    SEPT
    2022
    DEC
    2022
    MAR
    2023
    JUNE
    2023
    SEPT
    2023

    View Slide

  7. We Don’t Know The Future
    • So our apps prepare for many possibilities
    • Feature flags
    • Server-driven UI
    • Loose & Lenient APIs

    View Slide

  8. Backends are Stuck in the Past
    • Testing old client versions? So much work
    • Changing an API? So much work
    • Testing server-driven UI? So much work
    SEPT
    2021
    DEC
    2021
    MAR
    2022
    JUNE
    2022
    SEPT
    2022

    View Slide

  9. The Web Solved This in the ’90s
    Every user is always on the latest version
    It’s awesome for customers, engineers, velocity
    N

    View Slide

  10. Demo

    View Slide

  11. JS

    View Slide

  12. QuickJS
    • An embeddable JavaScript engine
    • 65,560 lines of C
    • 1.2 MiB native library in your APK
    • Authored by Fabrice Bellard and Charlie Gordon

    View Slide

  13. JAVASCRIPT SOURCE
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    IN:
    QuickJS

    View Slide

  14. JAVASCRIPT SOURCE
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    IN: OUT:
    QUICKJS BYTECODE
    QuickJS Compiler

    View Slide

  15. hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    IN: OUT:
    QUICKJS BYTECODE
    "Hello JavaScript"
    EVALUATION RESULT
    QuickJS Interpreter

    View Slide

  16. JAVASCRIPT SOURCE
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    IN: OUT:
    "Hello JavaScript"
    EVALUATION RESULT
    QuickJS

    View Slide

  17. JNI Bindings for QuickJS
    • JNI: Java Native Inte ace
    • Lets you use C and C++ libraries from the JVM or Android!

    View Slide

  18. https://speakerdeck.com/swankjesse/jni-hello-world

    View Slide

  19. @Test fun runJavaScript() {
    val quickJs = QuickJs.create()
    assertEquals(
    "Hello JavaScript",
    quickJs.evaluate("['Hello', 'JavaScript'].join(' ')"),
    )
    quickJs.close()
    }

    View Slide

  20. Kotlin/JS
    • Run Kotlin in your browser
    • Or any modern JavaScript engine, like QuickJS

    View Slide

  21. Kotlin/JS is Kotlin
    • Syntax: nullability, data classes, named & default parameters,
    extension functions, annotations, lambdas
    • Stdlib: strings, collections, transforms, sequences, ranges,
    regexes, firstNotNullOfOrNull
    • Tools: IntelliJ, Gradle
    • Libraries: Compose, Coroutines, Serialization, Okio

    View Slide

  22. Kotlin/JS is JS
    • Syntax: dynamic, js("...")
    • Stdlib: Console.log, JSON.stringify()
    • Tools: Webpack, Terser
    • Libraries: either NPM or Kotlin/Multiplatform

    View Slide

  23. package com.example
    @JsExport
    fun sayHello(): String {
    return listOf("Hello", "Kotlin/JS")
    .joinToString(separator = " ")
    }
    src / jsMain / kotlin / com / example / hello.kt

    View Slide

  24. @Test fun sayHello() {
    val quickJs = QuickJs.create()
    quickJs.loadJsModule("./kotlin-kotlin-stdlib-js-ir.js")
    quickJs.loadJsModule("./hello-kotlin-js.js")
    assertEquals(
    "Hello Kotlin/JS",
    quickJs.evaluate(
    "require('./hello-kotlin-js.js').com.example.sayHello()"
    )
    )
    quickJs.close()
    }
    src / jvmTest / kotlin / com / example / HelloTest.kt

    View Slide

  25. Inte ace Bridging

    View Slide

  26. Idea
    • Running Kotlin/JS in your app is neat, but clumsy
    • What if it worked like Retrofit?

    View Slide

  27. https://jakewharton.com/using-kotlin-js-and-native-on-android/

    View Slide

  28. API
    class Zipline {
    fun bind(name: String, instance: T)
    fun take(name: String): T
    ...
    companion object {
    fun create(...): Zipline
    }
    }

    View Slide

  29. interface WorldClockService {
    fun formatTime(location: Location): String
    }x
    src / commonMain / kotlin / com / example / WorldClockService.kt

    View Slide

  30. interface WorldClockService : ZiplineService {
    fun formatTime(location: Location): String
    }x
    src / commonMain / kotlin / com / example / WorldClockService.kt

    View Slide

  31. fun prepare(zipline: Zipline) {
    zipline.bind("clock", RealWorldClockService())
    }
    class RealWorldClockService : WorldClockService {
    override fun formatTime(location: Location): String {
    ...
    }
    }
    src / jsMain / kotlin / com / example / RealWorldClockService.kt

    View Slide

  32. src / androidMain / kotlin / com / example / ClockPresenter.kt
    class ClockPresenter(
    zipline: Zipline
    ) {
    val service = zipline.take("clock")
    fun displayTime(): String {
    return service.formatTime(Location(...))
    }
    }

    View Slide

  33. Implementation
    • Zipline has a Kotlin compiler plugin
    • It generates an adapter to implement bridging
    • It rewrites bind() and take() calls to pass that adapter
    • Outbound: implements the inte ace & encodes calls as JSON
    • Inbound: decodes JSON calls and calls the real implementation

    View Slide

  34. interface WorldClockService : ZiplineService {ab
    fun formatTime(location: Location): String
    }x
    src / commonMain / kotlin / com / example / WorldClockService.kt

    View Slide

  35. companion object {
    internal object Adapter
    : ZiplineServiceAdapter() {yyy
    interface WorldClockService : ZiplineService {
    fun formatTime(location: Location): String
    src / commonMain / kotlin / com / example / WorldClockService.kt

    View Slide

  36. companion object {
    internal object Adapter
    : ZiplineServiceAdapter() {yyy
    interface WorldClockService : ZiplineService {
    fun formatTime(location: Location): String
    src / commonMain / kotlin / com / example / WorldClockService.kt
    override fun outboundService(handler: Handler) =
    object : WorldClockService {
    override fun formatTime(location: Location) =
    handler.call(this, functionIndex = 0, location)
    as String
    }

    View Slide

  37. companion object {
    internal object Adapter
    : ZiplineServiceAdapter() {yyy
    interface WorldClockService : ZiplineService {
    fun formatTime(location: Location): String
    src / commonMain / kotlin / com / example / WorldClockService.kt
    override val functions = listOf(
    object : ZiplineFunction(
    "fun formatTime(location: Location): String",
    ) {
    override fun call(
    service: WorldClockService, args: List<*>
    ) = service.formatTime(args[0] as Location)
    },

    View Slide

  38. Kotlin Serialization
    • Bridged calls use JSON as an intermediate representation
    • And kotlinx.serialization to get that JSON

    View Slide

  39. Fast-enough JSON
    fun Json.encodeToStringFast(
    serializer: KSerializer,
    value: T,
    ): String {
    val jsValue = encodeToDynamic(serializer, value)
    return JSON.stringify(jsValue)
    }

    View Slide

  40. Suspension Bridges
    interface NetworkTimeService : ZiplineService {
    }
    suspend fun now( :
    ) Instant

    View Slide

  41. Suspension Bridges
    interface NetworkTimeService : ZiplineService {
    }
    fun now( )
    Instant
    callback: SuspendCallback< >

    View Slide

  42. Pass-by-Reference
    • How do we encode a SuspendCallback as JSON?
    • x

    View Slide

  43. Pass-by-Reference
    • How do we encode a SuspendCallback as JSON?
    • By building another bridge!

    View Slide

  44. Example
    • To encode:
    1. Generate an ID
    2. Call bind(id, service)
    3. Encode the ID
    • To decode:
    1. Decode the ID
    2. Call take(id)
    3. Return the service

    View Slide

  45. Flows
    interface WorldClockService : ZiplineService {
    fun formatNow(): Flow
    }

    View Slide

  46. Flows Pass-By-Reference
    • Returning a flow? Pass-by-reference of Flow
    • Collecting a flow? Pass-by-reference back of FlowCollector
    • Emitting to a collector? Uses the suspending emit() function,
    which is implemented as pass-by-reference of SuspendCallback

    View Slide

  47. Bridging Is Powe ul...
    • Call Kotlin/JS mostly like a regular function call
    • Offer data & services to Kotlin JS using the same mechanism
    • Capable concurrency
    • You never see the JavaScript
    • You never see the JSON

    View Slide

  48. ...But There’s Gotchas!
    • You need to close() everything you take()
    • Exceptions are wrapped in a ZiplineException
    • Code versions might be different!
    • Be mindful of parameter & response data size

    View Slide

  49. Packaging

    View Slide

  50. @Test fun sayHello() {
    val quickJs = QuickJs.create()
    quickJs.loadJsModule("./kotlin-kotlin-stdlib-js-ir.js")
    quickJs.loadJsModule("./hello-kotlin-js.js")
    assertEquals(
    "Hello Kotlin/JS",
    quickJs.evaluate(
    "require('./hello-kotlin-js.js').com.example.sayHello()"
    )
    )
    quickJs.close()
    }
    src / jvmTest / kotlin / com / example / HelloTest.kt

    View Slide

  51. @Test fun sayHello() {
    val quickJs = QuickJs.create()
    quickJs.loadJsModule("./kotlin-kotlin-stdlib-js-ir.js")
    quickJs.loadJsModule("./hello-kotlin-js.js")
    assertEquals(
    "Hello Kotlin/JS",
    quickJs.evaluate(
    "require('./hello-kotlin-js.js').com.example.sayHello()"
    )
    )
    quickJs.close()
    }
    src / jvmTest / kotlin / com / example / HelloTest.kt
    YUCK

    View Slide

  52. Kotlin/JS
    Source Code
    Running in
    QuickJS
    ? ?
    ?

    View Slide

  53. Kotlin/JS
    Source Code
    Running in
    QuickJS
    .js files
    build
    Kotlin/JS
    Gradle
    Plugin

    View Slide

  54. Kotlin/JS
    Source Code
    Running in
    QuickJS
    .js files .js files
    CDN
    build
    Kotlin/JS
    Gradle
    Plugin
    Your
    Upload
    Task

    View Slide

  55. Kotlin/JS
    Source Code
    Running in
    QuickJS
    .js files .js files
    CDN
    build
    Kotlin/JS
    Gradle
    Plugin
    Your
    Upload
    Task
    Zipline
    Loader

    View Slide

  56. .js files
    build

    View Slide

  57. compileSync
    main
    productionExecutable
    kotlin
    atomicfu.js
    kotlin-stdlib.js
    kotlinx-serialization.js
    kotlinx-coroutines-core.js
    hello-kotlin-js.js
    zipline.js
    build

    View Slide

  58. build / compileSync / main / productionExecutable / kotlin / manifest.zipline.json
    {
    "modules": [
    "atomicfu.js",
    "kotlin-stdlib.js",
    "kotlinx-serialization.js",
    "kotlinx-coroutines-core.js",
    "hello-kotlin-js.js",
    "zipline.js"
    ]
    }

    View Slide

  59. Kotlin/JS
    Source Code
    Running in
    QuickJS
    .js files
    .js files +
    manifest
    CDN
    build
    Kotlin/JS
    Gradle
    Plugin
    Your
    Upload
    Task
    Zipline
    Loader
    .js files +
    manifest
    build
    Zipline
    Gradle
    Plugin

    View Slide

  60. Remember how
    QuickJS Works?

    View Slide

  61. SOURCE
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    BYTECODE
    "Hello JavaScript"
    RESULT
    QuickJS
    Compiler
    QuickJS
    Interpreter

    View Slide

  62. .js source
    + manifest
    .js source
    + manifest
    Kotlin/JS
    Source Code
    Running in
    QuickJS
    .js files
    CDN
    build
    Kotlin/JS
    Gradle
    Plugin
    Your
    Upload
    Task
    Zipline
    Loader
    build
    Zipline
    Gradle
    Plugin

    View Slide

  63. bytecode
    + manifest
    bytecode
    + manifest
    Kotlin/JS
    Source Code
    Running in
    QuickJS
    .js files
    CDN
    build
    Kotlin/JS
    Gradle
    Plugin
    Your
    Upload
    Task
    Zipline
    Loader
    build
    Zipline
    Gradle
    Plugin

    View Slide

  64. ZiplineLoader
    • Takes the URL of a manifest
    • Returns a running Zipline (QuickJS + bridges)

    View Slide

  65. Loading Strategy
    1. Fetch a manifest
    2. Fetch all the bytecode, in parallel
    3. Load bytecode into QuickJS as it arrives

    View Slide

  66. Caching
    • We don’t update our kotlin stdlib every day
    • Only download it when it changes!
    • Same for all modules

    View Slide

  67. build / compileSync / main / productionExecutable / kotlin / manifest.zipline.json
    {
    "modules": {
    "atomicfu.js": {},
    "kotlin-stdlib.js": {},
    "kotlinx-serialization.js": {},
    "kotlinx-coroutines-core.js": {},
    "hello-kotlin-js.js": {},
    "zipline.js": {}
    }
    }

    View Slide

  68. build / compileSync / main / productionExecutable / kotlin / manifest.zipline.json
    {
    "modules": {
    "atomicfu.js": {
    "sha256": "5eb408f3363d431344133ef5"
    },
    "kotlin-stdlib.js": {
    "sha256": "f3363d431344133ef55eb408"
    },
    "kotlinx-serialization.js": {
    "sha256": "4133ef55eb408f3363d43134"
    },
    "kotlinx-coroutines-core.js": {
    "sha256": "d431344133ef55eb408f3363"
    },

    View Slide

  69. Embedded Code
    • Embed the latest bytecode + manifest in the APK
    • Minimize downloads on most launches
    • Something to fall back to when running offline

    View Slide

  70. Flows of Code
    val ziplineFlow: Flow = ziplineLoader.load(
    applicationName = "world-clock",
    manifestUrlFlow = repeatFlow(manifestUrl, 500L),
    )

    View Slide

  71. Always Fresh Code
    • In Development:
    • Load on save
    • In Production:
    • Load cached/embedded code if launched offline
    • Update to fresh code once connected

    View Slide

  72. Zipline On iOS

    View Slide

  73. Multiplatformization
    • Zipline is a regular Kotlin/Multiplatform library
    • QuickJS runs natively on iOS, no JNI required!

    View Slide

  74. Android App
    Multiplatform Libraries
    Downloadable Code

    View Slide

  75. Downloadable Code
    iOS App
    Multiplatform Libraries

    View Slide

  76. httpClient: OkHttpClient,
    fun ZiplineLoader(
    dispatcher: CoroutineDispatcher,
    manifestVerifier: ManifestVerifier,
    eventListener: EventListener = EventListener.NONE,
    nowEpochMs: () -> Long = systemEpochMsClock,
    ): ZiplineLoader

    View Slide

  77. urlSession: NSURLSession,
    fun ZiplineLoader(
    dispatcher: CoroutineDispatcher,
    manifestVerifier: ManifestVerifier,
    eventListener: EventListener = EventListener.NONE,
    nowEpochMs: () -> Long = systemEpochMsClock,
    ): ZiplineLoader

    View Slide

  78. context: Context,
    fileSystem: FileSystem,
    directory: Path,
    maxSizeInBytes: Long,
    ): ZiplineCache
    fun ZiplineCache(

    View Slide

  79. fileSystem: FileSystem,
    directory: Path,
    maxSizeInBytes: Long,
    ): ZiplineCache
    fun ZiplineCache(

    View Slide

  80. Using Zipline + MPP
    • Shared inte aces and types in commonMain
    • Implementations in hostMain, or androidMain + iosMain

    View Slide

  81. COOL THINGS
    5

    View Slide

  82. java.lang.Exception: unexpected timezone: America/Gotham
    at captureStack (runtime/coreRuntime.kt:86)
    at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js)
    at (app.cash.worldclock/TimeFormatter.kt:98)
    at (app.cash.worldclock/WorldClockJs.kt:615)
    at (InboundService.kt:837)
    at (zipline-root-zipline.js)
    at app.cash.zipline.OutboundService.runJob(platform.kt:37)
    at app.cash.zipline.EventLoop.run(CoroutineEventLoop.kt:57)
    at j.u.concurrent.Executor.runWorker(ThreadPoolExecutor.java:1167)
    at j.u.concurrent.Executor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:920)
    Kotlin Line Numbers
    #1

    View Slide

  83. java.lang.Exception: unexpected timezone: America/Gotham
    at captureStack (runtime/coreRuntime.kt:86)
    at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js)
    at (app.cash.worldclock/TimeFormatter.kt:98)
    at (app.cash.worldclock/WorldClockJs.kt:615)
    at (InboundService.kt:837)
    at (zipline-root-zipline.js)
    at app.cash.zipline.OutboundService.runJob(platform.kt:37)
    at app.cash.zipline.EventLoop.run(CoroutineEventLoop.kt:57)
    at j.u.concurrent.Executor.runWorker(ThreadPoolExecutor.java:1167)
    at j.u.concurrent.Executor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:920)
    Kotlin Line Numbers
    #1

    View Slide

  84. Kotlin Line Numbers
    #1
    bytecode
    files
    .kt files .js files
    Kotlin/JS
    Gradle
    Plugin
    Zipline
    Gradle
    Plugin

    View Slide

  85. Kotlin Line Numbers
    #1
    bytecode
    files
    .kt files
    .js files
    .js.map files
    Kotlin/JS
    Gradle
    Plugin
    Zipline
    Gradle
    Plugin

    View Slide

  86. Kotlin Line Numbers
    #1
    bytecode
    files
    .kt files
    .js files
    .js.map files
    Kotlin/JS
    Gradle
    Plugin
    Zipline
    Gradle
    Plugin

    View Slide

  87. val hprofFile = File("fibonacci.hprof")
    zipline.quickJs.startCpuSampling(hprofFile).use {
    // ...use Zipline...
    }
    Sampling Profiler
    #2

    View Slide

  88. Sampling Profiler
    #2

    View Slide

  89. java.lang.Exception: helloService created here, was not closed
    at EventListener.takeService(LoggingEventListener.kt:36)
    at Endpoint.take(Endpoint.kt:127)
    at Zipline.take(Zipline.kt:132)
    at ServiceLeakingTest.test(ServiceLeakingTest.kt:67)
    Not closed Detection
    #3

    View Slide

  90. Code Signing
    #4
    zipline {
    ...
    signingKeys {
    create("key1") {
    privateKeyHex.set("ae4737d95df505eac2424000559d0")
    algorithmId.set(Ed25519)
    }
    create("key2") {
    privateKeyHex.set("6207b6f19c9d7dfa8af31ed5d9789")
    algorithmId.set(EcdsaP256)
    }
    }
    }

    View Slide

  91. "modules": {
    "atomicfu.js": {
    "sha256": "5eb408f3363d431344133ef5"
    },
    "kotlin-stdlib.js": {
    "sha256": "f3363d431344133ef55eb408"
    },
    "kotlinx-serialization.js": {
    "sha256": "4133ef55eb408f3363d43134"
    },
    "kotlinx-coroutines-core.js": {
    "sha256": "d431344133ef55eb408f3363"
    },
    "hello-kotlin-js.js": {
    "sha256": "5eb408f3363d431344133ef5"
    Code Signing
    #4
    {

    View Slide

  92. "unsigned": {
    "signatures": {
    "key1": "0f91508b8451a8ed4eedf723f22613fe",
    "key2": "ff105ef4a5bc691614c3b8b472808e2b"
    }
    },
    "modules": {
    "atomicfu.js": {
    "sha256": "5eb408f3363d431344133ef5"
    },
    "kotlin-stdlib.js": {
    "sha256": "f3363d431344133ef55eb408"
    },
    "kotlinx-serialization.js": {
    "sha256": "4133ef55eb408f3363d43134"
    Code Signing
    #4
    {

    View Slide

  93. Code Signing
    #4
    val verifier = ManifestVerifier.Builder()
    .addEd25519("key1", "6207b6f19c9d7dfa8af3d5d".decodeHex())
    .addEcdsaP256("key2", "3041020100301306072a88c".decodeHex())
    .build()
    val ziplineLoader = ZiplineLoader(
    manifestVerifier = verifier,
    dispatcher = ...,
    httpClient = ...,
    )

    View Slide

  94. No Eval
    #5
    fun getMessage(): String {
    return eval(
    "['Hello', 'JavaScript'].join(' ')"
    ) as String
    }

    View Slide

  95. No Eval
    #5
    fun getMessage(): String {
    return eval(
    "['Hello', 'JavaScript'].join(' ')"
    ) as String
    }
    app.cash.zipline.QuickJsException: eval is not supported
    at JavaScript.(test.kt)
    at app.cash.zipline.QuickJs.execute(Native Method)
    at app.cash.zipline.QuickJs.execute(QuickJs.kt:147)
    at app.cash.zipline.QuickJs.evaluate(QuickJs.kt:111)
    at app.cash.zipline.Test.getMessage(Test.kt:123)

    View Slide

  96. COOL THINGS
    5
    Kotlin Line Numbers
    Sampling Profiler
    Not Closed Detection
    1.
    2.
    3.
    4.
    5.
    Code Signing
    No Eval

    View Slide

  97. Concerns

    View Slide

  98. Bigger Alternatives Exist
    JS ecosystem
    Works best when you go all-in
    JS ecosystem
    Strada is a rad idea
    Flutter ecosystem
    Go all-in
    React Native
    Web Views
    Flutter

    View Slide

  99. No Debugging
    • Zipline doesn’t (yet) have a step-debugger
    • You have to build for Kotlin/JVM and debug there

    View Slide

  100. Rules are Rules
    • Zipline lets you change behavior of already-released apps
    • Follow Apple’s and Google’s rules

    View Slide

  101. Powered by JavaScript?
    • But you never see the JavaScript
    • We’re gonna experiment with a Kotlin/WASM backend

    View Slide

  102. Next Steps

    View Slide

  103. Is This You?
    • Too much speculative code?
    • Too many feature flags?
    • Slow feedback loops?
    • Backwards-compatibility hell?

    View Slide

  104. Zipline Status
    • We’re close to shipping 1.0
    • In Cash App this fall!

    View Slide

  105. Thanks
    @jakewha on
    @jessewilson
    https://github.com/cashapp/zipline/

    View Slide