Android Server Driven UI-XML VS Compose Example & Benchmark

İbrahim Ethem Şen
8 min readMay 21, 2023

--

In our Android applications, we mostly use static layouts. The screens consist of fixed screens or animations designed according to the user interface. Data in the views is displayed using a type such as JSON obtained from a database. What if a JSON or similar file containing not only data but also the visual aspects such as colors, sizes, etc. for the user interface were to be received?

This situation is commonly referred to as Server-Driven or Dynamic UI. Views are dynamically prepared using JSON received from the server. This allows us to change the UI during the day or in real-time without releasing a new version. By doing so, we can speed up the development process and gain efficiency for our application.

In our applications, we have the option to build the entire structure ourselves or utilize libraries. Here are two examples of libraries

  1. Epoxy — Airbnb
  2. Litho — Facebook

I tried to prepare an example by simplifying it without using an external library. I wrote the example separately in XML and Compose. The purpose here was to evaluate performance and see which one could be more convenient to use. Let’s get started.

In our applications, we typically use standard views to create interfaces.
Let’s create containers by grouping the views we will use throughout the application, and use them universally Let’s create three containers: AppBar, Activity, and Mock, which we will use throughout the application.

The json coming from the server contains the necessary parameters for our containers. To keep it simple, I set them as parameters such as textSize, textColor or icon images. There is a type (view_type) that comes first from the json. This tells us which container is coming. According to this we learn which one to draw. Here I used ViewTypes in RecyclerView to make the layout easier. You can also write it custom or programmatically. Of course, this will change the way you process the Json and manage the drawing of the views.

Part of the code in RecyclerView looks like this

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
viewTypeList[position].apply {
when (viewType) {
ID_APP_BAR -> {
(holder as AppbarViewHolder).bind(containerAppbar)
}

ID_ACTIVITY -> {
(holder as ActivityViewHolder).bind(containerActivity, userListActivity)
}

ID_MOCK -> {
(holder as MockViewHolder).bind(containerMock)
}
}
}
}

I have a separate ViewHolder for each of my containers. Inside the ViewHolders I have models to get the necessary parameters.

data class ContainerActivity(
@SerializedName("container_activity_title")
val title : String = String.DEF_STRING,
@SerializedName("container_activity_title_size")
val titleSize : Float = Float.DEF_FLOAT,
@SerializedName("container_activity_title_color")
val titleColor : String = String.DEF_STRING,
@SerializedName("container_activity_end_title")
val endTitle : String = String.DEF_STRING,
@SerializedName("container_activity_end_click")
val endClick : String = String.DEF_STRING,
@SerializedName("container_activity_item")
val adapterActivity : AdapterActivity = AdapterActivity()
)

/** Tips
* Metehan Bolat @metehanbolatt
* */
val String.Companion.DEF_STRING by lazy { "def_string" }
val Int.Companion.DEF_INT by lazy { 0 }
val Boolean.Companion.DEF_BOOLEAN by lazy { false }
val Float.Companion.DEF_FLOAT by lazy { 0f }

I usually use default values in data classes in my projects. I gave default values in the examples here. Here I used default values with a method shown by Metehan. If there is a value other than the default values, I apply them to my views. In this case, it allows the containers to be created differently. For example, let’s change the views a bit.

When there are 2 Appbar and Mock Container in the incoming Json, our view on the left is formed. When there are 2 Activity Containers, our view on the right is formed and we do not make any changes in the meantime. All we do is to parse the previously prepared json and give the appropriate json to our application that creates the views.

Notice that we are not talking about the data coming with the views. I get the view and data separately in the application. You can make this happen together with various changes.

In the json that comes with UI, “price_size” writes textSize while in the json that comes with data, “product_price” writes the price of the product. The combination of the two json creates our application that the user will interact with. In order to process the json and create the view, I had to use a lot of code in XML in Adapter. Here, along with ViewHolder codes, Extension functions or private functions were included. My code for UI and Data to create each product was as follows.

class AdapterActivityViewHolder(private val binding: AdapterItemActivtyBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindUi(activityUi: AdapterActivity) {
binding.apply {
aiActivityProductTitleTv.textSize = activityUi.productTitleSize
aiActivityProductTitleTv.setUiTextColor(activityUi.productTitleColor)
aiActivityProductPriceTv.textSize=activityUi.productPriceSize
aiActivityProductPriceTv.setUiTextColor(activityUi.productPriceColor)
aiActivityProductReduction.textSize = activityUi.productReductionTitleSize
}
}
fun bindUser(activityItem: ItemActivity) {
binding.apply {
aiActivityProductTitleTv.text = activityItem.productName
aiActivityProductPriceTv.text = activityItem.productPrice.toString()
aiActivityProductReduction.text = activityItem.productReduction
aiActivityProductReduction.setUiTextColor(activityItem.productReductionColor,activityItem.productReductionBg)
aiActivityProductFavoriteIv.visibility = if (activityItem.productFavorite) View.VISIBLE else View.GONE
aiActivityProductIv.networkLoadImage(activityItem.productImage)
}
}
}

