Bye XML, it was nice knowing you (pt. 2)

Filip Wiesner
MateeDevs
Published in
9 min readApr 9, 2021

This article will focus on the basics of Compose and example of implementation of specific UI design (just looks — no state management nor architecture).
Not everything is explicitly defined for a reason. You should follow along with your own code and not just copy mine :)
If you really don’t want to code along or are just stuck,
here is the full project.
Part 1
here and part 3 here.

In the first part of this series, we’ve learned what is Composable function, how do we write one, how to use basic layouts like Row and Column and what is Modifier.
In this part, we will learn about Material theme, how to write custom Modifiers and how to manage state.
The best way to learn is to just start and see where we’ll end up. Remember that it’s not about the result but the mistakes we’ve made along the way :)

Back to the design

Design by Aris Rahmat Fatoni

Last time we started creating this header, but we only managed to do the location drop-down button in the top left corner of this image.
Our code might look like this:

So to sum up, we have HomeScreen component with just a header and in the Header component we just have a LocationSelector component.

Next, we’ll try to do the location value below the selector. Right away, we can see that it’s pretty much the same. There is an icon with text next to it, but the color is some custom pink, and the text is big and bold.
If we zoom out a little bit and look at the whole design, we can see that the pink color is the most prominent, and in the Android world, we would call it a primary color. Before, we would define such colors in XML file and assign them in the theme where Views from the Material library would access and use them. Let’s see how we can achieve this in Compose world.

Material theme

If you created your project in Android Studio with Compose template, you might already have a theme defined.
MaterialTheme is a Composable function like any other but instead of emitting UI, it provides some values to components inside it. How exactly this works is not important right now, so let’s focus on the theme itself.

If you are interested, read something about CompositionLocals (previously Ambients)

The MaterialTheme function takes 4 arguments: colors, typography, shapes and content. Colors are created with either lightColors() or darkColors() function (depends if isSystemInDarkTheme() is false or true) but in our case, the light colors will suffice.
Our design doesn’t have many colors, so we’ll end up with something like this:

Note that both lightColors() and darkColors() do the same. The only difference is that the default colors have different values, so if you define everything, you can just use Colors() constructor.

MainActivity onCreate function

We will skip typography for simplicity, and I eyeballed the shapes to be RoundedCornerShape with 10, 12 and 16 dp size for small, medium, and large.
Now when we have both colors and shapes, we can wrap our HomeScreen component with the material theme.

With the theme defined, we can access its values from our components by calling MaterialTheme.colors.primary or MaterialTheme.shapes.medium . Take the MaterialTheme function as a “theme context provider,” which will contain all of the theme-related values for styling your components. You can even nest these themes and, for example, define a custom theme for each screen in your app.

Modifiers…again!

Now we know everything needed for that LocationValue component, and it is pretty much the same as the LocationSelector but with the icon on the left (now with our new primary color as a tint).
There is not similar text style in default Typography values, so I’ll leave it up to you if you want to define your own text style in MaterialTheme or if you’ll just hardcode the values here.
We’re not doing anything fancy here, so I’ll stick to the latter option and add 28 sp font size, FontWeight.W500 and FontFamily.SansSerif as parameters to the Text component.

The last thing missing in our header is that notification bell, and it should be pretty straightforward, right? Firstly, we need a white background with padding, the icon, and then the top right notification dot.
We know how to add the icon, but what about the rest? The answer is Modifiers.

Advanced modifier usage
Let’s get one thing out of the way, “the order of modifiers in the chain does matter.” For example, there is a significant difference between Modifier.padding(8.dp).background(Color.White) and Modifier.background(Color.White).padding(8.dp). The first one will apply the padding first and then the background, and the second one will first draw the background and then apply the padding. That means that in the first example, the padding will be outside of the background and in the second example inside the background.
In our case, we’ll need background, padding, and size modifiers (in this order). We can use our defined surface color and medium shape from the Material theme for the background. I’ll leave the padding and size values up to you :)
With that done, let’s get to the harder part.

As in general with programming, there are multiple paths how to achieve one thing, and feel free to experiment for a bit before reading on.
For me, the best approach was to define a custom modifier where we’ll leverage the existing modifier drawWithContent. The way to do this is actually pretty simple. First, we define our withNotificationDot function with Modifier interface as a receiver (optionally with parameter dotRadius: Dp). In the function body, we’ll just call (and return) composed function, which is our gateway into the Modifier chain. This function takes lambda factory parameter, where we will implement our custom modifier behavior.
First, let’s think of what we need. We certainly need the color of the dot and the color of its outer ring, but that’s easy; we’ll just use the primary and surface color from the Material theme as before. Then we’ll need the radius of the dot, and (I’ll spoil the surprise a little) because we’ll draw it on Canvas, we’ll need it in pixels rather than dp. For this conversion, we’ll need density. Finally, we’ll need dimensions of the component to draw the dot in the right place. Luckily both density and size are included in DrawScope which we’ll use.

