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.

E0ac1256093c733a5d5a26085b90966f?s=128

Andy Dyer

May 22, 2019
Tweet

Transcript

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

  2. None
  3. Outline 3

  4. Outline 1. The problem 3

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

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

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

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

    architecture 4. Demo 5. Future plans & lessons learned 3
  9. Traditional App 4

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

  11. Meeting 6

  12. Meeting, ticket 7

  13. Meeting, ticket, code 8

  14. Meeting, ticket, code, test 9

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

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

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

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

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

    app release 14
  20. React Native + CodePush 15

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

  22. Home Screen 17

  23. Home Screen 18

  24. AppCraft 19

  25. Compass 20

  26. Furnace & Beetroot 21

  27. Apps Don't Care 22

  28. Apps Don't Care •Layout variants - phone vs. tablet, country,

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

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

    A/B tests •Localization •Tracking & analytics 22
  31. Golem & Lapis 23

  32. API Endpoints 24

  33. API Endpoints 1. Configuration 24

  34. API Endpoints 1. Configuration 2. Layout 24

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

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

    Data 24
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  49. Data Response { "component": { "component_id": "root-component", "items": [ {

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

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

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

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

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

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

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  56. Litho & Texture 27

  57. Litho: Component Example @LayoutSpec public object LithoComponentSpec { @OnCreateLayout fun

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

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

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

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

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

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

    onCreateLayout(c: ComponentContext): Component = Text.create(c) .text("Hello, MobiusConf!") .textSizeSp(40)) .build(); } 28
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  83. Basic Components 31

  84. Basic Components •Text 31

  85. Basic Components •Text •Button 31

  86. Basic Components •Text •Button •Image 31

  87. Basic Components (cont.) 32

  88. Basic Components (cont.) •Layout 32

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

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

  91. Specialized Components 33

  92. Specialized Components •Wish List button 33

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

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

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

    33
  96. Redux Architecture 34

  97. Redux/MVI Android Libraries 35

  98. Redux/MVI Android Libraries •RxRedux 35

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

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

  101. Rendering a Screen 36

  102. Rendering a Screen 1. Layout API request 36

  103. Rendering a Screen 1. Layout API request 2. Render layout

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

    with placeholders and/or defaults 3. Data API request 36
  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
  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
  107. Android Architecture 37

  108. Testing & CI 38

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

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

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

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

    API, etc. •Unit tests •Screenshot tests •End-to-end/integration tests 38
  113. TestCraft 39

  114. Demo

  115. None
  116. None
  117. Future Plans 43

  118. Future Plans •Initial production rollout 43

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

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

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

    created screens •Flutter and/or Jetpack Compose 43
  122. Lessons Learned 44

  123. Lessons Learned •The difference between a prototype and a production

    ready platform is immense. 44
  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
  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
  126. Lessons Learned (cont.) 45

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

    advantages. 45
  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
  129. Thank You! 46

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

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