Now we will look at how to do this on the Compose side. A Composable can be a view or it can cover the whole screen. I was able to do this with so little code that I can show all the code in Compose. Instead of using ViewTypes in RecyclerView, I have a LazyColumn

//Json
val homeScreenUi by viewModel.homeScreenUi

LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(homeScreenUi) {
when (it.viewType) {
"container_appbar" -> {
ContainerAppbarView()
}

"container_activity" -> {
ContainerActivityView(it.containerActivity, activityList)
}

"container_mock" -> {
ContainerMockView()
}
}
}
}

I use exactly the same structure as XML. I create the container in Column according to the ViewType that comes in the Json list. The Container structure that I created products for the ActivityViewHolder above is as follows

@Composable
fun ItemActivityView(activityItem: ItemActivity, containerActivity: ContainerActivity) {
Card(
modifier = Modifier
.width(140.dp)
.height(240.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
AsyncImage(
model = activityItem.productImage,
contentDescription = "",
Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)

if (activityItem.productFavorite) Icon(
painter = painterResource(id = AppDrawable.ic_star),
contentDescription = "",
modifier = Modifier.padding(8.dp)
)
}
Text(
text = activityItem.productReduction,
color = activityItem.productReductionColor.setTextColor(),
modifier = Modifier
.background(activityItem.productReductionBg.setTextColor())
.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text(
text = activityItem.productName,
color = containerActivity.adapterActivity.productTitleColor.setTextColor(),
fontSize = containerActivity.adapterActivity.productTitleSize.sp,
)
Text(
text = activityItem.productPrice.toString(),
color = containerActivity.adapterActivity.productPriceColor.setTextColor(),
fontSize = containerActivity.adapterActivity.productPriceSize.sp
)
}
}
}

There are some things we need to pay attention to, I can give some values directly. For example, I need a Float value for textSize. I can give it directly to Text. For Color, I can handle it with a small operation. When we write other Containers, we actually write all the UI codes on the compose side. In this case, we can say that we are using Server Oriented UI more easily with Compose.

In our scenario, we drew a screen with 3 Containers. Server oriented architecture is generally used in User specific interfaces. For example, Containers to be shown in events held in different countries. While there is a Container with the theme of May 19 in Turkey, a Container with a different theme can be shown in Azerbaijan. Another example can be the areas where applications are shown according to the user’s area of interest. The question may come to our minds, what if we want to make more than one screen? Let’s address this question again at the end of the article.

Although Compose makes it easier to create views, it’s not only because of the low code and ease of use for developers. Is there a difference in performance here?

I wrote benchmark tests on the samples I prepared. The tests were done on Pixel XL API 32. My benchmark test is as follows

//Compose
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.ibrahimethemsen.serverdrivencompose",
metrics = listOf(StartupTimingMetric(),FrameTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
val column = device.findObject(By.res("containerList"))
val container = Until.hasObject(By.res("containerActivity"))
column.wait(container, 5_000)
column.setGestureMargin(device.displayWidth / 5)
repeat(3) { column.fling(Direction.DOWN) }
device.waitForIdle()
}

//XML
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.ibrahimethemsen.serverdrivenxml",
metrics = listOf(StartupTimingMetric(),FrameTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
val recycler = device.findObject(By.res(packageName, "home_product_rv"))
val container = Until.hasObject(By.res("container_activity_constraint"))
recycler.wait(container, 5_000)
recycler.setGestureMargin(device.displayWidth / 5)
repeat(3) { recycler.fling(Direction.DOWN) }
device.waitForIdle()
}

In the tests, we simply open the app, scroll and repeat. The results of the tests were as follows

  • timeToInitialDisplayMs : Represents the time from the start of the application to the first display on the screen.
  • frameDurationCpuMs : Shows how much time the processor spends for each frame.
  • frameOverrunMs : Represents the cases where the time required to render the frame exceeds the time required to render the frame in time.

When we look at the results, timeToInitialDisplayMs data shows us that the Compose application starts faster. Minus values in frameDurationCpuMs and frameOverrunMs data show that there may be delays while others are processed on time. The fact that Compose is higher than XML in P99 shows that there may be problems, stuttering, etc. in animations. Still, in general, we can say that Compose provides a good result for our example in terms of both code and performance.

To summarize, the views such as data in the server-oriented UI are provided with Json. On the client side, the json file is processed to create the views. In this way, we can make changes to the UI instantly by skipping version update, play store… processes. Let’s look at our question “what if we want to make more than one screen”

When we prepare Json correctly and process it on the Client side, we can create applications with multiple screens on the XML and Compose side. For Compose, this situation is much simpler, we can do this quickly with the right state management. On the XML side, when we manage the Fragment with ID and restart its LifeCycle, we can create the application like multiple screens with an Activity and a Fragment. Here, while performing operations with Composables on the Compose side in user interaction, we actually enable the Fragment to restart itself on the XML side. Of course, this is a method I have implemented, if you have a better solution, I would like to listen.

For comments, suggestions or criticisms, you can reach us on LinkedIn or Twitter.

Repos of the examples are available on Github.

--

--