Building a Server-Driven UI for Android with a Custom Renderer

Sumeet Panchal
5 min readJul 26, 2024

--

In today’s fast-paced development environment, the ability to update your app’s user interface (UI) dynamically without releasing a new version can be precious. This is where a Server-Driven UI comes into play. In this blog, we will explore how to create a Server-Driven UI using a custom renderer and JSON schema. We’ll cover both the Android client-side implementation and the server-side code to push the JSON schema.

What is a Server-Driven UI?

A server-driven UI allows you to define and update your app’s UI using a JSON schema that is sent from the server. This means that changes to the app’s layout and components can be made without updating the app itself. The client app fetches the JSON schema from the server and renders the UI accordingly.

Defining the JSON Schema

The JSON schema is a crucial part of this approach, as it defines the structure and properties of the UI components. Here’s a simple example of a JSON schema:

Example JSON Schema

{
"type": "LinearLayout",
"orientation": "vertical",
"padding": "16dp",
"backgroundColor": "#FFFFFF",
"children": [
{
"type": "TextView",
"properties": {
"id": "welcomeText",
"text": "Welcome to Custom UI Renderer!",
"textSize": 18,
"textColor": "#000000",
"margin": "8dp"
}
},
{
"type": "ImageView",
"properties": {
"id": "headerImage",
"src": "https://example.com/image.png",
"width": "match_parent",
"height": "wrap_content",
"scaleType": "centerCrop",
"margin": "8dp"
}
},
{
"type": "Button",
"properties": {
"id": "actionButton",
"text": "Click Me",
"backgroundColor": "#FF6200EE",
"textColor": "#FFFFFF",
"textSize": 16,
"padding": "12dp",
"onClick": "handleButtonClick",
"margin": "8dp"
}
}
]
}

Explanation

type: Specifies the type of layout or component (e.g., LinearLayout, TextView, Button).

orientation: Defines the orientation for layouts (vertical or horizontal).

padding: Sets padding for the layout.

backgroundColor: Defines the background color.

children: Contains an array of child components.

  • properties: Specifies attributes for each component (e.g., id, text, src, textSize).

Implementing the UI Renderer in Android

We’ll create a custom UI renderer that parses the JSON schema and renders the corresponding Android views.

1. Create a Utility Class to Parse JSON Schema

Create a new Kotlin file named UIRenderer.kt and add the following code:

import android.content.Context
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.ImageView
import android.widget.Button
import org.json.JSONObject
import android.util.TypedValue
import android.graphics.Color

object UIRenderer {

fun render(context: Context, jsonSchema: JSONObject): View {
return parseView(context, jsonSchema)
}

private fun parseView(context: Context, jsonObject: JSONObject): View {
val viewType = jsonObject.getString("type")
val properties = jsonObject.optJSONObject("properties")
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)

when (viewType) {
"LinearLayout" -> {
val layout = LinearLayout(context)
layout.orientation = if (jsonObject.optString("orientation") == "horizontal") {
LinearLayout.HORIZONTAL
} else {
LinearLayout.VERTICAL
}
layout.setPadding(
dpToPx(context, jsonObject.optString("padding", "0dp")),
dpToPx(context, jsonObject.optString("padding", "0dp")),
dpToPx(context, jsonObject.optString("padding", "0dp")),
dpToPx(context, jsonObject.optString("padding", "0dp"))
)
layout.setBackgroundColor(Color.parseColor(jsonObject.optString("backgroundColor", "#FFFFFF")))

val children = jsonObject.optJSONArray("children")
if (children != null) {
for (i in 0 until children.length()) {
val childJson = children.getJSONObject(i)
val childView = parseView(context, childJson)
layout.addView(childView)
}
}
return layout
}
"TextView" -> {
val textView = TextView(context)
textView.text = properties?.getString("text") ?: ""
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, properties?.optDouble("textSize", 14.0)?.toFloat() ?: 14f)
textView.setTextColor(Color.parseColor(properties?.optString("textColor", "#000000")))
val margin = dpToPx(context, properties?.optString("margin", "0dp") ?: "0dp")
textView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(margin, margin, margin, margin)
}
return textView
}
"ImageView" -> {
val imageView = ImageView(context)
val src = properties?.getString("src")
if (src != null) {
// For simplicity, we are not loading images here. Consider using an image loading library.
// imageView.setImageURI(Uri.parse(src))
}
imageView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
return imageView
}
"Button" -> {
val button = Button(context)
button.text = properties?.getString("text") ?: ""
button.setBackgroundColor(Color.parseColor(properties?.optString("backgroundColor", "#FF6200EE")))
button.setTextColor(Color.parseColor(properties?.optString("textColor", "#FFFFFF")))
button.setTextSize(TypedValue.COMPLEX_UNIT_SP, properties?.optDouble("textSize", 14.0)?.toFloat() ?: 14f)
button.setPadding(
dpToPx(context, properties?.optString("padding", "0dp") ?: "0dp"),
dpToPx(context, properties?.optString("padding", "0dp") ?: "0dp"),
dpToPx(context, properties?.optString("padding", "0dp") ?: "0dp"),
dpToPx(context, properties?.optString("padding", "0dp") ?: "0dp")
)
val margin = dpToPx(context, properties?.optString("margin", "0dp") ?: "0dp")
button.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(margin, margin, margin, margin)
}
// Handle button click
button.setOnClickListener {
val onClickAction = properties?.getString("onClick")
if (onClickAction != null) {
// Handle action based on the value of `onClick`
}
}
return button
}
else -> throw IllegalArgumentException("Unknown view type: $viewType")
}
}

