$30 off During Our Annual Pro Sale. View Details »

AppCraft: Faster Than a Speeding Release Train

AppCraft: Faster Than a Speeding Release Train

At Zalando, we have a fairly small engineering team behind our flagship mobile app, with a constant stream of great ideas coming from product owners. A three-week release train isn't always fast enough to deploy changes such as UI tweaks, new A/B tests, and partner campaigns. This limitation is exacerbated by the fact that many users don't install app updates immediately.

We first looked at React Native & CodePush as a way to deploy app updates more quickly. Spoiler alert: integrating React Native with an existing app is hard and you can't just wave a magic wand to turn any JavaScript engineer into a mobile developer.

Since last summer, we've been building AppCraft, the internal name for our server-driven UI platform. AppCraft rethinks the traditional mobile-client-meets-REST-API architecture by moving most of the malleable logic to the server, reducing the client to what is essentially a domain-specific browser. This enables rapid iteration on app features by non-technical business people; freeing up our mobile engineering resources for more strategic (and let's face it — more interesting) tasks.

How does this even work? What did we learn along the way? In this talk, Andy will tell you and show you.

Andy Dyer

May 22, 2019
Tweet

More Decks by Andy Dyer

Other Decks in Technology

Transcript

  1. AppCraft
    Faster than a speeding release train
    #MobiusConf @dammitandy

    View Slide

  2. View Slide

  3. Outline
    3

    View Slide

  4. Outline
    1. The problem
    3

    View Slide

  5. Outline
    1. The problem
    2. Our solution
    3

    View Slide

  6. Outline
    1. The problem
    2. Our solution
    3. Mobile client architecture
    3

    View Slide

  7. Outline
    1. The problem
    2. Our solution
    3. Mobile client architecture
    4. Demo
    3

    View Slide

  8. Outline
    1. The problem
    2. Our solution
    3. Mobile client architecture
    4. Demo
    5. Future plans & lessons learned
    3

    View Slide

  9. Traditional App
    4

    View Slide

  10. What is the lifecycle
    of an app change?
    5

    View Slide

  11. Meeting
    6

    View Slide

  12. Meeting,
    ticket
    7

    View Slide

  13. Meeting,
    ticket, code
    8

    View Slide

  14. Meeting,
    ticket,
    code, test
    9

    View Slide

  15. Meeting,
    ticket, code,
    test, pull request
    10

    View Slide

  16. Meeting
    ticket, code, test,
    pull request,
    discussion
    11

    View Slide

  17. Meeting, ticket,
    code, test, pull
    request, discussion,
    merge PR
    12

    View Slide

  18. Meeting, ticket,
    code, test, pull
    request, discussion,
    merge PR,
    regression/QA
    13

    View Slide

  19. Meeting, ticket,
    code, test, pull
    request, discussion,
    merge PR, regression/QA,
    app release
    14

    View Slide

  20. React Native + CodePush
    15

    View Slide

  21. Cross-platform
    integration with
    existing apps is
    hard.
    16

    View Slide

  22. Home Screen
    17

    View Slide

  23. Home Screen
    18

    View Slide

  24. AppCraft
    19

    View Slide

  25. Compass
    20

    View Slide

  26. Furnace & Beetroot
    21

    View Slide

  27. Apps Don't Care
    22

    View Slide

  28. Apps Don't Care
    •Layout variants - phone vs. tablet, country, A/B tests
    22

    View Slide

  29. Apps Don't Care
    •Layout variants - phone vs. tablet, country, A/B tests
    •Localization
    22

    View Slide

  30. Apps Don't Care
    •Layout variants - phone vs. tablet, country, A/B tests
    •Localization
    •Tracking & analytics
    22

    View Slide

  31. Golem & Lapis
    23

    View Slide

  32. API Endpoints
    24

    View Slide

  33. API Endpoints
    1. Configuration
    24

    View Slide

  34. API Endpoints
    1. Configuration
    2. Layout
    24

    View Slide

  35. API Endpoints
    1. Configuration
    2. Layout
    3. Data
    24

    View Slide

  36. API Endpoints
    1. Configuration
    2. Layout
    3. Data
    4. Component Data
    24

    View Slide

  37. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  38. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  39. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  40. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  41. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  42. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  43. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  44. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  45. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  46. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  47. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  48. Layout Response
    {
    "screen_id": "example-screen",
    "component": {
    "component_id": "root-component",
    "type": "layout",
    "children": [
    {
    "component_id": "hello-text",
    "type": "text",
    "options": {
    "text": "Placeholder text"
    },
    "style": { ... },
    "events: [ ... ]
    }
    ]
    }
    }
    25

    View Slide

  49. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  50. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  51. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  52. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  53. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  54. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  55. Data Response
    {
    "component": {
    "component_id": "root-component",
    "items": [
    {
    "component_id": "hello-text",
    "options": {
    "text": "Hello, MobiusConf!"
    }
    }
    ]
    }
    }
    26

    View Slide

  56. Litho & Texture
    27

    View Slide

  57. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  58. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  59. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  60. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  61. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  62. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  63. Litho: Component Example
    @LayoutSpec
    public object LithoComponentSpec {
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext): Component =
    Text.create(c)
    .text("Hello, MobiusConf!")
    .textSizeSp(40))
    .build();
    }
    28

    View Slide

  64. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  65. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  66. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  67. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  68. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  69. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  70. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  71. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  72. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  73. Litho: Wrapping a Native View
    @MountSpec
    public object ColorComponentSpec {
    @OnCreateMountContent
    fun onCreateMountContent(Context c): ColorDrawable = ColorDrawable();
    @OnMount
    fun onMount(
    context: ComponentContext,
    colorDrawable: ColorDrawable,
    @Prop colorName: String) {
    colorDrawable.color = Color.parseColor(colorName)
    }
    }
    29

    View Slide

  74. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  75. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  76. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  77. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  78. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  79. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  80. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  81. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  82. Using a Litho Component
    fun createView(context: Context): View {
    val c = ComponentContext(context)
    val component =
    LithoComponent.create(c)
    .background(
    ColorComponent.create(c).colorName("blue")
    )
    .build()
    return LithoView.create(c, component))
    }
    30

    View Slide

  83. Basic Components
    31

    View Slide

  84. Basic Components
    •Text
    31

    View Slide

  85. Basic Components
    •Text
    •Button
    31

    View Slide

  86. Basic Components
    •Text
    •Button
    •Image
    31

    View Slide

  87. Basic Components (cont.)
    32

    View Slide

  88. Basic Components (cont.)
    •Layout
    32

    View Slide

  89. Basic Components (cont.)
    •Layout
    •Repeater
    32

    View Slide

  90. Basic Components (cont.)
    •Layout
    •Repeater
    •Parallax Layout
    32

    View Slide

  91. Specialized Components
    33

    View Slide

  92. Specialized Components
    •Wish List button
    33

    View Slide

  93. Specialized Components
    •Wish List button
    •Cart button
    33

    View Slide

  94. Specialized Components
    •Wish List button
    •Cart button
    •Follow button
    33

    View Slide

  95. Specialized Components
    •Wish List button
    •Cart button
    •Follow button
    •Price
    33

    View Slide

  96. Redux Architecture
    34

    View Slide

  97. Redux/MVI Android Libraries
    35

    View Slide

  98. Redux/MVI Android Libraries
    •RxRedux
    35

    View Slide

  99. Redux/MVI Android Libraries
    •RxRedux
    •Mobius
    35

    View Slide

  100. Redux/MVI Android Libraries
    •RxRedux
    •Mobius
    •MvRx
    35

    View Slide

  101. Rendering a Screen
    36

    View Slide

  102. Rendering a Screen
    1. Layout API request
    36

    View Slide

  103. Rendering a Screen
    1. Layout API request
    2. Render layout with placeholders and/or defaults
    36

    View Slide

  104. Rendering a Screen
    1. Layout API request
    2. Render layout with placeholders and/or defaults
    3. Data API request
    36

    View Slide

  105. Rendering a Screen
    1. Layout API request
    2. Render layout with placeholders and/or defaults
    3. Data API request
    4. Rerender layout bound with data and events
    36

    View Slide

  106. Rendering a Screen
    1. Layout API request
    2. Render layout with placeholders and/or defaults
    3. Data API request
    4. Rerender layout bound with data and events
    5. Additional data requests, if necessary
    36

    View Slide

  107. Android Architecture
    37

    View Slide

  108. Testing & CI
    38

    View Slide

  109. Testing & CI
    •Static analysis - code styles, lint, public API, etc.
    38

    View Slide

  110. Testing & CI
    •Static analysis - code styles, lint, public API, etc.
    •Unit tests
    38

    View Slide

  111. Testing & CI
    •Static analysis - code styles, lint, public API, etc.
    •Unit tests
    •Screenshot tests
    38

    View Slide

  112. Testing & CI
    •Static analysis - code styles, lint, public API, etc.
    •Unit tests
    •Screenshot tests
    •End-to-end/integration tests
    38

    View Slide

  113. TestCraft
    39

    View Slide

  114. Demo

    View Slide

  115. View Slide

  116. View Slide

  117. Future Plans
    43

    View Slide

  118. Future Plans
    •Initial production rollout
    43

    View Slide

  119. Future Plans
    •Initial production rollout
    •Fully dynamic home screen
    43

    View Slide

  120. Future Plans
    •Initial production rollout
    •Fully dynamic home screen
    •Algorithmically created screens
    43

    View Slide

  121. Future Plans
    •Initial production rollout
    •Fully dynamic home screen
    •Algorithmically created screens
    •Flutter and/or Jetpack Compose
    43

    View Slide

  122. Lessons Learned
    44

    View Slide

  123. Lessons Learned
    •The difference between a prototype and a production ready platform
    is immense.
    44

    View Slide

  124. Lessons Learned
    •The difference between a prototype and a production ready platform
    is immense.
    •Don’t cut corners. Paint both sides of the fence. YPGNI.
    44

    View Slide

  125. Lessons Learned
    •The difference between a prototype and a production ready platform
    is immense.
    •Don’t cut corners. Paint both sides of the fence. YPGNI.
    •If something seems obvious but no one else is doing it, it’s either
    genius or foolish...maybe both.
    44

    View Slide

  126. Lessons Learned (cont.)
    45

    View Slide

  127. Lessons Learned (cont.)
    •A Redux/MVI architecture has a lot of advantages.
    45

    View Slide

  128. Lessons Learned (cont.)
    •A Redux/MVI architecture has a lot of advantages.
    •Pair programming really is as great as people say it is.
    45

    View Slide

  129. Thank You!
    46

    View Slide

  130. Thank You!
    •Slides: bit.ly/2YCrQFb
    46

    View Slide

  131. Thank You!
    •Slides: bit.ly/2YCrQFb
    •We're hiring! jobs.zalando.com
    46

    View Slide