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

    View Slide

  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.

    View Slide

  3. UI should be a breeze to change


    Code should be dynamic


    Building new UI should be fast


    Learn, adopt , launch quickly
    Goal

    View Slide

  4. Whats the
    problem?

    View Slide

  5. 5
    Service / API
    Client
    Service / API
    Service / API
    Client Client
    Whats the
    problem?

    View Slide

  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?

    View Slide

  7. Write code to render a State


    Handle events from these State changes
    Logic - Changes?

    View Slide

  8. Execution
    Easy to understand


    Flexible for designers


    Launch without a need to open Play Console


    DRY - minimise repetition


    Easy to maintain

    View Slide

  9. State
    Rendering

    - View model

    - Engine
    Components
    Source: Spotify
    Execution

    View Slide

  10. Rendering

    - View model

    - Engine
    Compo

    View Slide

  11. Components

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. Solution
    Response UI Component
    Data
    View

    Component
    Container

    View Slide

  16. Components driven UI

    View Slide

  17. Server response - Checkbox

    View Slide

  18. Server response - Checkbox
    {


    "isEnabled":true,


    "isSelected":false,


    "text":"I\u0027m a checkbox
    :
    P",


    "viewType":"checkbox"


    }


    View Slide

  19. Server response - Checkbox
    {


    "isEnabled":true,


    "isSelected":false,


    "text":"I\u0027m a checkbox
    :
    P",


    "viewType":"checkbox"


    }


    View Slide

  20. Screen

    View Slide

  21. Server Components - Card
    {


    "id":"7b0dae3729f54340bccf",


    "children"
    :
    [


    / /
    sub components


    ],


    "viewType":"viewgroup",


    "type":"card"


    }


    View Slide

  22. Server Components - Card
    {


    "id":"7b0dae3729f54340bccf",


    "children"
    :
    [


    / /
    sub components


    ],


    "viewType":"viewgroup",


    "type":"card"


    }


    View Slide

  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"


    }


    View Slide

  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

    View Slide

  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"


    }


    View Slide

  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

    View Slide

  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

    View Slide

  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"


    }

    View Slide

  29. Epoxy

    from Airbnb

    Litho

    from Facebook

    Proteus

    from Flipkart

    Graywater

    from Tumblr

    Groupie


    Jetpack Compose
    Frameworks

    View Slide

  30. Jetpack Compose

    View Slide

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

    View Slide

  32. Jetpack Compose
    LazyColumn

    View Slide

  33. Jetpack Compose
    LazyColumn

    View Slide

  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

    View Slide

  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

    View Slide

  36. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View Slide

  37. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View Slide

  38. Jetpack Compose

    View Slide

  39. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View Slide

  40. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View Slide

  41. Jetpack Compose

    View Slide

  42. Jetpack Compose

    View Slide

  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

    View Slide

  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()


    }

    View Slide

  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()


    }

    View Slide

  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


    )


    }


    })

    View Slide

  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


    )


    }


    })

    View Slide

  48. Element DTO
    interface ElementDto : State {


    val children: List?


    val viewType: ViewTypes


    val subtitle: String?


    val title: String?


    val buttonTitle: String?


    val buttonDeepLink: String?


    val imageUrl: String?


    val id: String?


    }

    View Slide

  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

    View Slide

  50. data class ScreenDto (


    val children: List? = null


    )
    DTO

    View Slide

  51. UI binding
    interface ComposableElement {


    @Composable


    fun compose(hoist: Map> >
    )


    fun getHoist()
    :
    Map> >

    }


    View Slide

  52. EmptyElement
    class EmptyElement : ComposableElement {


    @Composable


    override fun compose(hoist: Map> >
    ) {


    }


    override fun getHoist()
    :
    Map> >
    {


    return mapOf()


    }


    }


    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  58. override fun map(response: ServerResponse)
    :
    HomeUIModel {


    val elementsDTOList = response.data.sections
    ? .
    map { item
    - >

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }
    API

    View Slide

  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

    View Slide

  60. override fun map(response: ServerResponse)
    :
    HomeUIModel {


    val elementsDTOList = response.data.sections
    ? .
    map { item
    - >

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }
    Transformer - map

    View Slide

  61. Map
    override fun map(response: ServerResponse)
    :
    HomeUIModel {


    val elementsDTOList = response.data.sections
    ? .
    map { item
    - >

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }

    View Slide

  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

    View Slide

  63. override fun map(response: ServerResponse)
    :
    HomeUIModel {


    val elementsDTOList = response.data.sections
    ? .
    map { item
    - >

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }
    SDUI

    View Slide

  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

    View Slide

  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

    View Slide

  66. private fun generateChildrenDTO(item: CardsItem)
    :
    ElementDto {




    }
    Generating Child DTO

    View Slide

  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 {




    }

    View Slide

  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 {




    }

    View Slide

  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()


    }


    View Slide

  70. class MainActivity : ComponentActivity() {


    . .

    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    CustomTheme {


    Screen(screenData)
    / /
    screenData
    - >
    API


    }


    }


    }


    }


    }
    Activity

    View Slide

  71. Activity
    class MainActivity : ComponentActivity() {


    . .

    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    CustomTheme {


    Screen(screenData)
    / /
    screenData
    - >
    API


    }


    }


    }


    }


    }

    View Slide

  72. Key learnings
    - Think of Server driven UI as a set of implementation.

    View Slide

  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.

    View Slide

  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.

    View Slide

  75. References
    Compose Server driven UI


    • https://speakerdeck.com/aldefy/a-page-out-of-server-driven-ui-
    on-android


    • https://github.com/vipulasri/JetDelivery


    View Slide

  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

    View Slide

  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.


    View Slide

  78. Thats all folks!
    https://cal.com/adit/30min
    🎯@aditlal
    🔗aditlal.dev

    View Slide