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

A page out of Server driven UI on Android

A page out of Server driven UI on Android

In this talk we dive into few example's of server-driven UI (SDUI), it's important to understand the general idea of SDUI and how it provides an advantage over traditional client-driven UI and why it's so important and current hot-topic.
We take a look at how JetPack Compose can be used to build SDUI , we also look at some tips and tricks to navigate the code from start to finish.

Adit Lal
PRO

June 26, 2021
Tweet

More Decks by Adit Lal

Other Decks in Programming

Transcript

  1. Google Developers 🎯@aditlal 🔗aditlal.dev Lucknow A page out of Server

    driven UI on Android Adit Lal 
 Individual Consultant
  2. Server Driven UI - What to Except Control Core UI

    Components on the fl y for either all the users or one can chose it for some users Plays well into A/B testing Baked with Feature fl ags Do it once reap bene fi ts over and over again.
  3. UI should be a breeze to change Code should be

    dynamic Building new UI should be fast Learn, adopt , launch quickly Goal
  4. Whats the problem?

  5. 5 Service / API Client Service / API Service /

    API Client Client Whats the problem?
  6. Heavy client speci fi c UI logic Maintaining more than

    one client can lead to duplicate code Delivery gets hard with scale Building UI components from scratch can add to the timelines Logic - Downsides?
  7. Write code to render a State Handle events from these

    State changes Logic - Changes?
  8. Execution Easy to understand Flexible for designers Launch without a

    need to open Play Console DRY - minimise repetition Easy to maintain
  9. State Rendering 
 - View model 
 - Engine Components

    Source: Spotify Execution
  10. Rendering 
 - View model 
 - Engine Compo

  11. Components

  12. • UI = fun(state) Source: Spotify • state = json

    • json = backend • backend
  13. • UI = fun(state) Source: Spotify • state = json

    • json = backend • backend
  14. UI = fun(backend) Source: Spotify • UI = fun(state) backend

  15. Solution Response UI Component Data View 
 Component Container

  16. Components driven UI

  17. Server response - Checkbox

  18. Server response - Checkbox { "isEnabled":true, "isSelected":false, "text":"I\u0027m a checkbox

    : P", "viewType":"checkbox" }
  19. Server response - Checkbox { "isEnabled":true, "isSelected":false, "text":"I\u0027m a checkbox

    : P", "viewType":"checkbox" }
  20. Screen

  21. Server Components - Card { "id":"7b0dae3729f54340bccf", "children" : [ /

    / sub components ], "viewType":"viewgroup", "type":"card" }
  22. Server Components - Card { "id":"7b0dae3729f54340bccf", "children" : [ /

    / sub components ], "viewType":"viewgroup", "type":"card" }
  23. Server Components - Card { "children" : [ { "viewType":"circular_image",

    "imageUrl":"https: / / source.unsplash.com/random/widthxheight" }, { "children" : [ { "viewType":"text", "text":"Adit Lal" }, { "viewType":"text", "text":"Engineer" } ], "viewType":"viewgroup", "type":"linear" } ], "viewType":"viewgroup", "type":"card" }
  24. { "children" : [ { "viewType":"circular_image", "imageUrl":"https: / / source.unsplash.com/random/widthxheight"

    }, { "children" : [ { "viewType":"text", "text":"Adit Lal" }, { "viewType":"text", "text":"Engineer" } ], "viewType":"viewgroup", "type":"linear" } ], "viewType":"viewgroup", "type":"card" } Server Components - Card
  25. Server Components - Card { "children" : [ { "viewType":"circular_image",

    "imageUrl":"https: / / source.unsplash.com/random/widthxheight" }, { "children" : [ { "viewType":"text", "text":"Adit Lal" }, { "viewType":"text", "text":"Engineer" } ], "viewType":"viewgroup", "type":"linear" } ], "viewType":"viewgroup", "type":"card" }
  26. { "id":"82aae191a52a6610c5cd878a600d5cabbc9101be", "children" : [ { "viewType":"icon", "meta" : {

    "iconColor" : 0, "icon":"11" }, "id":"2ab2541e1e15d0638a504fcf7850cde8180f8140", "marginsHorizontal" : 16 }, { "viewType":"text", "id":"77d3095a51466f0ba0f6cae2e2f93093813f9125", "text":"Hey, Adit! I'm your Support Assistant.\nAsk me anything.", "marginsHorizontal" : 16 } ], "viewType":"viewgroup", "type":"card" } Server Components - Card #2
  27. { "id":"82aae191a52a6610c5cd878a600d5cabbc9101be", "children" : [ { "viewType":"icon", "meta" : {

    "iconColor" : 0, "icon":"11" }, "id":"2ab2541e1e15d0638a504fcf7850cde8180f8140", "marginsHorizontal" : 16 }, { "viewType":"text", "id":"77d3095a51466f0ba0f6cae2e2f93093813f9125", "text":"Hey, Adit! I'm your Support Assistant.\nAsk me anything.", "marginsHorizontal" : 16 } ], "viewType":"viewgroup", "type":"card" } Server Components - Card #2
  28. Server Components - Card #2 { "id":"82aae191a52a6610c5cd878a600d5cabbc9101be", "children" : [

    { "viewType":"icon", "meta" : { "iconColor" : 0, "icon":"11" }, "id":"2ab2541e1e15d0638a504fcf7850cde8180f8140", "marginsHorizontal" : 16 }, { "viewType":"text", "id":"77d3095a51466f0ba0f6cae2e2f93093813f9125", "text":"Hey, Adit! I'm your Support Assistant.\nAsk me anything.", "marginsHorizontal" : 16 } ], "viewType":"viewgroup", "type":"card" }
  29. Epoxy 
 from Airbnb 
 Litho 
 from Facebook 


    Proteus 
 from Flipkart 
 Graywater 
 from Tumblr 
 Groupie Jetpack Compose Frameworks
  30. Jetpack Compose

  31. Source : https://www.harivignesh.dev/android:-con fi guration-driven-ui-from-epoxy-to-compose LazyColumn Jetpack Compose

  32. Jetpack Compose LazyColumn

  33. Jetpack Compose LazyColumn

  34. { "type": "product_deal_section", "content": { "title": "Health Supplements", "subtitle": "Amazing

    Deals for your daily", "button": { "title": "See all", "deepLink": “app: / / see_all/categories” } }, "cards": [ ] } Jetpack Compose
  35. { "type": "product_deal_section", "content": { "title": "Health Supplements", "subtitle": "Amazing

    Deals for your daily", "button": { "title": "See all", "deepLink": “app: / / see_all/categories” } }, "cards": [ ] } Jetpack Compose
  36. "cards": [ { "card_type": "product_image_title", "id": “sku9aabcd", "name": "Boost Jar",

    "price": 247, "addedQty": 0, "imageUrl": "product_url", "offerPrice": 222 } ] Jetpack Compose
  37. "cards": [ { "card_type": "product_image_title", "id": “sku9aabcd", "name": "Boost Jar",

    "price": 247, "addedQty": 0, "imageUrl": "product_url", "offerPrice": 222 } ] Jetpack Compose
  38. Jetpack Compose

  39. "cards": [ { "card_type": "product_image_title", "id": “sku9aabcd", "name": "Boost Jar",

    "price": 247, "addedQty": 0, "imageUrl": "product_url", "offerPrice": 222 } ] Jetpack Compose
  40. "cards": [ { "card_type": "product_image_title", "id": “sku9aabcd", "name": "Boost Jar",

    "price": 247, "addedQty": 0, "imageUrl": "product_url", "offerPrice": 222 } ] Jetpack Compose
  41. Jetpack Compose

  42. Jetpack Compose

  43. @Composable fun ProductsCarousel( productCarouselState: ProductCarouselState, modif i er: Modif i

    er = Modif i er, displaySeeAll: Boolean = true ) { ConstraintLayout(modif i er.f i llMaxWidth()) { | 
 
 
 
 
 
 
 
 Carousel Container
  44. @Composable fun ProductsCarousel( productCarouselState: ProductCarouselState, modif i er: Modif i

    er = Modif i er, displaySeeAll: Boolean = true ) { ConstraintLayout(modif i er.f i llMaxWidth()) { | 
 
 
 
 
 
 
 
 Carousel Container val () = createRefs() Box { productCarouselState.heading() } Box { productCarouselState.subTitle() } 

  45. @Composable fun ProductsCarousel( productCarouselState: ProductCarouselState, modif i er: Modif i

    er = Modif i er, displaySeeAll: Boolean = true ) { ConstraintLayout(modif i er.f i llMaxWidth()) { | 
 
 
 
 
 
 
 
 Carousel Container val () = createRefs() Box { productCarouselState.heading() } Box { productCarouselState.subTitle() } 

  46. ConstraintLayout(modif i er.f i llMaxWidth()) { heading() subTitle() if (displaySeeAll)

    { Text( text = stringResource(R.string.cta_see_all), . . modif i er = Modif i er .constrainAs(seeAllText) .clickable{} ) } LazyRow(content = { itemsIndexed(productCarouselState.items) { index, item - > itemLayout( item =item ) } })
  47. ConstraintLayout(modif i er.f i llMaxWidth()) { heading() subTitle() if (displaySeeAll)

    { Text( text = stringResource(R.string.cta_see_all), . . modif i er = Modif i er .constrainAs(seeAllText) .clickable{} ) } LazyRow(content = { itemsIndexed(productCarouselState.items) { index, item - > itemLayout( item =item ) } })
  48. Element DTO interface ElementDto : State { val children: List<ElementDto>?

    val viewType: ViewTypes val subtitle: String? val title: String? val buttonTitle: String? val buttonDeepLink: String? val imageUrl: String? val id: String? }
  49. sealed class ChildrenTypes(val key: String) : ChildType { object RoundImageAndTitleChild

    : HomeChildren("round_image_title") object ProductImageAndTitleChild : HomeChildren("product_image_title") object OfferCardChild : HomeChildren("large_image_title_subtitle") object BrandsChild : HomeChildren("brands_image_title") } Types
  50. data class ScreenDto ( val children: List<ElementDto>? = null )

    DTO
  51. UI binding interface ComposableElement { @Composable fun compose(hoist: Map<String, MutableState<String

    > > ) fun getHoist() : Map<String, MutableState<String > > }
  52. EmptyElement class EmptyElement : ComposableElement { @Composable override fun compose(hoist:

    Map<String, MutableState<String > > ) { } override fun getHoist() : Map<String, MutableState<String > > { return mapOf() } }
  53. class Screen (screenData: ScreenData) { var elements = screenData.children ?

    . map { it.getComposableElement() } ?: listOf() @Composable fun compose() { Column { val f i elds = elements.map { it.getHoist() } Column { elements.zip(f i elds).map { it.f i rst.compose(it.second) } } } } } Root screen
  54. class Screen (screenData: ScreenData) { var elements = screenData.children ?

    . map { it.getComposableElement() } ?: listOf() @Composable fun compose() { Column { val f i elds = elements.map { it.getHoist() } Column { elements.zip(f i elds).map { it.f i rst.compose(it.second) } } } } } Root screen
  55. class Screen (screenData: ScreenData) { var elements = screenData.children ?

    . map { it.getComposableElement() } ?: listOf() @Composable fun compose() { Column { val f i elds = elements.map { it.getHoist() } Column { elements.zip(f i elds).map { it.f i rst.compose(it.second) } } } } } Root screen
  56. class Screen (screenData: ScreenData) { var elements = screenData.children ?

    . map { it.getComposableElement() } ?: listOf() @Composable fun compose() { Column { val f i elds = elements.map { it.getHoist() } Column { elements.zip(f i elds).map { it.f i rst.compose(it.second) } } } } } Root screen
  57. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } API response to UI
  58. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } API
  59. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } Return a UI model
  60. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } Transformer - map
  61. Map override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList

    = response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) }
  62. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } UI layer model
  63. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } SDUI
  64. private fun generateDTO(item: SectionsItem) : ElementDto { return when (item.type)

    { CarouselSection.key - > CarouselElementDTO( title = item.content.title, children = item.cards ? . map { childItem - > generateChildrenDTO(childItem) }, buttonDeepLink = item.content.button.deepLink, buttonTitle = item.content.button.title ) .... } Generating DTO
  65. private fun generateDTO(item: SectionsItem) : ElementDto { return when (item.type)

    { CarouselSection.key - > CarouselElementDTO( title = item.content.title, children = item.cards ? . map { childItem - > generateChildrenDTO(childItem) }, buttonDeepLink = item.content.button.deepLink, buttonTitle = item.content.button.title ) .... } Generating DTO
  66. private fun generateChildrenDTO(item: CardsItem) : ElementDto { } Generating Child

    DTO
  67. Generating Child DTO return when (item.cardType) { ProductImageAndTitleChild.key - >

    ProductItemElementDTO( id = item.id, name = item.name ?: "", price = item.price ?: 0.0, offerPrice = item.offerPrice ?: 0.0, imageUrl = item.imageUrl, qty = 0 ) else - > EmptyElementDTO() } private fun generateChildrenDTO(item: CardsItem) : ElementDto { }
  68. Generating Child DTO return when (item.cardType) { ProductImageAndTitleChild.key - >

    ProductItemElementDTO( id = item.id, name = item.name ?: "", price = item.price ?: 0.0, offerPrice = item.offerPrice ?: 0.0, imageUrl = item.imageUrl, qty = 0 ) else - > EmptyElementDTO() } private fun generateChildrenDTO(item: CardsItem) : ElementDto { }
  69. private fun generateChildrenDTO(item: CardsItem) : ElementDto { } Generating Child

    DTO return when (item.cardType) { ProductImageAndTitleChild.key - > ProductItemElementDTO( id = item.id, name = item.name ?: "", price = item.price ?: 0.0, offerPrice = item.offerPrice ?: 0.0, imageUrl = item.imageUrl, qty = 0 ) else - > EmptyElementDTO() }
  70. class MainActivity : ComponentActivity() { . . override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContent { CustomTheme { Screen(screenData) / / screenData - > API } } } } } Activity
  71. Activity class MainActivity : ComponentActivity() { . . override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CustomTheme { Screen(screenData) / / screenData - > API } } } } }
  72. Key learnings - Think of Server driven UI as a

    set of implementation.
  73. - Think of Server driven UI as a set of

    implementation. Key learnings - Method to logical standpoint - We can think of server driven UI as moving the model and the controller to the server.
  74. Key learnings - Biggest advantage that outweighs all of the

    disadvantages, all of the trade o ff s is that you make this large upfront investment in separating the thick client from thick back end. - Faster time to market.
  75. References Compose Server driven UI • https://speakerdeck.com/aldefy/a-page-out-of-server-driven-ui- on-android • https://github.com/vipulasri/JetDelivery

  76. References More • https://proandroiddev.com/server-driven-ui-using-jetpack- compose-8fae9889bb2b • https://github.com/aldefy/StarWarsApp (XML - Epoxy)

    • https://jetc.dev/ 
 Beagle - https://github.com/ZupIT/beagle Epoxy - https://github.com/haroldadmin/MoonShot
  77. Check out https://github.com/aldefy/Andromeda 
 https://bit.ly/3Nic0JF - Sample catalog app 


    
 Andromeda is an open-source Jetpack Compose design system. A collection of guidelines and components can be used to create amazing compose app user experiences. Foundations introduce Andromeda tokens and principles while Components provide the bolts and nuts that make Andromeda Compose wrapped apps tick.
  78. Thats all folks! https://cal.com/adit/30min 🎯@aditlal 🔗aditlal.dev