Save 37% off PRO during our Black Friday Sale! »

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 multiple offerings such as JetPack Compose or Epoxy (Airbnb's library), and we then take a look at some tips and tricks to navigate the code from start to finish.

F41e61439763a44b50250cc72e3e2d29?s=128

Adit Lal

June 26, 2021
Tweet

Transcript

  1. A page out of Server driven UI on Android

  2. Individual Consultant 🎯@aditlal 🔗aditlal.dev India Adit Lal

  3. 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. Server Driven UI - Embrace
  4. UI should be a breeze to change Code should be

    dynamic Building new UI should be fast Learn, adopt , launch quickly Server Driven UI - Goal
  5. Whats the problem?

  6. UI Logic - Now Service / API Client Service /

    API Service / API Client Client
  7. 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 UI Logic - Downsides?
  8. Write code to render a State Handle events from these

    State changes UI Logic - Change
  9. UI = fun(state) UI Logic - Idea !! Source: Spotify

  10. Idea Execution

  11. Execution Easy to understand Flexible for designers Launch without a

    need to open Play Console DRY - minimise repetition Easy to maintain
  12. Let’s breakdown State Rendering 
 - View model 
 -

    Engine Components Source: Spotify
  13. Rendering 
 - View model 
 - Engine Compo

  14. Components

  15. • UI = fun(state) • state = json • json

    = backend UI equation Source: Spotify
  16. UI = fun(backend) Revision Source: Spotify

  17. Frameworks Epoxy 
 from Airbnb 
 Litho 
 from Facebook

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

    
 Proteus 
 from Flipkart 
 Graywater 
 from Tumblr 
 Groupie Jetpack Compose
  19. Epoxy

  20. Solution Response UI Component Data View 
 Component Container

  21. Components driven UI

  22. Epoxy

  23. Server response - Checkbox

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

    : P", "viewType":"checkbox" }
  25. Server Components

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

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

    / sub components ], "viewType":"viewgroup", "type":"card" }
  28. 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",
  29. { "children" : [ { "viewType":"circular_image", "imageUrl":"https: / / source.unsplash.com/random/widthxheight"

    }, { "children" : [ { "viewType":"text", "text":"Adit Lal" }, { "viewType":"text", "text":"Engineer" } ], "viewType":"viewgroup", Server Components - Card
  30. { "children" : [ { "viewType":"circular_image", "imageUrl":"https: / / source.unsplash.com/random/widthxheight"

    }, { "children" : [ { "viewType":"text", "text":"Adit Lal" }, { "viewType":"text", "text":"Engineer" } ], "viewType":"viewgroup", Server Components - Card
  31. { "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", Server Components - Card #2
  32. { "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", Server Components - Card #2
  33. { "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", Server Components - Card #2
  34. Epoxy- RecyclerView 
 EpoxyModel = ViewModel State/Data EpoxyModel View

  35. Epoxy- RecyclerView Item <-> EpoxyModel <->Layout resource

  36. Item interface BaseComponentData: Parcelable { val id: String }

  37. Item interface ComponentData : BaseComponentData { val width: Width val

    height: Height val viewType: String val paddingHorizontal: Int val paddingVertical: Int val gravity: Gravity val extraPayload: Parcelable? }
  38. Item JSON { "gravity": "5", "height": "0", "id": "textId", "text":

    "Hello World ! ! ", "viewType": "text", "width": "1" }
  39. Item JSON { "gravity": "5", "height": "0", "id": "textId", "text":

    "Hello World ! ! ", "viewType": "text", "width": "1" }
  40. Item @Parcelize data class TextComponentData( override val width: Width =

    Width.FILL, override val height: Height = Height.WRAP, override val id: String = "", override val gravity: Gravity = Gravity.NO_GRAVITY, override val viewType: String = "text", override val paddingHorizontal: Int = 0, override val paddingVertical: Int = 0, override val extraPayload: Parcelable ? = null, val text: String = "" ) : ComponentData
  41. Item @Parcelize data class TextComponentData( override val width: Width =

    Width.FILL, override val height: Height = Height.WRAP, override val id: String = "", override val gravity: Gravity = Gravity.NO_GRAVITY, override val viewType: String = "text", override val paddingHorizontal: Int = 0, override val paddingVertical: Int = 0, override val extraPayload: Parcelable ? = null, val text: String = "" ) : ComponentData
  42. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text } class ViewHolder : BaseEpoxyHolder() { val textView: TextView by bind(R.id.text) }
  43. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text } class ViewHolder : BaseEpoxyHolder() { val textView: TextView by bind(R.id.text) }
  44. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text } class ViewHolder : BaseEpoxyHolder() { val textView: TextView by bind(R.id.text) }
  45. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text } class ViewHolder : BaseEpoxyHolder() { val textView: TextView by bind(R.id.text) }
  46. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text } class ViewHolder : BaseEpoxyHolder() { val textView: TextView by bind(R.id.text) }
  47. Epoxy- Controller class ComponentController() : TypedEpoxyController<List<BaseComponentData > > () {

    override fun buildModels(data: List<BaseComponentData>) { data.map { componentData - > generateModel().addTo(this) } } }
  48. Epoxy- Controller class ComponentController( val componentClickHandler: ComponentClickHandler, val uncaughtViewData: ViewComponentNotDrawnHandler

    ) : TypedEpoxyController<List<BaseComponentData > > () { override fun buildModels(data: List<BaseComponentData>) { data.map { componentData - > generateModel( data = componentData, componentClickHandler = { extra - > componentClickHandler(extra) }, viewComponentNotDrawnHandler = { data - > uncaughtViewData(data) } ).addTo(this) } }
  49. Epoxy- Controller class ComponentController( val componentClickHandler: ComponentClickHandler, val uncaughtViewData: ViewComponentNotDrawnHandler

    ) : TypedEpoxyController<List<BaseComponentData > > () { override fun buildModels(data: List<BaseComponentData>) { data.map { componentData - > generateModel( data = componentData, componentClickHandler = { extra - > componentClickHandler(extra) }, viewComponentNotDrawnHandler = { data - > uncaughtViewData(data) } ).addTo(this) } }
  50. Epoxy- Controller class ComponentController( val componentClickHandler: ComponentClickHandler, val uncaughtViewData: ViewComponentNotDrawnHandler

    ) : TypedEpoxyController<List<BaseComponentData > > () { override fun buildModels(data: List<BaseComponentData>) { data.map { componentData - > generateModel( data = componentData, componentClickHandler = { extra - > componentClickHandler(extra) }, viewComponentNotDrawnHandler = { data - > uncaughtViewData(data) } ).addTo(this) } }
  51. Epoxy- Controller class ComponentController( val componentClickHandler: ComponentClickHandler, val uncaughtViewData: ViewComponentNotDrawnHandler

    ) : TypedEpoxyController<List<BaseComponentData > > () { override fun buildModels(data: List<BaseComponentData>) { data.map { componentData - > generateModel( data = componentData, componentClickHandler = { extra - > componentClickHandler(extra) }, viewComponentNotDrawnHandler = { data - > uncaughtViewData(data) } ).addTo(this) } }
  52. Epoxy- Controller class ComponentController( val componentClickHandler: ComponentClickHandler, val uncaughtViewData: ViewComponentNotDrawnHandler

    ) : TypedEpoxyController<List<BaseComponentData > > () { override fun buildModels(data: List<BaseComponentData>) { data.map { componentData - > generateModel( data = componentData, componentClickHandler = { extra - > componentClickHandler(extra) }, viewComponentNotDrawnHandler = { data - > uncaughtViewData(data) } ).addTo(this) } }
  53. Epoxy- Extension fun generateModel( data: BaseComponentData, componentClickHandler: ComponentClickHandler, viewComponentNotDrawnHandler: ViewComponentNotDrawnHandler

    ) : EpoxyModel < * > { return when (data) { is TextComponentData - > { TextComponent_() .id(data.id) .textComponentData(data) .componentClickHandler(componentClickHandler) } else - > { / * Do Nothing * / } }
  54. Epoxy- Extension fun generateModel( data: BaseComponentData, componentClickHandler: ComponentClickHandler, viewComponentNotDrawnHandler: ViewComponentNotDrawnHandler

    ) : EpoxyModel < * > { return when (data) { is TextComponentData - > { TextComponent_() .id(data.id) .textComponentData(data) .componentClickHandler(componentClickHandler) } else - > { / * Do Nothing * / } }
  55. Epoxy- Extension fun generateModel( data: BaseComponentData, componentClickHandler: ComponentClickHandler, viewComponentNotDrawnHandler: ViewComponentNotDrawnHandler

    ) : EpoxyModel < * > { return when (data) { is TextComponentData - > { TextComponent_() .id(data.id) .textComponentData(data) .componentClickHandler(componentClickHandler) } else - > { / * Do Nothing * / } }
  56. Epoxy- Extension fun generateModel( data: BaseComponentData, componentClickHandler: ComponentClickHandler, viewComponentNotDrawnHandler: ViewComponentNotDrawnHandler

    ) : EpoxyModel < * > { return when (data) { is TextComponentData - > { TextComponent_() .id(data.id) .textComponentData(data) .componentClickHandler(componentClickHandler) } else - > { / * Do Nothing * / } }
  57. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData @EpoxyAttribute var componentClickHandler: ComponentClickHandler = {} override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text } class ViewHolder : BaseEpoxyHolder() { val textView: TextView by bind(R.id.text)
  58. Epoxy- EpoxyModel @EpoxyModelClass(layout = R.layout.component_text_view) abstract class TextComponent : EpoxyModelWithHolder<TextComponent.ViewHolder>()

    { @EpoxyAttribute lateinit var textComponentData: TextComponentData @EpoxyAttribute var componentClickHandler: ComponentClickHandler = {} override fun bind(holder: ViewHolder) { super.bind(holder) holder.textView.text = textComponentData.text holder.textView.setOnClickListener { componentClickHandler(textComponentData.extraPayload ! ! ) } }
  59. Screen class ScreenListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? =

    null ) : FrameLayout(context, attrs) { private var componentClickHandler: ComponentClickHandler = {} private val controller: ComponentController by lazy { ComponentController( componentClickHandler = { componentClickHandler(it) }, uncaughtViewData = { data - > viewComponentNotDrawnHandler(data) } ) } . .
  60. Screen class ScreenListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? =

    null ) : FrameLayout(context, attrs) { private var componentClickHandler: ComponentClickHandler = {} private val controller: ComponentController by lazy { ComponentController( componentClickHandler = { componentClickHandler(it) }, uncaughtViewData = { data - > viewComponentNotDrawnHandler(data) } ) } . .
  61. Screen class ScreenListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? =

    null ) : FrameLayout(context, attrs) { private var componentClickHandler: ComponentClickHandler = {} private val controller: ComponentController by lazy { . . } private var layoutManager: LinearLayoutManager = LinearLayoutManager(context) private var binding: ScreenListViewBinding = ScreenListViewBinding.inflate(LayoutInflater.from(context), this, true) }
  62. Screen class ScreenListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? =

    null ) : FrameLayout(context, attrs) { private var componentClickHandler: ComponentClickHandler = {} private val controller: ComponentController by lazy { . . } private var layoutManager: LinearLayoutManager = LinearLayoutManager(context) private var binding: ScreenListViewBinding = ScreenListViewBinding.inflate(LayoutInflater.from(context), this, true) init { with(binding.eRV) { layoutManager = this@AndromedaListView.layoutManager setController(controller) } }
  63. Screen class ScreenListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? =

    null ) : FrameLayout(context, attrs) { . . init { . . } fun setUpComponents(components: List<BaseComponentData>) { controller.setData(components) } fun setComponentClickHandler(componentClickHandler: ComponentClickHandler) { this.componentClickHandler = componentClickHandler } }
  64. Network State private val componentRuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory .of(ComponentData : :

    class.java, "viewType", true) .registerSubtype(TextComponentData : : class.java, “text”) . . val gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapterFactory(componentRuntimeTypeAdapterFactory) .registerTypeAdapterFactory( object : TypeAdapterFactory { override fun <T : Any> create(gson: Gson, type: TypeToken<T>) : TypeAdapter<T> { val kclass = Reflection.getOrCreateKotlinClass(type.rawType) return if (kclass.sealedSubclasses.any()) { SealedClassTypeAdapter<T>(kclass, gson) } else gson.getDelegateAdapter(this, type) }
  65. Network State private val componentRuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory .of(ComponentData : :

    class.java, "viewType", true) .registerSubtype(TextComponentData : : class.java, “text”) . . val gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapterFactory(componentRuntimeTypeAdapterFactory) .registerTypeAdapterFactory( object : TypeAdapterFactory { override fun <T : Any> create(gson: Gson, type: TypeToken<T>) : TypeAdapter<T> { val kclass = Reflection.getOrCreateKotlinClass(type.rawType) return if (kclass.sealedSubclasses.any()) { SealedClassTypeAdapter<T>(kclass, gson) } else gson.getDelegateAdapter(this, type) }
  66. Network State private val componentRuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory .of(ComponentData : :

    class.java, "viewType", true) .registerSubtype(TextComponentData : : class.java, “text”) . . val gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapterFactory(componentRuntimeTypeAdapterFactory) .registerTypeAdapterFactory( object : TypeAdapterFactory { override fun <T : Any> create(gson: Gson, type: TypeToken<T>) : TypeAdapter<T> { val kclass = Reflection.getOrCreateKotlinClass(type.rawType) return if (kclass.sealedSubclasses.any()) { SealedClassTypeAdapter<T>(kclass, gson) } else gson.getDelegateAdapter(this, type) }
  67. Network State private val componentRuntimeTypeAdapterFactory = RuntimeTypeAdapterFactory .of(ComponentData : :

    class.java, "viewType", true) .registerSubtype(TextComponentData : : class.java, “text”) . . val gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapterFactory(componentRuntimeTypeAdapterFactory) .registerTypeAdapterFactory( object : TypeAdapterFactory { override fun <T : Any> create(gson: Gson, type: TypeToken<T>) : TypeAdapter<T> { val kclass = Reflection.getOrCreateKotlinClass(type.rawType) return if (kclass.sealedSubclasses.any()) { SealedClassTypeAdapter<T>(kclass, gson) } else gson.getDelegateAdapter(this, type) }
  68. Compose

  69. UI as a function {Kotlin} () - > 📱 Declarative

    UI
  70. Compose

  71. Compose

  72. Compose

  73. @Composable fun showBannerElement() { Container(width = 150.dp, height = 178.dp)

    { Clip(shape = RoundedCornerShape(5.dp)) { DrawImage(image = imageResource(id = R.drawable.placeholder_banner)) } } } Banner
  74. Banner @Composable fun showBannerElement() { Container(width = 150.dp, height =

    178.dp) { Clip(shape = RoundedCornerShape(5.dp)) { DrawImage(image = imageResource(id = R.drawable.placeholder_banner)) } } }
  75. @Composable fun showBannerElement() { Container(width = 150.dp, height = 178.dp)

    { Clip(shape = RoundedCornerShape(5.dp)) { DrawImage(image = imageResource(id = R.drawable.placeholder_banner)) } } } Banner
  76. @Composable private fun showBannerText() { Align(alignment = Alignment.BottomLeft) { Container(

    modif i er = LayoutWidth.Fill, height = 50.dp, alignment = Alignment.CenterLeft ) { DrawShape(shape = RectangleShape, color = Color.Black.copy(alpha = 0.3f)) Text( text = “Cute Puppy", style = currentTextStyle().copy(color = Color.White, fontSize = 12.sp), modif i er = LayoutPadding(10.dp) ) } } } Banner
  77. @Composable private fun showBannerText() { Align(alignment = Alignment.BottomLeft) { Container(

    modif i er = LayoutWidth.Fill, height = 50.dp, alignment = Alignment.CenterLeft ) { DrawShape(shape = RectangleShape, color = Color.Black.copy(alpha = 0.3f)) Text( text = “Cute Puppy", style = currentTextStyle().copy(color = Color.White, fontSize = 12.sp), modif i er = LayoutPadding(10.dp) ) } } } Banner Cute Puppy
  78. @Composable private fun showBannerText() { Align(alignment = Alignment.BottomLeft) { Container(

    modif i er = LayoutWidth.Fill, height = 50.dp, alignment = Alignment.CenterLeft ) { DrawShape(shape = RectangleShape, color = Color.Black.copy(alpha = 0.3f)) Text( text = “Cute Puppy", style = currentTextStyle().copy(color = Color.White, fontSize = 12.sp), modif i er = LayoutPadding(10.dp) ) } } } Banner Cute Puppy
  79. class BannerElement(val elementDto: ElementDto) : ComposableElement { val f i

    eldName = elementDto.data?:"value" @Composable override fun compose(hoist: Map<String, MutableState<String > > ) { Align(alignment = Alignment.BottomLeft) { Container( modif i er = LayoutWidth.Fill, height = 50.dp, alignment = Alignment.CenterLeft ) { DrawShape(shape = RectangleShape, color = Color.Black.copy(alpha = 0.3f)) Text( text = hoist.get(f i eldName) ? . value ?: "", style = currentTextStyle().copy(color = Color.White, fontSize = 12.sp), modif i er = LayoutPadding(10.dp) ) } } override fun getHoist() : Map<String, MutableState<String > > { return mapOf(Pair(f i eldName, mutableStateOf(elementDto.default?:""))) } } Banner Cute Puppy
  80. UI binding interface ComposableElement { @Composable fun compose(hoist: Map<String, MutableState<String

    > > ) fun getHoist() : Map<String, MutableState<String > > }
  81. enum class ViewType { BANNER, TEXT, PROMO } DTO

  82. @JsonClass(generateAdapter = true) data class ScreenDto ( val children: List<ElementDto>?

    = null ) @JsonClass(generateAdapter = true) data class ElementDto ( val children: List<ElementDto>? = null, val label: String? = null, val viewtype: ViewType? = null, val default: String? = null, val data: String? = null ) DTO
  83. class BannerElement(val elementDto: ElementDto) : ComposableElement { val f i

    eldName = elementDto.data?:"value" @Composable override fun compose(hoist: Map<String, MutableState<String > > ) { Align(alignment = Alignment.BottomLeft) { Container( modif i er = LayoutWidth.Fill, height = 50.dp, alignment = Alignment.CenterLeft ) { DrawShape(shape = RectangleShape, color = Color.Black.copy(alpha = 0.3f)) Text( text = hoist.get(f i eldName) ? . value ?: "", style = currentTextStyle().copy(color = Color.White, fontSize = 12.sp), modif i er = LayoutPadding(10.dp) ) } } override fun getHoist() : Map<String, MutableState<String > > { return mapOf(Pair(f i eldName, mutableStateOf(elementDto.default?:""))) } } Banner Cute Puppy
  84. EmptyElement class EmptyElement : ComposableElement { @Composable override fun compose(hoist:

    Map<String, MutableState<String > > ) { } override fun getHoist() : Map<String, MutableState<String > > { return mapOf() } }
  85. 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
  86. 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
  87. 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
  88. 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
  89. class MainActivity : ComponentActivity() { . . override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel : : class.java) setContent { CustomTheme { MyScreenContent() } } } } . . . } Activity
  90. class MainActivity : ComponentActivity() { . . override fun onCreate(savedInstanceState:

    Bundle?) { . . . } data class StringHolder(var held: MutableState<String>) val ScreenJson = ambientOf<StringHolder>() } Activity
  91. @Composable fun MyScreenContent() { val screenJson = viewModel.dashboardItems / /

    server api call val screenJsonString = StringHolder(remember {mutableStateOf(screenJson)}) val moshi = Moshi.Builder() .build() val screenAdapter = moshi.adapter(ScreenData : : class.java) Providers(ScreenJson provides screenJsonString) { val holder = ScreenJson.current screenAdapter.fromJson(holder.held.value) ? . let { Screen(it).compose() } } } Content
  92. @Composable fun MyScreenContent() { val screenJson = viewModel.dashboardItems / /

    server api call val screenJsonString = StringHolder(remember {mutableStateOf(screenJson)}) val moshi = Moshi.Builder() .build() val screenAdapter = moshi.adapter(ScreenData : : class.java) Providers(ScreenJson provides screenJsonString) { val holder = ScreenJson.current screenAdapter.fromJson(holder.held.value) ? . let { Screen(it).compose() } } } Content
  93. @Composable fun MyScreenContent() { val screenJson = viewModel.dashboardItems / /

    server api call val screenJsonString = StringHolder(remember {mutableStateOf(screenJson)}) val moshi = Moshi.Builder() .build() val screenAdapter = moshi.adapter(ScreenData : : class.java) Providers(ScreenJson provides screenJsonString) { val holder = ScreenJson.current screenAdapter.fromJson(holder.held.value) ? . let { Screen(it).compose() } } } Content
  94. @Composable fun MyScreenContent() { val screenJson = viewModel.dashboardItems / /

    server api call val screenJsonString = StringHolder(remember {mutableStateOf(screenJson)}) val moshi = Moshi.Builder() .build() val screenAdapter = moshi.adapter(ScreenData : : class.java) Providers(ScreenJson provides screenJsonString) { val holder = ScreenJson.current screenAdapter.fromJson(holder.held.value) ? . let { Screen(it).compose() } } } Content
  95. @Composable fun MyScreenContent() { val screenJson = viewModel.dashboardItems / /

    server api call val screenJsonString = StringHolder(remember {mutableStateOf(screenJson)}) val moshi = Moshi.Builder() .build() val screenAdapter = moshi.adapter(ScreenData : : class.java) Providers(ScreenJson provides screenJsonString) { val holder = ScreenJson.current screenAdapter.fromJson(holder.held.value) ? . let { Screen(it).compose() } } } Content
  96. class MainActivity : ComponentActivity() { . . override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel : : class.java) setContent { CustomTheme { MyScreenContent() } } } } . . . } Activity
  97. class MainActivity : ComponentActivity() { . . override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel : : class.java) setContent { CustomTheme { MyScreenContent() } } } . . . } Activity
  98. References Compose Server driven UI • https://github.com/vipulasri/JetDelivery • https://proandroiddev.com/server-driven-ui-using-jetpack- compose-8fae9889bb2b

    • https://github.com/mwshubham/JetpackCompose • https://github.com/Foso/Jetpack-Compose-Playground/ • https://jetc.dev/ 
 Beagle - https://github.com/ZupIT/beagle Epoxy - https://github.com/haroldadmin/MoonShot
  99. Thats all folks! https://calendly.com/aditlal/30min 🎯@aditlal 🔗aditlal.dev