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. 2.
  2. 8.

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

    architecture 4. Demo 5. Future plans & lessons learned 3
  3. 11.
  4. 30.

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

    A/B tests •Localization •Tracking & analytics 22
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 49.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  18. 50.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  19. 51.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  20. 52.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  21. 53.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  22. 54.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  23. 55.

    Data Response { "component": { "component_id": "root-component", "items": [ {

    "component_id": "hello-text", "options": { "text": "Hello, MobiusConf!" } } ] } } 26
  24. 57.

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

    onCreateLayout(c: ComponentContext): Component = Text.create(c) .text("Hello, MobiusConf!") .textSizeSp(40)) .build(); } 28
  25. 58.

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

    onCreateLayout(c: ComponentContext): Component = Text.create(c) .text("Hello, MobiusConf!") .textSizeSp(40)) .build(); } 28
  26. 59.

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

    onCreateLayout(c: ComponentContext): Component = Text.create(c) .text("Hello, MobiusConf!") .textSizeSp(40)) .build(); } 28
  27. 60.

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

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

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

    onCreateLayout(c: ComponentContext): Component = Text.create(c) .text("Hello, MobiusConf!") .textSizeSp(40)) .build(); } 28
  30. 63.

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

    onCreateLayout(c: ComponentContext): Component = Text.create(c) .text("Hello, MobiusConf!") .textSizeSp(40)) .build(); } 28
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 103.

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

    with placeholders and/or defaults 36
  51. 104.

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

    with placeholders and/or defaults 3. Data API request 36
  52. 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
  53. 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
  54. 111.

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

    API, etc. •Unit tests •Screenshot tests 38
  55. 112.

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

    API, etc. •Unit tests •Screenshot tests •End-to-end/integration tests 38
  56. 114.
  57. 115.
  58. 116.
  59. 121.
  60. 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
  61. 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
  62. 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