Tuesday, December 20, 2022

Declarative UI and UDF with Android Databinding in XML

Introduction

I know: A lot of buzzwords. Except Databinding, which is kind of the opposite. The thing is I've been a big fan of Android Databinding since its release in 2016 because I think it did good for a lot of the architectural issues that Android was suffering from. When Jetpack Compose came along people praised it for many good reasons but also for wrong ones. The word "declarative" was thrown around a lot, and conversely "imperative" when talking about legacy Android XML (example sources: ([1], [2], [3]). I felt that we were given the option to do declarative UI architecture already with Android Databinding but everyone seemed to have missed. Was I crazy?

With the example project shown below I wanted to prove to myself that declarative UI could be done with Android XML Databinding. As a developer with "senior" in the title, I'm obliged to truly understand the meaning behind the buzzwords in the title of this post. If you too are an Android developer feeling the slightest uncertainty about the meaning of those, please grant yourself a couple of minutes to read and understand as well. And if I've missed anything or misunderstood I'm happy to be corrected.

What I made is a a proof of concept app that illustrates how Android Databinding with XML can be declarative - simply by using a declarative ViewModel written for compose and applying that to a databound XML view. I will go in depth about that in a second - but first a bit more about the terms "declarative" and "imperative":

Making XML with Databinding declarative

First of all, there's nothing inherent in XML combined with Java/Kotlin that makes it imperative. The language expressing the UI does not define the UI paradigm. It's how you use that language. I think StackOverflow is a good source for finding the consensus here. I'm not saying "truth" because I'm not sure there is a single truth. Some say it is a spectrum and not a binary definiton.

Anyway, this accepted answer states:

The term is more often used for UI frameworks with a strict separation of the look of the UI from the behavior, which means the code reacting to UI events. For example, using XAML, you declare the look of your UI in a specific XML dialect, but you implement the behavior in separate program code.

Notice how he uses XAML as an example here. XAML is XML, just as Android's legacy view system is. It's not the markup lanugage, is the fact that we often use events to procedurally mutate dependent views that make it declarative. Oh and separation of looks from behaviour? Definitely well separated. They're even in different langauges as opposed to Compose.

The other answer is good as well, as it highlights that it's about describing WHAT instead of HOW. A view that's repeated in this blog post from Luns Muema.

Declarative UI is a UI that's designed in a declarative way (you describe what it should be like) rather than an imperative way (you code the steps to create it.)

The definition used by the Flutter documentation (which is a declarative UI framework) also resonnates surprisingly well with Databinding in XML if you use it the way I do here.

Here, rather than mutating an old instance b when the UI changes, Flutter constructs new Widget instances. The framework manages many of the responsibilities of a traditional UI object (such as maintaining the state of the layout) behind the scenes with RenderObjects. RenderObjects persist between frames and Flutter’s lightweight Widgets tell the framework to mutate the RenderObjects between states. The Flutter framework handles the rest.
As in Compose and in this example, Flutter uses immutable state. The framework, Flutter, constructs new widget instances. In our case, the Databinding framework, constructs new widget instances or mutates existing.

I'll take one more example: This blog post by Kaan Enes states

A declarative UI pattern defines the UI as a function of the “state.” A “state” is nothing but a variable or an object that holds a value, which can change based on user input or other business logic. When the state changes, the framework will redraw the UI
Again, this is exactly what happens with my immutable state bound to data in XML below.

What I did

So let's get one thing straight first. Android Databinding out of the box is not inherently declarative. I had to come up with a set of rules you need to follow to get both a 100% declarative pattern and unidirectional data flow (UDF) working:

  1. One-way databinding only. The second we introduce two-way binding we mutate view state.
  2. Never write logic in your XML binding. Instead bind to a single property from the ViewModel.
  3. Your view state must be immutable
  4. Your view model must expose one single observable field: The view state

For point 1 this means that you have to stick to @{viewModel.prop} only, never the @={}-syntax. So for instance for EditText's where it would be natural to use two-way binding for the text property in traditional Android Databinding, you'd instead have to bind to the event onTextChanged and let the ViewModel create a new state from that.

To elaborate on number 2: Not under any circumstance should you write expressions in XML:

  • android:text="@{state.name}" is cool.
  • android:visibility="@{state.personList != null && state.personList.count > 0}" is horrible and you should take a four month unpaid leave to contemplate about your life choices.
This is a good rule even if you don't go for declarative UI. It simply makes the code more testable.

Point 3 and 4 from above should be familiar if you've done Compose.

This diagram illustrates the setup:

If you've written compose apps you should be familiar with differentiating between ViewModel and ViewState:

  • The ViewModel is the Kotlin file that inherits from androidx.lifecycle.ViewModel and has the responsibility of changing the state. "Changing" means copying the state and changing one or more properties. You may bind events to the ViewModel but not properties.
  • The ViewState is an immutable Kotlin class that represents whatever changable state you see in the view. This is where you bind your XML properties to.

