RizTech Academy logo
RizTech Academy
ArchitectureLesson 1 of 625 min

Why Android punishes you for skipping this

Mandi Bhav works. It has three tabs, a searchable price list, a detail screen with a week of history, and it survives rotation, process death and the back gesture. So why spend a whole module rearranging code that already does its job? Because of what the next three modules ask of it. Module 8 keeps prices on the phone, module 9 fetches them from a server, and module 10 refreshes them in the background while no screen is open. Each of those needs one answer to a question the app has never had to ask: where do prices come from, and who owns them? Today the honest answer is "several places, and nobody". This lesson measures that, names what it will cost, and sets out what the rest of the module does about it.

Counting the sources of truth

Module 6 left a comment on PriceSource promising that every screen asks it for prices. Every screen does, but each one makes its own. To see how many, a scratch copy logs every PriceSource as it is created, with the class and function that created it:

init {
    val caller = Throwable().stackTrace.first {
        it.className.startsWith("com.riztechacademy") && !it.className.contains("PriceSource")
    }
    android.util.Log.d("Arch", "PriceSource created source=${hashCode()} by ${caller.className.substringAfterLast('.')}.${caller.methodName}")
}

Throwable().stackTrace is the list of function calls that led here, and the first frame from the app's own package outside PriceSource is whoever wrote PriceSource(). It is a debugging trick, slow and never for production, and it answers the question in one run. The steps: open the Markets tab, tap Nashik APMC, tap Nashik onion, rotate twice, press Back twice, open the Prices tab, tap Lasalgaon onion, press Back, open Markets again.

D Arch    ( 6714): PriceSource created source=245734911 by MarketsScreenKt.MarketsRoute
D Arch    ( 6714): PriceSource created source=18627231 by MarketsScreenKt.MarketRoute
D Arch    ( 6714): PriceSource created source=82152516 by PriceDetailViewModel.<init>
D Arch    ( 6714): PriceSource created source=232605638 by MarketsScreenKt.MarketRoute
D Arch    ( 6714): PriceSource created source=22629379 by MarketsScreenKt.MarketsRoute
D Arch    ( 6714): PriceSource created source=196148071 by PriceDetailViewModel.<init>
D Arch    ( 6714): PriceSource created source=164960806 by MarketsScreenKt.MarketsRoute

Seven objects in under a minute, each holding its own reference to the price list. The two rotations made two of them: MarketsRoute and MarketRoute keep theirs in remember, and a rotation builds a new composition, so remember started again. And the Prices tab, the screen every user opens first, does not appear at all, because PricesViewModel skips PriceSource and reads SamplePrices.all directly. That is four routes from the screens to the data:

Screen Where it gets prices Lives as long as
Prices SamplePrices.all, read in PricesViewModel The ViewModel
Price detail A PriceSource() made by PriceDetailViewModel The ViewModel, one per visit
Markets list A PriceSource() in remember in MarketsRoute The composition: rebuilt on rotation
One market A PriceSource() in remember in MarketRoute The composition: rebuilt on rotation

Today none of this shows, because the sample data never changes: seven copies of the same unchanging list agree with each other. The day the data can change, they stop agreeing.

What it will cost

Walk the plan for the next modules through this code.

  • Module 9's refresh. A farmer taps "Refresh" on the Prices tab and Lasalgaon onion goes from ₹2,450 to ₹2,525. Which of the seven copies hears about it? Whichever one the refresh button can reach. She taps the card, and the detail screen, holding its own copy, shows ₹2,450. Lesson 3 builds exactly this and shows it on the emulator.
  • Module 8's database. Each PriceSource() would open the database itself. Seven objects each holding a connection, and four places to change when the schema changes.
  • Module 10's background sync. A worker that refreshes prices at 6 am has no screen and no ViewModel. It needs to reach the same data the screens read, and there is no "same data" to reach.
  • Testing. PricesViewModel reads a global object, so a test cannot give it different prices; it can only test the nine sample ones. MarketsRoute reads data inside a composable, so it cannot be tested without running Compose at all.

None of these is a crash today. Each is a bug that will arrive in a later module, in code that looks finished, and will be blamed on whatever was changed last.

Why Android in particular punishes this

Every platform rewards structure. Android punishes its absence faster, for reasons modules 2 and 5 showed you on the emulator:

  • The system creates your entry points. You never call MainActivity(); Android does, again after every rotation and after process death. You cannot pass it a repository in a constructor. The tempting fix is a global object, which works until a test needs different data, or until it quietly holds a Context and leaks it.
  • Your objects are destroyed under you. Module 5 showed an activity replaced three times in one rotation test and a process killed while the farmer took a phone call. Anything stored in the wrong owner disappears with it. remember in MarketsRoute is a small example: a piece of data owned by a composition that lasts until the next rotation.
  • Several things run at once. A screen, a background refresh and later a notification worker all touch prices, on different lifetimes. Without one owner, each keeps its own picture of the world.

