When building a mobile app with AI, the temptation is to ask the agent to build the whole thing. And it does build it, but it fails. What decides the outcome isn't which model you use, but how much you've locked down before it writes the first line.

There's a move almost everyone repeats the first time they try building a mobile app with an AI agent: open the chat, ask it for the entire app in one go, and sit back and wait. A few seconds later you've got screens, navigation, and something that compiles. It feels like magic.

That feeling lasts right up until you try to build the second feature on top of the first one.

Because a model never tells you "I don't know." It always hands you something that looks finished, even when it isn't. And if you haven't marked out the path, that "something" comes from what the model assumes you wanted, not from what you actually wanted. It wanders off, goes its own way, makes its own calls. For an afternoon experiment, that's perfectly fine. For an app headed to production, it's where the problems start.

The problem isn't that it fails — it's that it never fails

An AI agent always hands you a solution because that's exactly what it's trained to do: fill the gap with the first thing that lets it deliver something. And that's the risk: it hands back an answer that looks finished but almost never respects what you've already built, and it doesn't warn you about what it made up along the way.

Anyone who's seriously tried this will recognize the example. Throughout this series, we're going to build a shared-expenses app — the kind you use to split what a group spends (a dinner, a trip, a shared flat) and figure out who owes who at the end.

So, we start by asking the agent for the add-expense screen, and instead of following the project's architecture, it takes the fast lane. All the logic — UI, API calls, split calculation, parsing, persistence — crammed into a single giant class.

// A screen that does EVERYTHING at once:

class AddExpenseActivity : AppCompatActivity() {
    private val db = AppDatabase.get()    // data
    private fun save(amount: Double, payerId: String) {
        lifecycleScope.launch { 

        // business rules stuffed into the UI:

            val members = db.groupDao().members(currentGroupId)
            val share   = amount / members.size   // split
            members.forEach { m ->

             // persistence

                db.balanceDao().add(m.id, share, payerId)                          
}
         // and while it's at it, syncs with the backend right here:

            // direct API call

            val res = api.post("$BASE_URL/expenses", body)
            Json.decodeFromString<ExpenseDto>(res) // parsing
            runOnUiThread { showBalance() }        // UI + state
        }
    }
    // ...and below that, 400 more lines drawing the form.
}

It compiles, it works in the demo, but you've just broken one of the most expensive rules to break on mobile (the UI calling the backend directly, with the split baked right into the screen), and the next screen is going to be built on top of it.

If we repeat this on the balances screen, the settle-up screen, and the create-group screen, we'll realize we no longer have an app — we have a snowball, where every new piece drags along the mess from the last one, and untangling it costs more than doing it right from the start.

The first time you run into something like this, it takes a while to sink in, because even though it "works," the solution doesn't hold up. The code is there, it compiles, and the demo goes fine — but the problem was never that one file: it was that every file after it was going to copy its bad example.

That's the trap of asking and waiting: it doesn't break on day one, it breaks on day 20.

Defining is the new craft

Well defined, that same agent gets you moving at a speed that didn't exist two years ago. And the whole difference fits in one word: defined.

The more you break the task down and the more tightly you close off what you're asking for, the less room you leave the model to fill in with what you didn't want. Asking for "the add-expense screen" and leaving it at that is not the same as scoping the request so tightly there's no room left for improvisation:

The same screen, tightly scoped:

  • Task: Composable AddExpenseScreen — UI only
  • Follows the existing ui/domain/data layer pattern
  • No business logic or backend calls from the screen
  • The split is calculated by the SplitExpense use case (already implemented)
  • Persists to the local DB. Syncing isn't this layer's concern
  • States: loading / error / data

One task, one scope, one layer.

If you notice, the second version doesn't leave the agent a single decision it can make up on its own: the split calculation lives in its own use case, persistence lives in the data layer, syncing lives somewhere else. The screen just renders. That same task, directed with this level of precision, usually comes out right on the first try. And when it doesn't, the failure is contained to the UI: there's no need to chase it across five tangled responsibilities.

The concrete difference shows up best when you contrast how it's asked:

// ✖️ Open-ended - the model decides the architecture for you
"Build me the screen for adding an expense to the group."

// ✔️ Scoped - you decide and the agent executes
"Composable AddExpenseScreen, UI layer only.
- Takes an AddExpenseUiState and emits events to the ViewModel.
- The split is resolved by the SplitExpense use case — do NOT calculate it here.
- No backend calls from the Composable.
- States to render: loading | error | data."

The first version invites the mega-class. The second one leaves no room to improvise. That's where the developer's job shifts: the skill isn't just typing code anymore, it's defining things precisely enough that the code coming out of the agent is yours, not its.

They're not using different models. They're defining things differently.

For me, that's the line that separates people who say AI "isn't good for anything serious" from people who actually use it to ship.

Start before the code: let the agent interview you

The first thing to do when starting an app isn't coding, it's specifying. We need to flip the usual script: instead of asking the agent to build, we ask it to interview us.

