Slide 1

Slide 1 text

Google Developers 🎯@aditlal 🔗aditlal.dev Lucknow A page out of Server driven UI on Android Adit Lal 
 Individual Consultant

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

UI should be a breeze to change Code should be dynamic Building new UI should be fast Learn, adopt , launch quickly Goal

Slide 4

Slide 4 text

Whats the problem?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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?

Slide 7

Slide 7 text

Write code to render a State Handle events from these State changes Logic - Changes?

Slide 8

Slide 8 text

Execution Easy to understand Flexible for designers Launch without a need to open Play Console DRY - minimise repetition Easy to maintain

Slide 9

Slide 9 text

State Rendering 
 - View model 
 - Engine Components Source: Spotify Execution

Slide 10

Slide 10 text

Rendering 
 - View model 
 - Engine Compo

Slide 11

Slide 11 text

Components

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Solution Response UI Component Data View 
 Component Container

Slide 16

Slide 16 text

Components driven UI

Slide 17

Slide 17 text

Server response - Checkbox

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Screen

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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" }

Slide 24

Slide 24 text

{ "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

Slide 25

Slide 25 text

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" }

Slide 26

Slide 26 text

{ "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

Slide 27

Slide 27 text

{ "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

Slide 28

Slide 28 text

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" }

Slide 29

Slide 29 text

Epoxy 
 from Airbnb 
 Litho 
 from Facebook 
 Proteus 
 from Flipkart 
 Graywater 
 from Tumblr 
 Groupie Jetpack Compose Frameworks

Slide 30

Slide 30 text

Jetpack Compose

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Jetpack Compose LazyColumn

Slide 33

Slide 33 text

Jetpack Compose LazyColumn

Slide 34

Slide 34 text

{ "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

Slide 35

Slide 35 text

{ "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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Jetpack Compose

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Jetpack Compose

Slide 42

Slide 42 text

Jetpack Compose

Slide 43

Slide 43 text

@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

Slide 44

Slide 44 text

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


Slide 45

Slide 45 text

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


Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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? }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

data class ScreenDto ( val children: List? = null ) DTO

Slide 51

Slide 51 text

UI binding interface ComposableElement { @Composable fun compose(hoist: Map > ) fun getHoist() : Map > }

Slide 52

Slide 52 text

EmptyElement class EmptyElement : ComposableElement { @Composable override fun compose(hoist: Map > ) { } override fun getHoist() : Map > { return mapOf() } }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

private fun generateChildrenDTO(item: CardsItem) : ElementDto { } Generating Child DTO

Slide 67

Slide 67 text

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 { }

Slide 68

Slide 68 text

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 { }

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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.

Slide 75

Slide 75 text

References Compose Server driven UI • https://speakerdeck.com/aldefy/a-page-out-of-server-driven-ui- on-android • https://github.com/vipulasri/JetDelivery

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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.

Slide 78

Slide 78 text

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