So many things improve when you stop fussing about what should be and embrace what actually is. Android user interface architecture is no exception.

android bot being split

You’re not going to get it right the first time.

What should be: you conceive perfectly defined intents, fragments, functions, classes, etc. in your head. They’re SOLID, they’re idiomatic, they’re perfect. When it’s time to code, you merely type them into the machine. You’re going to get so many high fives when people see this code.

What is: even with years of Android development experience, you still don’t know how this code is going to shake out. This is a feature, not a bug; it’s in the doing of the work that we discover the work that we must do. You’re not going to get it right the first time.

Stop trying.

Instead, draft. Then edit. Then edit again. Just like with good writing. Write the smallest bit of code that can be shown to work, then edit it toward its architecture. Put another way: lump, then split.

Here, I’ll show you.

Lump

This is a Jetpack Compose function I just scribbled out that shows whether a given year is a leap year or not.

@Composable
fun LeapYear(year: Int) {
    val result = when {
        year % 4 == 0 -> {
            when {
                year % 100 == 0 -> year % 400 == 0
                else -> true
            }
        }
        else -> false
    }
    val negation = if (result) "" else "not"
    Text("$year is $negation a leap year.",
        style = MaterialTheme.typography.h3,
        modifier = Modifier.padding(20.dp))
}

It’s a lump, all right. In this case, a lump of imperative and declarative lines of code. Just… grooving together. Maintaining their coolness together. Worshiping together in the church of their choice. Beautiful for people, poisonous for code.

But–and this is my point–I didn’t try to get this right, code-wise. My first task is to get the software right. To make it run. I’m making a beeline for evidence. While I’m drafting, I run the app about as often as my little Gradle will allow, creating a short feedback loop. And I nudge my lump along until it does what I think it should.

And it’s a good thing I did it that way, too. I found a spacing bug:

Enough space for an astronaut between 'is' and 'not'

    // the fix
    val negation = if (result) "" else " not"    
    Text("$year is$negation a leap year.",

Spacing fixed

Split

Now I shift my focus. With a working lump, it’s time to split my code.

But not literally. If I were to refactor a single class or function without changing its public API, I would split it literally, in place. Usually with a healthy dose of the Extract Method command.

What I’m going for here, though, is an architectural change. I’m splitting one element into two: a View (Jetpack Compose, declarative code) and a View Model (plain Kotlin, imperative code). Architecture is about boundaries. An architectural change is one that disrupts the public API, the boundary, between at least two elements. If I just dive straight at this, splitting the code in place, I’ll break its boundaries, and I’ll stop having working software.

Instead, I want to preserve the value I’ve created at each step. I want to be interruptible. To thread the needle of changing my code’s structure without disrupting its behavior, I use the strangler pattern.

Using the imperative code in my lump as a reference, I create a new pair of files and test-drive a View Model. For brevity here I’ll just link to the repo for these, here and here.

It sure looks like I could split that View Model further, too–it’s doing at least two things. For the sake of staying on my point, I’ll leave this as an exercise for the reader.

Then, I inject the model into the View…

@Composable
fun LeapYear(year: Int, model: LeapYearViewModel = viewModel()) {
    model.year = year
    val result = when {
        year % 4 == 0 -> {
            when {
    // etc...        
}

…and swap out the local message for the model’s message.

    Text(model.message,

At this point, I’ve disturbed the working code enough that I want to run the app again to find out if I’ve broken anything.

2023 is still not a leap year

Nope. 2023 is still not a leap year.

Now I can delete those nasty imperative bits from my View.

@Composable
fun LeapYear(year: Int, model: LeapYearViewModel = viewModel()) {
    model.year = year
    Text(model.message,
        style = MaterialTheme.typography.h3,
        modifier = Modifier.padding(20.dp))
}

Satisfying.

Architecture is a process, not a prerequisite.

You’re not going to get it right the first time. Not your app, not your code. Not anything worth making well. Stop trying.