Android Kotlin DSL configured lists

Peter Marinov
5 min readSep 30, 2019

--

Kronstadt, Kotlin Island, Russia

A long time ago in a galaxy far, far away… It was about that time when the RecyclerView was released and I had just started my first Android job. As with most Android applications lists were a big part of the information displayed.

Back then, I thought about lists as a way to display similar (if not the same) items and that was hard enough. I thought it too complex to create so many files for a single list. An Adapter, LayoutManager and then ViewHolders for every new list — too much work.

Introducing the DiverseRecyclerAdapter

Developed at Trading212 (which I am part of 😛), out of necessity to solve problems and provide long term stable solutions.

The library itself is small but awesomely powerful. So powerful in fact that we’re still finding new ways to leverage it and come up with elegant solutions. In this article, I’ll focus on how it allowed us to use DSL to define one of our main tabs in a human-readable way.

What are DSLs and why should we care?

We always try to write code using the vocabulary of the application domain. In some cases, we can go to the next level and actually program using the vocabulary, syntax, and semantics — the language — of the domain.
- The Pragmatic Programmer

DSL stands for Domain Specific Language, it is a tool in the programmer’s arsenal which helps expressing business rules, objectives and ideas. For me it most of all defines a clear language between the Business(or the one implementing it) and the System. If we view programming as a conversation between the OS and the developer, then, using a language that both parties understand well, is essential. You don’t want to be negotiating peace treaties in a language you don’t know.

DSL configured list

A while back we ran into the problem, that one of the main menu tabs was getting more and more complex in terms of what buttons should be shown. Which of them should float and which should be docked. The screen looked mostly the same, but slightly different depending on the state. It was in those slight differences the pasta began to form 🍝.

Cloudy with a Chance of Meatballs — Spaghetti

We wanted to make the addition of a new feature button, to be as simple as it sounds — buttons += FeatureXButton()

We leveraged Kotlin operator overloading which allowed us to use syntax like:

if (condition) +Button()

Here’s how a menu config would look like:

fun generateMenu(context: Context) =
menuConfig {

header {
hasLogout = true
separatorSetting = SEPARATOR_SETTING_INVISIBLE
userDisplayName = { User.username ?: "" }
}

content {
+ItemSeparatorRecyclerItem()

if (isFeatureXEnabled)
{
+FeatureXRecyclerItem()
+ItemSeparatorRecyclerItem(separatorOffset)
}

if (isFeatureYEnabled)
{
+FeatureYRecyclerItem()
+ItemSeparatorRecyclerItem(separatorOffset)
}
// ... etc
}

if (isDockButtonNeeded)
{
dockItem = DockItem { context, parent ->
LayoutInflater.from(context)
.inflate(R.layout.view_docked_btn, parent, false)
}
}
}

The result is something like this:

Interpreting the config

The parent, which holds the RecyclerView, adapter and header container, calls generateMenu.

val menuConfig = generateMenu(context) 

Compare the new config to the old one (see MenuConfiguration#equals)

if(menuConfig != currentConfig)
{
setNewConfig(menuConfig)
}

A full-blown diff is possible in MenuConfiguration#equals because you have access to the items’ data inside the adapters.

DiffUtil can come in handy.

fun setNewConfig(config: MenuConfig) {
toolbar.apply {
hasLogout = config.hasLogout
separatorSetting = config.separatorSetting
userDisplayName = config.userDisplayName()
}
headerContentHolder.removeAllViews()
config.header.viewControllers.forEach {
it
.addIn(headerContentHolder)
}
...

We’ve set up a simple ViewGroup (headerContentHolder) above the recycler to hold custom views that won't be scrolling with the list.

// We're not using DiffUtil for simplicity
// so we can go ahead and reset the adapter state
adapter.removeAll(false)
adapter.addItems(
newConfig.content.items, // Items defined in the config
true /* notify the adapter */)
currentConfig = newConfig

I’ve skipped a lot of the ceremony around the screen setup, like item click listeners and OnScroll listeners.

Because we’ve set up our UI to be able to interpret this kind of configuration, it is a small step to being able to define the config as a remote resource. This would mean that with a minimal effort the menu can be updated dynamically with no need to release.

DSL Set-Up

We start by creating the main config class.

The header and content are defined as lateinit so the app will crash if the user fails to define them.

So far we have a way to say:

menuConfig {   header { }   content { }
}

A cool thing here is that while in any of the menu “scopes” (header, content) the compiler is providing accurate autocomplete.

These parts are all straightforward when examined separately, but by combining them a very expressive domain language is formed. It allowed us to communicate with the OS like we’re talking to another human being.

If the user has saved his account he can log out — translates into

hasLogout = !isGuestUser

We’ve gone one step further and split the menu variants in different menu configs.

val menuConfig = if(AppIsVariantX) generateMenuX() else generateMenuY()

The whole DSL definition: https://gist.github.com/dragoncodes/41dc2140cdd7efaf243c92789dfdb96c

Conclusion

DSLs are very versatile and provide a lot of leverage for cool solutions.

The output of the DSL doesn’t even have to be used directly. It could be a way to describe a sub-system, and later with the help of code generators such as KotlinPoet, be fully implemented at compile-time.

Another interesting side effect of using a DSL in this way is that it forces the “presentation” layer to only interpret data. This means that data can be swapped at any point with another provider.

The opposite also applies. Since the “presentation” is only interpreting, it could also be swapped (with Kotlin Native for example).

Note
DiverseRecyclerAdapter
is not required to achieve this abstraction via DSL, but it helps a lot.

For a deeper dive into Kotlin DSLs:

--

--