We give it the context of what we want and let it ask questions. It lays out the possible paths, puts the options on the table, tells us which one it recommends, and we make the final call. That interview brings to light the decisions we didn't even know we had to make — the same ones that, left unresolved, are exactly what turns into the snowball later on.

Example:

- You: "I want an app to track shared expenses."

- Agent: Are groups fixed, or created per event? Is the split even, or by percentage? Do you need offline mode from day one? My recommendation: groups per event, configurable split, no offline for now. Shall we go with that?

- You: Done: groups per event, configurable split, no offline.

In three questions, the agent has locked down three decisions that, left unasked, it would have assumed on its own — almost always the opposite of what we wanted. That's what specifying is: surfacing what you'd take for granted before the code takes it for granted on your behalf.

Out of that conversation comes, almost without trying, the scope of the first version. The shared-expenses app ended up looking like this:

Shared-expenses app MVP

  • Create a group per event (a dinner, a trip).
  • Add an expense and record who paid.
  • Configurable split: even, or by percentage.
  • Balance screen: who owes who.
  • Settle a debt and mark it as paid.

Deliberately out of v1: in-app real payments, multi-currency, and offline mode.

Saying out loud what's not included is half the work. Three lines ("no in-app payments, no multi-currency, no offline") save the agent from the temptation to build a payment gateway nobody asked for, and save you the time of ripping it back out.

You don't need to lock everything down at once, either. It's enough to close off just enough that the first task has no gaps the model can slip through. The rest gets defined when its turn comes, one piece at a time.

Defining isn't writing a giant document before you start — it's not leaving any important decision to chance. Once those decisions are made, the next step is writing them down where the agent can check them while it codes (we'll get to that in the post about context) and breaking the "what" down into closed specs, which is a topic for another article altogether.

Specifying first isn't bureaucracy — it's moving the moment of thinking to before you write the code, when changing your mind costs one sentence instead of a refactor.

Loose to experiment, tight to ship

It's worth not mixing up two worlds that look alike but aren't. For a prototype, a quick test, or a weekend of tinkering, let the agent run loose: it's disposable, and the speed more than makes up for the mess. Nobody's going to maintain that.

For something headed to production, open-endedness comes at a cost, with interest. Every shortcut the model invents is technical debt someone — you, three months from now — will have to untangle. The difference between the fun experiment and the maintenance headache isn't in the agent: it's in how much discipline you put in front of it.

In the shared-expenses app, that discipline fits into one rule repeated to the point of boredom: the UI never talks to the backend. The screen asks the domain, the domain decides, the data layer saves locally, and the backend only comes in to sync.

Written down like that, the rule is trivial — and respected on every screen, it's the difference between an app that scales and a snowball.

// The layer that decides lives in domain/, outside the screen:
class SplitExpense(private val repo: ExpenseRepository) {
    suspend operator fun invoke(expense: Expense, split: Split) {
        val shares = when (split) {
            is Split.Equal   -> expense.equalShares()
            is Split.Percent -> expense.byPercent(split.weights)
        }
        repo.save(expense, shares)   // to the local DB; sync happens elsewhere
    }
}

The screen just calls SplitExpense and renders the result. It doesn't know how the split is calculated or where it's stored, and precisely because it doesn't know, the day the split logic changes, only this file gets touched. We'll get into how to chain these pieces together with the agent without it skipping layers in another post about development.

Natural language: one more layer, not a threat

A lot of people still resist this, and almost always for the same reason: they'd rather see and touch the code with their own hands. I get it, I come from that world too, but I think that's looking at the shift from the wrong angle.

In the beginning, we programmed the processor directly, in assembly. Then high-level languages arrived, and we stopped fighting with registers and memory. Every leap hid the layer below so we could think at a higher level. Directing an agent in natural language is the next rung on that same ladder: one more layer of abstraction, not the disappearance of the craft.

And just like every leap before it, we still need to know what we're building. Whoever coded in C understood what was happening underneath; whoever directs an agent has to understand the architecture they want, or the agent will make it up.

In the expenses app, if we're not clear that the split logic lives in the domain layer and not on the screen, the agent decides — and decides badly, not because it's clumsy, but because nobody told it where each thing belonged.

Abstraction frees you from the keyboard, not from judgment.

Conclusion

The invitation is straightforward: try it for real, and do it right. Don't ask for everything at once and expect it to turn out well. Define it, break it into pieces, let it interview you, and direct it yourself. Speed shows up on its own once the foundation is locked down.

In the next articles in this series, we'll look at exactly how to lock it down, phase by phase: the context the agent reads before touching anything, the skills you use to teach it to do things your way, the specs that break the work into pieces, and finally, development.

If there's one line to take away from all this, let it be this: with an agent, the bottleneck is no longer writing the code — it's knowing exactly what you want.

The shared-expenses app we'll be building throughout this series isn't going to fall apart because of the model. It'll hold up based on what we lock down before it writes the first line. And that part, fortunately, is entirely up to us.

Tell us what you think.

Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.