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

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

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. 5 Service / API Client Service / API Service /

    API Client Client Whats the problem?
  5. 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?
  6. Execution Easy to understand Flexible for designers Launch without a

    need to open Play Console DRY - minimise repetition Easy to maintain
  7. Server Components - Card { "id":"7b0dae3729f54340bccf", "children" : [ /

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

    / sub components ], "viewType":"viewgroup", "type":"card" }
  9. 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" }
  10. { "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
  11. 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" }
  12. { "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
  13. { "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
  14. 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" }
  15. Epoxy 
 from Airbnb 
 Litho 
 from Facebook 


    Proteus 
 from Flipkart 
 Graywater 
 from Tumblr 
 Groupie Jetpack Compose Frameworks
  16. { "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
  17. { "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
  18. "cards": [ { "card_type": "product_image_title", "id": “sku9aabcd", "name": "Boost Jar",

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

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

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

    "price": 247, "addedQty": 0, "imageUrl": "product_url", "offerPrice": 222 } ] Jetpack Compose
  22. @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
  23. @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() } 

  24. @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() } 

  25. 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 ) } })
  26. 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 ) } })
  27. 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? }
  28. 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
  29. EmptyElement class EmptyElement : ComposableElement { @Composable override fun compose(hoist:

    Map<String, MutableState<String > > ) { } override fun getHoist() : Map<String, MutableState<String > > { return mapOf() } }
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } API
  36. 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
  37. override fun map(response: ServerResponse) : HomeUIModel { val elementsDTOList =

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

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

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

    response.data.sections ? . map { item - > generateDTO(item) } return HomeUIModel( screenDto = ScreenDto(elementsDTOList ?: emptyList()) ) } SDUI
  41. 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
  42. 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
  43. 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 { }
  44. 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 { }
  45. 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() }
  46. class MainActivity : ComponentActivity() { . . override fun onCreate(savedInstanceState:

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

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CustomTheme { Screen(screenData) / / screenData - > API } } } } }
  48. - 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.
  49. 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.
  50. 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
  51. 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.