Google's guide to app architecture names the principles that answer these, and you have already met two of them:

Principle What it means for Mandi Bhav Where
Separation of concerns Screens draw; something else decides where prices come from Lessons 2 and 3
Drive the UI from data models The screen shows a model of prices that outlives it, not data it fetched itself Lessons 3 and 6
Single source of truth One owner of the prices; everything else asks it Lesson 3
Unidirectional data flow State flows down to the screen, events flow up to the owner Module 5, and lesson 4

The guide's own advice on app components is the one this module keeps coming back to: do not store application data or state in them. An activity, a composable and a remember are app components or live inside one. A repository is not.

The plan

Problem today Lesson that fixes it
No agreed place for each kind of code 2, UI, domain and data layers
Seven PriceSources; the Prices tab bypasses even those 3, one repository, the single source of truth
MarketsRoute reads data in a composable 3, a ViewModel for each Markets screen
PricesScreen takes ten parameters and gains more with every feature; the two ViewModels expose state in different shapes 4, one UI-state object per screen
A one-time message such as "3 prices updated" has nowhere safe to live 4, events as state
Each ViewModel builds its own dependencies, so nothing can be shared or replaced 5, dependency injection with Hilt
A screen that is already open never sees new data 6, flows from the repository to the screen

At the end of it the screens look the same, with two additions the farmer can see: a "Refresh" button and a line saying which market pays the most for the commodity she chose.

What it costs, and what it is not for

Architecture is not free, and the course will not pretend it is.

  • More files. This module adds nine source files to main and removes one. A reader has to follow a call through three of them to see where a price comes from.
  • More concepts. Repositories, use cases, UI-state classes, dependency injection, flows. Each is a word a new team member has to learn before they can change a screen.
  • A slower build. Lesson 5 measures Hilt's cost: on this machine a clean debug build went from about 4 seconds to about 8.

What it is not for: a prototype, a one-screen tool, or an app you will throw away next month. A single-screen app that shows a price list from one source does not need a domain layer, and a team that adds one anyway has made the code longer and no safer. The test is the one this lesson used: will more than one part of the app need the same data, will that data change while the app is running, and will someone need to test the rules without the screen? Mandi Bhav answers yes to all three, and the modules ahead make each answer louder.

The trap in the other direction is architecture as a set of folders. Copying a template with data, domain and presentation packages, a use case for every repository call and an interface for every class produces something that looks structured and has none of the benefits, because the structure was not chosen for a problem. Google's domain-layer guide says this directly: the domain layer is optional, and adding use cases that only forward a call to the data layer adds complexity for little benefit. Every piece this module adds will be added because a measurement or a bug in Mandi Bhav asked for it.

Check your work

Why the sample data hides the problem. Seven copies of an unchanging list always agree. The disagreement only appears when one copy changes.

Why rotation created two PriceSource objects. MarketsRoute and MarketRoute kept theirs in remember, and rotation builds a new composition, which starts every remember again.

Why the Prices tab never logged a PriceSource. PricesViewModel reads SamplePrices.all directly, a fourth path to the data.

Why you cannot give MainActivity a repository in its constructor. Android creates activities itself, again after each rotation and after process death.

What "single source of truth" means here. One owner of the prices that every screen and worker asks, so a change made once is seen everywhere.

When the extra layers are not worth it. A prototype or a single-screen app with one source of data that never changes while running.

Practice 4. No PriceDetailViewModel line appears for the rotation: the ViewModel survives it, as module 5 showed, so its PriceSource does too. The two extra objects come from the composables, which is the difference between keeping data in a ViewModel and keeping it in remember.

Practice

  1. In a scratch copy of the module 6 checkpoint, add the logging init block to PriceSource and repeat the steps above. Count your objects and compare the creators with the log here.
  2. Find every place in the module 6 code that reads SamplePrices outside tests and previews. For each one, write down who owns the data it reads and how long that owner lives.
  3. Write one sentence for each of the four principles in the table, using a Mandi Bhav example that is not in this lesson.
  4. Rotate the emulator while on Nashik onion's detail screen and predict which of the three Markets-tab screens will log a new PriceSource. Check it, then explain the answer above.
  5. Break it on purpose, as a thought experiment you will run for real in lesson 3: suppose PriceDetailViewModel could change its PriceSource's prices. List every screen that would show the old value afterwards.
  6. Harder: take a small app you have written, or one from a tutorial, and apply this lesson's test to it. Does more than one part need the same data, can the data change while the app runs, and does anyone need to test the rules without the screen? Decide which of this module's lessons that app would need, and which it would not.

Official documentation

Next: the three layers, and which of Mandi Bhav's code belongs in each.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship