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

Server Driven Compose With Firebase

Jaewoong
October 24, 2024

Server Driven Compose With Firebase

Server Driven Compose With Firebase

Jaewoong

October 24, 2024
Tweet

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. G E T S T R E A M .

    I O Server Driven Compose With Firebase
  2. G E T S T R E A M .

    I O skydoves @github_skydoves Lead Android Developer Advocate @ Stream Jaewoong Eum
  3. Data-Driven UI { "title": "Server Driven Compose", "items": [ {

    "title": "City", "url": "https://github.com/skydoves" }, { "title": "Suits", "url": "https://github.com/skydoves" } ] } @Composable fun TimelineContent() { Text(..) LazyColumn { } } Backend Client
  4. Data-Driven UI { "clientIdentiferData": { "clientID": "89012", "webPropertyID": "89012-01", "campaignID":

    "004" }, "journeyKeyData": { "campaignCreationPhase": "Teach", "purchaseJourneyPhase": "Awareness", "topicCategory": "Organizer", "topicSubCategory": "Event Team", "topicSubSubCategory": "Null" }, "uploadedData": [{ "dataCategoryLabel": "name of organization", "dataStrings": [{ "string": "UBM" }] }, { "dataCategoryLabel": "our mission", "dataStrings": [{ "string": "We Serve the Needs of B2B Buyers" }] }, { "dataCategoryLabel": "our history", "dataStrings": [{ "string": "UBM was fonded in 1948", "string2": "It is was purchased by Informa in 2017", "string3": "We Serve the Needs of B2B Buyers" }] }, { Backend Client All the form and behavioral structures of the layout are defined by the client "What and how" to draw is the client's responsibility
  5. Data-Driven UI Disadvantages of a Data-Driven UI • Slow update

    cycle: The lengthy review and approval processes for releases and publication can cause significant delays in delivering critical updates. • Slow user adoption: Since users must manually download and install updates, the adoption of new features and bug fixes is slower. Forced updates can negatively impact the user experience. • Slow feature experimentation: Due to the slow update cycle, it becomes challenging for teams to quickly experiment with and iterate on specific features they want to test. • Slow feedback loop: The slow update cycle and adoption rate make it difficult to gather user feedback and quickly implement changes in response.
  6. Web-Driven UI Disadvantages of Hybrid Apps • Learning Curve: Native

    app developers need to acquire new knowledge and skills specific to web apps, involving new languages and frameworks. This shift requires a significant learning curve comparable to a job transition. • Performance Degradation: Hybrid apps rely on web browser engines like WebView Chromium) for rendering, which generally doesn't offer the same level of performance as native rendering systems. • Internet Dependency: While some Progressive Web Apps PWAs support offline caching, most hybrid apps heavily depend on an active internet connection, introducing limitations based on network availability. • Limited Access to Device Features: Hybrid apps often face restrictions in accessing hardware features like GPS, camera, and NFC with the same precision as native apps. Although some features are available in PWAs, they remain limited, and complex protocols like JavaScript Interface are required for integration with WebView.
  7. Server-Driven UI Backend @Composable fun TextUi() { .. } @Composable

    fun ImageUi() { .. } @Composable fun ListUi() { .. } Client
  8. Server-Driven UI Backend All the form and behavioral structure of

    the layout is defined on the server The server is responsible for "what" to draw, and the client is responsible for "how" to draw Client @Composable fun TextUi() { .. } @Composable fun ImageUi() { .. } @Composable fun ListUi() { .. }
  9. Server-Driven UI Advantages of Server-Driven UI • Faster Feature Experimentation:

    Changes to features (layouts) can be quickly deployed without needing app updates, allowing faster feedback loops and enabling rapid experimentation with new functionalities. • Consistent UI: By establishing a stable component design system, Server-Driven UI can ensure consistent user interfaces and behavior across different app versions as long as the core specifications are maintained. • Native Performance: Server-Driven UI retains flexibility while allowing components to be rendered with native performance, outperforming web and hybrid apps in terms of speed and responsiveness. • Reduced Workload for Mobile Developers: Product managers and designers can define the layout, and the backend takes responsibility for deciding "what to do," allowing mobile developers to focus on developing individual components.
  10. Realtime Database • Easy server setup: Even developers without backend

    knowledge can quickly setup a server and easily operate it through the dashboard UI. Firebase Realtime Database
  11. Realtime Database • Easy server setup: Even developers without backend

    knowledge can quickly setup a server and easily operate it through the dashboard UI. • JSON response: You can generate a response by directly importing/exporting a JSON file. Firebase Realtime Database
  12. Realtime Database • Easy server setup: Even developers without backend

    knowledge can quickly setup a server and easily operate it through the dashboard UI. • JSON response: You can generate a response by directly importing/exporting a JSON file. • Real-time: A client SDK is provided that allows you to observe database response changes in real-time, allowing you to reflect and visually confirm changes in values in real-time. Firebase Realtime Database
  13. Problems with Realtime Database Android SDK • Java-based code: The

    SDK is written entirely in Java code and still provides a callback-based API. • No custom serialization: Neither the official SDK nor the community libraries provide custom serialization options for snapshot data. Therefore, it is difficult to support nullability and handle fallbacks flexibly. You must define a default value. Firebase Realtime Database val listener = object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val value = snapshot.child("post") // .. } override fun onCancelled(error: DatabaseError) { // .. } } @IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, ..
  14. Firebase Realtime Database dependencies { implementation("com.github.skydoves:firebase-database-ktx:0.2.0") } private val database

    = Firebase.database(BuildConfig.REALTIME_DATABASE_URL).reference private val json = Json { isLenient = true ignoreUnknownKeys = true prettyPrint = true } val timelineUi = database.flow<TimelineUi>( path = { dataSnapshot -> dataSnapshot.child("timeline") }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ).flatMapLatest { result -> .. }
  15. Component Design Systems { "title": { "text": "Server Driven Compose",

    "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } }
  16. Component Design Systems { "title": { "text": "Server Driven Compose",

    "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } } Text Image
  17. Component Design Systems { "title": { "text": "Server Driven Compose",

    "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } } Text Image List
  18. Component Design Systems { "title": { "text": "Server Driven Compose",

    "size": 26, "fontWeight": "bold" }, "list": { "layout": "grid", "itemSize": { "width": 150, "height": 150 }, "items": [ { "title": "City", "url": "https://github.com/skydoves", "scaleType": "crop" }, { "title": "Suits", "url": "https://github.com/skydoves", "scaleType": "crop" } ] } } Text Image List UiComponent Apply the same abstraction to all UI components
  19. Component Design Systems @Serializable sealed interface UiComponent @Serializable data class

    TextUi( val text: String, val size: Int, val fontWeight: String ) : UiComponent @Serializable data class ImageUi( val url: String, val size: DpSizeUi = DpSizeUi(0, 0), val scaleType: String, val contentDescription: String = "", ) : UiComponent @Serializable data class DpSizeUi( val width: Int, val height: Int )
  20. Component Design Systems @Serializable sealed interface UiComponent @Serializable data class

    TextUi( val text: String, val size: Int, val fontWeight: String ) : UiComponent @Serializable data class ImageUi( val url: String, val size: DpSizeUi = DpSizeUi(0, 0), val scaleType: String, val contentDescription: String = "", ) : UiComponent @Serializable data class DpSizeUi( val width: Int, val height: Int ) @Serializable data class ListUi( val layout: String, val itemSize: DpSizeUi, val items: List<ImageUi>, ) : UiComponent fun String.toLayoutType(): LayoutType { return if (this == "grid") LayoutType.GRID else if (this == "column") LayoutType.COLUMN else LayoutType.ROW } enum class LayoutType(val value: String) { GRID("grid"), COLUMN("column"), ROW("row") } Unifying component types with the UiComponent interface
  21. Component Design Systems @Serializable data class TextUi( val text: String,

    val size: Int, val fontWeight: String ) : UiComponent @Composable fun ConsumeTextUi( textUi: TextUi, modifier: Modifier = Modifier ) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.textHighEmphasis, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) }
  22. Component Design Systems @Serializable data class TextUi( val text: String,

    val size: Int, val fontWeight: String ) : UiComponent @Composable fun ConsumeTextUi( textUi: TextUi, modifier: Modifier = Modifier ) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.textHighEmphasis, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) } val mockTextUi1 = TextUi( text = "Title", size = 32, fontWeight = "bold" ) ConsumeTextUi( textUi = mockTextUi1 ) High reusability. 1. Easy to write previews 2. Easy to write UI tests 3. Suitable for server-driven development
  23. Component Design Systems @Serializable data class ImageUi( val url: String,

    val size: DpSizeUi = DpSizeUi(0, 0), val scaleType: String, val contentDescription: String = "", ) : UiComponent @Composable fun ConsumeImageUi( imageUi: ImageUi, modifier: Modifier = Modifier, imageOptions: ImageOptions? = null ) { GlideImage( modifier = modifier.size(imageUi.size), imageModel = { imageUi.url }, imageOptions = imageOptions ?: ImageOptions( contentScale = imageUi.scaleType.toContentScale(), contentDescription = imageUi.contentDescription ), previewPlaceholder = painterResource(R.drawable.preview) ) }
  24. @Serializable data class TimelineTopUi( val banner: ImageUi ): UiComponent @Serializable

    data class TimelineCenterUi( val title: TextUi, val list: ListUi ): UiComponent @Serializable data class TimelineBottomUi( val title: TextUi, val list: ListUi ): UiComponent Component Design Systems @Serializable data class TimelineUi( val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent Composes a series of responses with identical UiComponents from the top to the bottom level objects.
  25. Component Design Systems @Serializable data class TimelineUi( val version: Int,

    val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow<List<UiComponent>> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() @Serializable data class TimelineTopUi( val banner: ImageUi (UiComponent) ): UiComponent @Serializable data class TimelineCenterUi( val title: TextUi, (UiComponent) val list: ListUi (UiComponent) ): UiComponent @Serializable data class TimelineBottomUi( val title: TextUi, (UiComponent) val list: ListUi (UiComponent) ): UiComponent UiComponent.buildUiComponentList()
  26. Component Design Systems @Serializable data class TimelineUi( val version: Int,

    val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow<List<UiComponent>> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent TimelineUi top center bottom
  27. Component Design Systems @Serializable data class TimelineUi( val version: Int,

    val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow<List<UiComponent>> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent List<UiComponent> top center bottom TimelineUi ImageUi TextUi List<imageUi> TextUi List<imageUi>
  28. Component Design Systems @Serializable data class TimelineUi( val version: Int,

    val top: TimelineTopUi (OrderedUiComponent), val center: TimelineCenterUi (OrderedUiComponent), val bottom: TimelineBottomUi (OrderedUiComponent) ) : UiComponent val timelineUi: StateFlow<List<UiComponent>> = repository.fetchTimelineUi() .flatMapLatest { result -> flowOf(result.getOrNull()?.buildUiComponentList()) } .filterNotNull() .stateIn() UiComponent OrderedUiComponent OrderedUiComponent OrderedUiComponent UiComponent UiComponent UiComponent UiComponent UiComponent @Serializable sealed interface OrderedUiComponent : UiComponent { val order: Int } sortedBy { .. }
  29. Component Design Systems @Composable fun UiComponent.Consume( modifier: Modifier = Modifier,

    ) { when (this) { is TextUi -> ConsumeTextUi( textUi = this, ) is ImageUi -> ConsumeImageUi( imageUi = this, modifier = modifier, ) is ListUi -> ConsumeList( listUi = this, modifier = modifier, ) else -> ConsumeDefaultUi( uiComponent = this, ) } } Column( modifier = Modifier ) { timelineUi.components.forEach { uiComponent -> UiComponent.Consume() } } UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent UiComponent List<UiComponent> • After serializing the response, all components are abstracted into a single UiComponent interface • Consume the given UiComponent list using Column/Row/LazyList, etc.
  30. Component Design Systems @Composable fun Timeline(..) { val timelineUi by

    timelineViewModel.timelineUi.collectAsStateWithLifecycle() Column( modifier = Modifier .fillMaxSize() .padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { timelineUi.components.forEach { uiComponent -> uiComponent.Consume() } } }
  31. The Other Screens @Composable fun PostDetails( detailsUi: ScreenUi ) {

    Column( modifier = Modifier .background(ServerDrivenTheme.colors.background) .fillMaxSize() .padding(12.dp) .verticalScroll(state = rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp) ) { detailsUi.components.forEach { component -> component.Consume() } } }
  32. Action Handler { "banner": { "url": "https://github.com/skydoves", "size": { "width":

    0, "height": 250 }, "scaleType": "crop", "handler": { "type": "click", "actions": { "navigate": "to" } } } } Handling actions for navigation, deep links, etc. Define actions for components such as click, touch, etc.
  33. Action Handler @Serializable data class Handler( val type: String, val

    actions: Map<String, String> ) enum class HandlerType(val value: String) { CLICK("click") } enum class HandlerAction(val value: String) { NAVIGATION("navigation") } enum class NavigationHandler(val value: String) { TO("to") } @Serializable data class ImageUi( val url: String, val handler: Handler? = null ) : UiComponent • Since action handlers may not exist depending on the component, it is optional. • It is also a good idea to define them in a top-level abstraction interface such as UiComponent.
  34. Action Handler @Composable fun ConsumeImageUi( imageUi: ImageUi, modifier: Modifier =

    Modifier, navigator: (UiComponent) -> Unit = {}, imageOptions: ImageOptions? = null ) { GlideImage( modifier = modifier .consumeHandler( handler = imageUi.handler, navigator = { navigator.invoke(imageUi) } ) .size(imageUi.size) .clip(RoundedCornerShape(8.dp)), .. } @Composable fun Modifier.consumeHandler( handler: Handler?, navigator: () -> Unit ): Modifier { if (handler == null) return this handler.actions.forEach { element -> val action = if (element.key == HandlerAction.NAVIGATION.value && element.value == NavigationHandler.TO.value ) { { navigator } } else { {} } val newModifier = if (handler.type == HandlerType.CLICK.value) { Modifier.clickable { action.invoke() } } else { Modifier } then(newModifier) } return this
  35. Component Versioning New Concerns • What if the design system

    specifications change significantly? • What if different designs need to be reflected for each app version? • What if multiple types of components are needed for flexible processing of events and rewards?
  36. Component Versioning { "timeline": { "version": 1, "top": { ..

    } } } @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent
  37. Component Versioning { "timeline": { "version": 1, "top": { ..

    } } } @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent enum class UiVersion(val value: Int) { VERSION_1_0(1), VERSION_2_0(2); companion object { fun toUiVersion(value: Int): UiVersion { return when (value) { VERSION_1_0.value -> VERSION_1_0 VERSION_2_0.value -> VERSION_2_0 else -> throw RuntimeException("undefined version!") } } } } Setting the versioning scope of the design system Depending on the situation, you can process versions by screen or by component.
  38. Component Versioning @Composable fun ConsumeTextUi( version: UiVersion, textUi: TextUi, modifier:

    Modifier = Modifier ) { if (version == UiVersion.VERSION_1_0) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.textHighEmphasis, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) } else if (version == UiVersion.VERSION_2_0) { Text( modifier = modifier, text = textUi.text, color = ServerDrivenTheme.colors.primary, fontSize = textUi.size.sp, fontWeight = textUi.fontWeight.toFontWeight() ) } }
  39. Component Versioning @Composable fun UiComponent.Consume( modifier: Modifier = Modifier, version:

    UiVersion = UiVersion.VERSION_1_0, navigator: (UiComponent) -> Unit = {} ) { when (this) { is TextUi -> ConsumeTextUi( textUi = this, modifier = modifier, version = version ) is ImageUi -> ConsumeImageUi( imageUi = this, modifier = modifier, version = version, navigator = navigator ) .. } }
  40. Fallback New Concerns • What if incorrect layout information is

    received? ◦ Non-existent component version information ◦ Layout data with errors • What if fetching layout information fails?
  41. Fallback @Composable fun UiComponent.Consume( modifier: Modifier = Modifier, version: UiVersion

    = UiVersion.VERSION_1_0, navigator: (UiComponent) -> Unit = {} ) { when (this) { is TextUi -> ConsumeTextUi( textUi = this, modifier = modifier, version = version ) .. else -> ConsumeDefaultUi( uiComponent = this, version = version ) } } Appropriate fallback handling is required when component matching fails for various reasons, such as serialization failure and data corruption.
  42. Fallback When applying component versioning, appropriate fallback handling is required

    to prevent incorrect version information from being passed down due to mistakes by PMs or backend developers. enum class UiVersion(val value: Int) { VERSION_1_0(1), VERSION_2_0(2); companion object { fun toUiVersion(value: Int): UiVersion { return when (value) { VERSION_1_0.value -> VERSION_1_0 VERSION_2_0.value -> VERSION_2_0 else -> throw RuntimeException("undefined version!") or else -> VERSION_1_0 } } } } fun String.toLayoutType(): LayoutType { return if (this == "grid") LayoutType.GRID else if (this == "column") LayoutType.COLUMN else LayoutType.ROW }
  43. Recap: Advantages Advantages of Server-Driven UI • Faster Feature Experimentation:

    New features or layouts can be modified and deployed without needing app updates or going through the review process, leading to faster feedback loops and quicker iterations. This benefits both product managers and mobile developers by reducing repetitive work. • Consistent UI: By establishing a stable component design system, it ensures a consistent UI and behavior across various app versions, as long as the core specifications remain the same. • Native Performance: While offering the flexibility of web apps, Server-Driven UI maintains native-level performance for rendering components, which is superior to typical web-based solutions. • Reduced Burden on Mobile Developers: Developers can focus on "how to present" while the backend dictates "what to present," simplifying the overall development process and reducing workload.
  44. Recap: Downside Disadvantages of Server-Driven UI • Increased Latency: Since

    the client has to fetch layout information from the backend, rendering and displaying components may take longer compared to data-driven UI, potentially impacting user experience. • Higher Complexity and Costs: The entire team must clearly define roles and responsibilities for rendering layout data and managing component versions. Without this, the backend team may bear an extra burden, or overall communication costs between teams could rise. • Fallback Handling: Since layout data is generated across multiple teams (PMs, designers, and backend), there’s a risk of data issues. Proper fallback mechanisms must be in place to handle any defects in the layout data.
  45. Recap: Suitable Case Situations where Server-Driven UI is suitable •

    Home screen: When applied to screens that are first exposed to the user when entering the app, such as the home (feed, timeline) screen, and that require frequent changes, it can be very effective. • Screens with high session time: When applied to screens where the user stays in the app for the longest session time, such as live broadcasts and entertainment, it can be more effective.
  46. Thank you! Lead Android Developer Advocate @ GetStream Jaewoong Eum

    Google Developer Expert skydoves @github_skydoves