So this sets the stage for declarative UI where the state defines the UI completely and you can do cool stuff like state history playback.

Unidirectional Data Flow

UDF is an important concept in Compose. You need to understand that properly in order to succeed with declarative architecture with Databinding. The basics is that events bubble up and state trickle down in the view hierarchy. Let's take this trivial example:

You have a password input field and a "Continue" button. Whenever the password field is empty the Continue button should be disabled. Once a single character is entered, the buttons should enable. Illustrated:

With UDF and declarative programming you do not let the EditText alter the button state directly. Nor do you let the Button read the state of the EditText to determine its enabled-state (although that may comply to declarativism but not UDF?). Instead you let the textChanged-event bubble up to the ViewModel, which alter the state object and passes it down to the button.

Example project

Github link: https://github.com/Nilzor/declarative-databinding

The example project I wrote is a bit more complicated. It is the sketch of an app selling public transport tickets with an arbitrary number of traveler type categories. I've set the data to have three categories: Adult, Child and Bicycle. You enter the number of each category by using increase/decrease buttons and get a total summed at the bottom

The project is implemented both using Jetpack Compose and XML Databinding. It shares a common ViewModel and ViewState. There is just a thin wrapper around the ViewModel to allow the Android Databinding to pick up on changes. I convert the StateFlow to an Observable. You can see that in the file ShoppingCartViewModelDataBinding.

If you look at the ViewState it represents the views pretty much one-to-one:

The challenge here lies in the introduction of the list (RecyclerView for the XML). You need to propagage the event happening at the leaf node when you increase or decrease the traveler count to the top and then alter the "Total count". With Jetpack Compose this is pretty trivial. With Databinding you have to do a bit more grunt work. First of all I use Evan Tatarka's binding-collection-adapter which allows me to databind to list items without writing adapters (more tips here). But I then encountered the issue that the elements in the list do not have access to the root ViewModel for event propagation. It is bound directly to the sub-state that only expose a single list item. There might be several ways to solve this but I chose to bind the event directly to the root Activity, as that is always "exposed" by simply using the legacy android:onClick="handleClick" syntax, which requires of you to write a function in the Activity that takes a single View parameter as input. The activity then passes that event on to the ViewModel. There may be better ways to solve this, but it worked for me. The architecture for Databinding is then a slightly altered:

Example flow

I know you all don't have time to download the example project so here I illustrate a flow of three steps with the state data model alongside screenshots for both the Jetpack Compose rendering and Android XML Databinding:

  • State 1: Add 1 adult
  • State 2: Add 1 child (sum: 1 adult, 1 child)
  • State 3: Add 1 child (sum: 1 adult, 2 children)

STATE 1    
    
PageState(
  productList = [
	  ProductCounterState(
		count = 1
		enableDecrease = true
		name = "Adult"
	  ),
	  ProductCounterState(
		count = 0
		enableDecrease = false
		name = "Child"
	  ),
	  ProductCounterState(
		count = 0
		enableDecrease = false
		name = "Bicycle"
	  )
	]
  totalProductCount = 1
)
    
STATE 2

PageState(
  productList = [
	  ProductCounterState(
		count = 1
		enableDecrease = true
		name = "Adult"
	  ),
	  ProductCounterState(
		count = 1
		enableDecrease = true
		name = "Child"
	  ),
	  ProductCounterState(
		count = 0
		enableDecrease = false
		name = "Bicycle"
	  )
	]
  totalProductCount = 2
)
STATE 3

PageState(
  productList = [
	  ProductCounterState(
		count = 1
		enableDecrease = true
		name = "Adult"
	  ),
	  ProductCounterState(
		count = 2
		enableDecrease = true
		name = "Child"
	  ),
	  ProductCounterState(
		count = 0
		enableDecrease = false
		name = "Bicycle"
	  )
	]
  totalProductCount = 3
)

Compose screenshots

Databinding screenshots

In short: The presentation is correct both for Compose and Databining with a code base that share the exact same ViewModel/ViewState file. There is no noticable lag or flickering in the UI for any of the view implementations.

The example project also contains a bonus rewind / replay functionality for the databinding example for those who want to play around with that. Notice the buttons on the bottom of the activity when starting the Databinding Activity (cropped out of the screenshots). This was trivial to implement by wrapping the ViewModel in a history-holder.

Conclusion

I did this experiment to prove to myself that I understood what declarative UI really was and that my gut feeling that you could do declarative UI in with Android Databinding in XML. I'm not saying that I will use use this pattern going forward or that I recommend others to use it, but you should be aware of the possibility. If your app contains legacy Android XML views with imperative logic and you're looking to rewrite it, it will be just a big of a job to go to Delcarative XML using Databinding as it would be to rewrite it to Jetpack Compose. The latter option would be the best options in most cases, as Jetpack Compose has a bunch of other benefits that I have not dug into here.

Jetpack Compose is awesome, but Android databinding with XML can be awesome too if used correctly.