private fun dpToPx(context: Context, dp: String): Int {
val density = context.resources.displayMetrics.density
return (dp.replace("dp", "").toFloat() * density).toInt()
}
}

2. Fetch and Render the UI in Your Activity

Modify your MainActivity to use the UIRenderer:

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.json.JSONObject

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Fetch the UI schema and render it
runBlocking {
launch {
val uiSchema = fetchUISchemaFromServer()
val rootView = UIRenderer.render(this@MainActivity, JSONObject(uiSchema))
setContentView(rootView)
}
}
}

private suspend fun fetchUISchemaFromServer(): String {
return withContext(Dispatchers.IO) {
// Replace this with your actual network request to fetch the JSON schema
// For demonstration purposes, returning a hardcoded JSON schema
"""{
"type": "LinearLayout",
"orientation": "vertical",
"padding": "16dp",
"backgroundColor": "#FFFFFF",
"children": [
{
"type": "TextView",
"properties": {
"id": "welcomeText",
"text": "Welcome to Custom UI Renderer!",
"textSize": 18,
"textColor": "#000000",
"margin": "8dp"
}
},
{
"type": "ImageView",
"properties": {
"id": "headerImage",
"src": "https://example.com/image.png",
"width": "match_parent",
"height": "wrap_content",
"scaleType": "centerCrop",
"margin": "8dp"
}
},
{
"type": "Button",
"properties": {
"id": "actionButton",
"text": "Click Me",
"backgroundColor": "#FF6200EE",
"textColor": "#FFFFFF",
"textSize": 16,
"padding": "12dp",
"onClick": "handleButtonClick",
"margin": "8dp"
}
}
]
}"""
}
}
}

Explanation

runBlocking and launch: Used for coroutine operations to fetch the UI schema from the server. runBlocking ensures that the coroutine completes before proceeding.

fetchUISchemaFromServer: This method simulates a network request to fetch the JSON schema. Replace the hardcoded JSON string with your actual network request logic.

UIRenderer.render: Calls the UIRenderer utility to parse the JSON schema and generate the appropriate Android view hierarchy.

  • setContentView(rootView): Sets the dynamically created view hierarchy as the content view of the activity.

Server-Side Code to Push JSON Schema

For the server-side implementation, we’ll use a simple Node.js server to serve the JSON schema.

Server Implementation with Node.js

1. Setup Your Node.js Project

mkdir server-driven-ui
cd server-driven-ui
npm init -y
npm install express

2. Create the Server File

Create a file named server.js:

const express = require('express');
const app = express();
const port = 3000;

app.get('/ui-schema', (req, res) => {
res.json({
"type": "LinearLayout",
"orientation": "vertical",
"padding": "16dp",
"backgroundColor": "#FFFFFF",
"children": [
{
"type": "TextView",
"properties": {
"id": "welcomeText",
"text": "Welcome to Custom UI Renderer!",
"textSize": 18,
"textColor": "#000000",
"margin": "8dp"
}
},
{
"type": "ImageView",
"properties": {
"id": "headerImage",
"src": "https://example.com/image.png",
"width": "match_parent",
"height": "wrap_content",
"scaleType": "centerCrop",
"margin": "8dp"
}
},
{
"type": "Button",
"properties": {
"id": "actionButton",
"text": "Click Me",
"backgroundColor": "#FF6200EE",
"textColor": "#FFFFFF",
"textSize": 16,
"padding": "12dp",
"onClick": "handleButtonClick",
"margin": "8dp"
}
}
]
});
});

app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

3. Run the Server

node server.js

Now, your Android app will fetch the UI schema from http://localhost:3000/ui-schema and render it accordingly.

Conclusion

In this blog, we demonstrated how to build a server-driven UI using a custom JSON schema and renderer in Android. We created a simple Node.js server to provide the JSON schema to the Android client. This approach enables dynamic and flexible UI updates without needing to release a new app version.

Feel free to adapt and extend this example based on your needs, adding more component types, and properties, and handling more complex UI scenarios.

Happy coding!

This blog post provides a complete guide on creating and implementing a server-driven UI using a custom renderer and JSON schema. If you have any more questions or need further clarification, feel free to ask!

npm install express

--

--

Sumeet Panchal
Sumeet Panchal

Written by Sumeet Panchal

Programming enthusiast specializing in Android and React Native, passionate about crafting intuitive mobile experiences and exploring innovative solutions.

No responses yet