Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

G E T S T R E A M . I O skydoves @github_skydoves Lead Android Developer Advocate @ Stream Jaewoong Eum

Slide 3

Slide 3 text

Approaches to Server-Driven UI

Slide 4

Slide 4 text

Data-Driven UI Client Backend data

Slide 5

Slide 5 text

Data-Driven UI Client Backend data Bind data into components

Slide 6

Slide 6 text

Data-Driven UI Client Backend data Bind data into components Render Data-Driven UI

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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.

Slide 10

Slide 10 text

Web-Driven UI Backend Hybrid App Client data WebView browser Hybrid Application

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

Server-Driven UI Backend domain data + components

Slide 13

Slide 13 text

Server-Driven UI Backend Consume Client Render domain data + components Server-Driven UI

Slide 14

Slide 14 text

Server-Driven UI Backend

Slide 15

Slide 15 text

Server-Driven UI Backend @Composable fun TextUi() { .. } @Composable fun ImageUi() { .. } @Composable fun ListUi() { .. } Client

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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.

Slide 18

Slide 18 text

Firebase Realtime Database

Slide 19

Slide 19 text

Firebase Realtime Database

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Server-Driven UI Consume Client Render domain data + components Server-Driven UI Firebase Realtime Database

Slide 24

Slide 24 text

Server-Driven UI dependencies { implementation(platform("com.google.firebase:firebase-bom:33.2.0")) implementation("com.google.firebase:firebase-database") }

Slide 25

Slide 25 text

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 { return mapOf( "uid" to uid, "author" to author, "title" to title, ..

Slide 26

Slide 26 text

Firebase Realtime Database

Slide 27

Slide 27 text

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( path = { dataSnapshot -> dataSnapshot.child("timeline") }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ).flatMapLatest { result -> .. }

Slide 28

Slide 28 text

Firebase Realtime Database

Slide 29

Slide 29 text

Component Design in Jetpack Compose

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Component Design Systems @Serializable sealed interface UiComponent

Slide 35

Slide 35 text

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 )

Slide 36

Slide 36 text

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, ) : 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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Component Design Systems

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Component Design Systems @Serializable data class TimelineUi( val version: Int, val top: TimelineTopUi, val center: TimelineCenterUi, val bottom: TimelineBottomUi ) : UiComponent val timelineUi: StateFlow> = 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()

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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 ● After serializing the response, all components are abstracted into a single UiComponent interface ● Consume the given UiComponent list using Column/Row/LazyList, etc.

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Action Handler Click

Slide 50

Slide 50 text

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.

Slide 51

Slide 51 text

Action Handler @Serializable data class Handler( val type: String, val actions: Map ) 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.

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Jetpack Compose vs XML

Slide 54

Slide 54 text

Component Versioning & Fallback

Slide 55

Slide 55 text

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?

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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.

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Component Versioning

Slide 61

Slide 61 text

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?

Slide 62

Slide 62 text

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.

Slide 63

Slide 63 text

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 }

Slide 64

Slide 64 text

Recap

Slide 65

Slide 65 text

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.

Slide 66

Slide 66 text

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.

Slide 67

Slide 67 text

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.

Slide 68

Slide 68 text

Open-source & Article github.com/skydoves/server-driven-compose getstream.io/blog/server-driven-compose-firebase

Slide 69

Slide 69 text

Learn Android & Kotlin With Dove Letter!

Slide 70

Slide 70 text

Thank you! Lead Android Developer Advocate @ GetStream Jaewoong Eum Google Developer Expert skydoves @github_skydoves