drawWithContent
Now let’s get down to business and call drawWithContent. We can see that it takes one lambda parameter onDraw with ContentDrawScope receiver, which is just DrawScope with one extra method. This receiver will let us decide when/where we want to draw the component itself. We’ll use this functionality to draw the dot on top of the icon.
Now when we are inside the onDraw function, we can just draw the two circles (dot and outline). I’ll encourage you again to experiment for a little bit before looking at the solution.

Finished? OK, let’s see the code :)

That wasn’t so hard, was it? If we wanted to do this in the View system, we would’ve needed at least 2 XML files (one for the dot drawable and one for the layout where we would use it).

Finishing touches

Now when we have all the components we need, we can finally put them together. Now the question is, what layout will we use? It certainly can’t be just Column or Row. One option is to use Compose version of Constraint Layout, but I would advise against that. Even in compose docs, there is a note about this:

In the View system, ConstraintLayout was the recommended way to create large and complex layouts, as a flat view hierarchy was better for performance than nested views are. However, this is not a concern in Compose, which is able to efficiently handle deep layout hierarchies.

So I would say that sometimes less is more, and I would be more inclined to rather use a combination of rows and columns, which will be more than enough in most cases. But if you know that ConstraintLayout is what you really need, then don’t hesitate to use it.

I think that using a combination of Row and Column is the right way, so let’s use that. The location selector and value are in one column next to the notification bell. I’ll leave the implementation up to you, but I’ll give you a hint: you will probably need fillMaxWidth() modifier and SpaceBetween arrangement.

Moving on: Search

With the header out of the way, let’s get to the second part, the search bar. This will be pretty simple (only ~50 lines of code), but we will get to a new concept: State.

The first thing we will do is to add an OutlinedTextField which is pretty much exactly what is in the design, the only difference being the corner radius. Unfortunately, at the time of writing (compose beta 4), there is still no way to customize the shape of the outline, so the default radius will have to suffice (you can follow this issue).
When we call the OutlinedTextField function, we can notice that two parameters are required: value and onValueChange. If we just pass an empty string and empty lambda and run the app, we will see that nothing happens. To understand this, we will have to sidetrack again.

State
This is one of the most important concepts in Compose (and declarative UI frameworks in general) so research a bit more if everything is not clear (I would recommend this video).
One of the biggest improvements from the View system is that you hold the state, not the View you use. I can recall at least a dozen times where I needed to intercept TextField changes, but it’s not an easy nor intuitive thing to do in the View system. We hold the state in Compose so we can decide what is and what is not a valid state. But enough praise, and let’s get to the detail.

In the previous article, I talked about Composable functions being able to remember the previous invocation, but I didn’t go into details. The way Compose is exposing this functionality is with remember {} function. When called, the remember function will execute the calculation lambda and remember the result. Then on every recomposition, it will return the same object and won’t recalculate again unless one of the key parameters changes (This is similar behavior to useState in ReactJS).

OK, so we can remember something between recompositions, so let’s jump back to our search text field and use the remember function like this:

We save our initial state to searchValue and whenever the text field registers a change, we just mutate it. Seems like this could work, but when we run the code and try to type into the text field, we can see that nothing changes. Why is that?
There are a couple of problems with this. The first one is that we are indeed mutating the variable searchValue but only in the local function scope, so if the function recomposes, the variable will be overwritten by the cached value in remember{}. The second and more important problem is that our function is not recomposed at all because the Compose runtime doesn’t know that something changed. remember itself won’t save us here, so we’ll have to reach for some additional tool - State.

Compose documentation describes State interface as a “value holder where reads to the value property during the execution of a Composable function will subscribe it to changes of that value,” and that looks like exactly what we need. We can instantiate this state using mutableStateOf (similarly to mutableListOf from Kotlin standard library) for basic objects or mutableStateListOf for lists. So let’s update our code:

When we run this, we get exactly the behavior we wanted. Let’s sum up what exactly is happening when we type something into the text field:

  1. onValueChange is called
  2. we call the setter of searchValue with new value
  3. Compose runtime will recompose our function because it knows that OutlinedTextField is reading searchValuestate and it has been updated
  4. OutlinedTextField is called with the updated value

If you want to know more about the State and recomposition, I suggest reading this article by Zach Klippenstein.

Summary

This article is getting a little bit too long, so I’ll end it on this. The rest of the Search component should be pretty straightforward, but you can look into my implementation if you get stuck on something.
In the next part, we’ll focus on LazyRow / LazyColumn, the equivalent of RecyclerView.

--

--