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 full-size 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 full-size 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 full-size slide

  4. Whats the
    problem?

    View full-size slide

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

    View full-size 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 full-size slide

  7. Write code to render a State


    Handle events from these State changes
    Logic - Changes?

    View full-size slide

  8. Execution
    Easy to understand


    Flexible for designers


    Launch without a need to open Play Console


    DRY - minimise repetition


    Easy to maintain

    View full-size slide

  9. State
    Rendering

    - View model

    - Engine
    Components
    Source: Spotify
    Execution

    View full-size slide

  10. Rendering

    - View model

    - Engine
    Compo

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. Solution
    Response UI Component
    Data
    View

    Component
    Container

    View full-size slide

  15. Components driven UI

    View full-size slide

  16. Server response - Checkbox

    View full-size slide

  17. Server response - Checkbox
    {


    "isEnabled":true,


    "isSelected":false,


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


    "viewType":"checkbox"


    }


    View full-size slide

  18. Server response - Checkbox
    {


    "isEnabled":true,


    "isSelected":false,


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


    "viewType":"checkbox"


    }


    View full-size slide

  19. Server Components - Card
    {


    "id":"7b0dae3729f54340bccf",


    "children"
    :
    [


    / /
    sub components


    ],


    "viewType":"viewgroup",


    "type":"card"


    }


    View full-size slide

  20. Server Components - Card
    {


    "id":"7b0dae3729f54340bccf",


    "children"
    :
    [


    / /
    sub components


    ],


    "viewType":"viewgroup",


    "type":"card"


    }


    View full-size slide

  21. 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 full-size slide

  22. {


    "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 full-size 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 full-size slide

  24. {


    "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 full-size slide

  25. {


    "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 full-size slide

  26. 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 full-size slide

  27. Epoxy

    from Airbnb

    Litho

    from Facebook

    Proteus

    from Flipkart

    Graywater

    from Tumblr

    Groupie


    Jetpack Compose
    Frameworks

    View full-size slide

  28. Jetpack Compose

    View full-size slide

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

    View full-size slide

  30. Jetpack Compose
    LazyColumn

    View full-size slide

  31. Jetpack Compose
    LazyColumn

    View full-size slide

  32. {


    "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 full-size slide

  33. {


    "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 full-size slide

  34. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View full-size slide

  35. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View full-size slide

  36. Jetpack Compose

    View full-size 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 full-size slide

  38. "cards": [


    {


    "card_type": "product_image_title",


    "id": “sku9aabcd",


    "name": "Boost Jar",


    "price": 247,


    "addedQty": 0,


    "imageUrl": "product_url",


    "offerPrice": 222


    }


    ]
    Jetpack Compose

    View full-size slide

  39. Jetpack Compose

    View full-size slide

  40. Jetpack Compose

    View full-size slide

  41. @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 full-size slide

  42. @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 full-size 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
    val () = createRefs()


    Box {


    productCarouselState.heading()


    }


    Box {


    productCarouselState.subTitle()


    }

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. data class ScreenDto (


    val children: List? = null


    )
    DTO

    View full-size slide

  49. UI binding
    interface ComposableElement {


    @Composable


    fun compose(hoist: Map> >
    )


    fun getHoist()
    :
    Map> >

    }


    View full-size slide

  50. EmptyElement
    class EmptyElement : ComposableElement {


    @Composable


    override fun compose(hoist: Map> >
    ) {


    }


    override fun getHoist()
    :
    Map> >
    {


    return mapOf()


    }


    }


    View full-size slide

  51. 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 full-size slide

  52. 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 full-size 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 full-size 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 full-size slide

  55. 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 full-size slide

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


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

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }
    API

    View full-size slide

  57. 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 full-size slide

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


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

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }
    Transformer - map

    View full-size slide

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


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

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }

    View full-size slide

  60. 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 full-size slide

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


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

    generateDTO(item)


    }


    return HomeUIModel(


    screenDto = ScreenDto(elementsDTOList ?: emptyList())


    )


    }
    SDUI

    View full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

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




    }
    Generating Child DTO

    View full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

  68. class MainActivity : ComponentActivity() {


    . .

    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    CustomTheme {


    Screen(screenData)
    / /
    screenData
    - >
    API


    }


    }


    }


    }


    }
    Activity

    View full-size slide

  69. Activity
    class MainActivity : ComponentActivity() {


    . .

    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    CustomTheme {


    Screen(screenData)
    / /
    screenData
    - >
    API


    }


    }


    }


    }


    }

    View full-size slide

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

    View full-size slide

  71. - 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 full-size slide

  72. 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 full-size slide

  73. References
    Compose Server driven UI


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


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


    View full-size slide

  74. 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 full-size slide

  75. 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 full-size slide

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

    View full-size slide