The archaeology of app architecture
Every recommended Android architecture is a scar from a specific failure: the God Activity, the fragment lifecycle, the lost state on rotation. Reading the sequence explains why the current advice looks the way it does.
Nobody designed the recommended Android architecture in one go. It accumulated, one correction at a time, from the failures of the previous recommendation. Reading it as a history is more useful than reading it as a rulebook.
TL;DR#
- Era 1 (2008): the Activity does everything — UI, data loading, state, navigation.
- Era 2 (2011): Fragments promise reuse, and add a harder lifecycle.
- Era 3 (2014–2016): third-party patterns arrive — MVP, , RxJava, Dagger.
- Era 4 (2017): Architecture Components — , ,
LiveData— become the official answer to rotation. - Era 5 (2018–2021): AndroidX, Navigation, WorkManager, Hilt, and then Compose make unidirectional data flow the natural shape.
- Era 6 (2025–): Compose-first, with View libraries in maintenance.1
Mental model#
Think of a city that grew without a plan.
The first buildings went up wherever they fit (the God Activity). Then a grid was imposed (Fragments) and it did not match the buildings. Then architects arrived with theories (MVP, MVVM). Eventually the city published a building code (Architecture Components), and the code was written by people who had watched the earlier buildings fall down.
Where the analogy breaks#
Cities keep their old buildings. Android does not: a screen can be rewritten without moving the users. That is why the architecture keeps changing faster than the platform — the cost of migration is low enough that each generation of advice actually gets adopted.
The big picture#
Text description of this diagram
A timeline of Android app architecture from 2008 to today, in three lanes: platform, libraries and recommended practice. The platform lane shows the Activity doing everything in 2008 and Fragments arriving in 2011. The libraries lane shows Volley in 2013, Data Binding in 2015, Architecture Components in 2017, AndroidX and the Navigation component in 2018, WorkManager in 2018, Hilt in 2020 and Compose 1.0 in 2021. The practice lane shows Kotlin support in 2017, Compose being announced in 2019, unidirectional data flow becoming the guide default in 2021, and Compose-first in 2025. Each event is a reaction to the previous era's most visible failure.
Read the lanes as a conversation: the platform made a promise, the libraries tried to keep it, and practice changed when the libraries made a new shape possible.
Deep dive#
Era 1: the God Activity (2008–2011)#
// The shape almost every early Android app had.
public class NotesActivity extends Activity {
private List<Note> notes;
private DatabaseHelper db;
@Override protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.notes);
db = new DatabaseHelper(this); // data access
notes = db.loadAll(); // blocking I/O on main
ListView list = findViewById(R.id.list); // view wiring
list.setAdapter(new NotesAdapter(notes)); // presentation
findViewById(R.id.add).setOnClickListener(v -> {
notes.add(new Note("Untitled"));
db.insert(notes.get(notes.size() - 1)); // business logic
((BaseAdapter) list.getAdapter()).notifyDataSetChanged();
});
}
}This is not stupidity; it is what the platform encouraged. Activity had a lifecycle, a context, a
window and a callback for everything. There was no state holder that survived rotation, so state lived
in the Activity by necessity.
The failure modes are the ones the rest of this module addresses: untestable, unmaintainable past a few hundred lines, blocking the main thread, losing state on rotation.
Era 2: Fragments (2011)#
Fragments were introduced in API 11 for tablets: reusable UI pieces that could be composed into different layouts for different screen sizes.2
They solved reuse and introduced a second lifecycle with more states, more edge cases, and a
getActivity() that could return null. The fragment lifecycle is the origin of a whole genre of
Android bugs, and the community's eventual answer was to use fewer fragments, not more.
Era 3: imported patterns (2014–2016)#
As apps grew, the community imported patterns from other platforms:
- MVP — a Presenter holds the logic, the View is an interface. Testable, but the interface boilerplate was heavy.
- MVVM — a ViewModel exposes observable state, the view binds to it. This is the one that survived.
- Data Binding (2015) — layout XML can bind to observable variables, so the layout becomes a function of state.
<!-- Data Binding: the layout declares its dependencies. -->
<layout>
<data>
<variable name="vm" type="com.example.NotesViewModel" />
</data>
<TextView android:text="@{vm.title}" />
</layout>Data Binding was the first official move toward declarative UI on Android, and it was ahead of its time in a way that hurt: expressions in XML were hard to debug and impossible to unit test.
Era 4: Architecture Components (2017)#
Google's answer was a small set of libraries that solved the specific failures:
| Component | Failure it fixed |
|---|---|
Lifecycle | manual register/unregister in every activity |
ViewModel | state lost on rotation |
LiveData | callbacks to a destroyed view |
Room | hand-written SQLite boilerplate |
class NotesViewModel(private val repo: NotesRepository) : ViewModel() {
val notes: LiveData<List<Note>> = repo.notes.asLiveData()
}
class NotesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: NotesViewModel by viewModels()
vm.notes.observe(this) { notes -> render(notes) }
}
}Two lines did what an entire era of manual lifecycle management had failed to do: the observer is removed automatically, and the state survives rotation.
Era 5: AndroidX, Navigation, Compose (2018–2021)#
AndroidX (2018) renamed and reset the support library, letting every library ship independently. That is what made the rest possible: Navigation, WorkManager, Compose and Hilt could all version separately.
- Navigation component (2018) modelled navigation as destinations and a graph instead of
FragmentTransactioncalls. - WorkManager (2018) gave deferrable work a guarantee.
- Hilt (2020) made dependency injection practical for Android components.
- Compose (2021) made the UI a function of state, which made UDF the natural architecture rather than a discipline.
@Composable
fun NotesRoute(viewModel: NotesViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
NotesScreen(
state = state,
onAdd = viewModel::add,
onOpen = viewModel::open,
)
}Note the shape: state down, events up, one state object. That is unidirectional data flow, and Compose made it the path of least resistance instead of a convention.
Era 6: Compose-first (2025–)#
Android 17 states that new platform and Jetpack APIs target Compose only, and that the View libraries enter maintenance mode.1 The architectural consequence is not "rewrite everything". It is that the destination is now unambiguous: new screens are composables in a navigation graph, state lives in a ViewModel, and the data layer is unchanged.
The pattern that did not survive: the God Fragment#
When activities were split up, the same problem reappeared one level down: a fragment that held the data, the network calls, the adapters and the navigation. The "God Fragment" was the God Activity with a harder lifecycle, and it produced the same class of bug — plus a new one, because a fragment can outlive its view.
// The shape that replaced it.
class NotesFragment : Fragment() {
private val viewModel: NotesViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentNotesBinding.bind(view)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state -> binding.render(state) }
}
}
}
}Two details in that snippet are the era's real lesson. viewLifecycleOwner instead of the fragment's
own lifecycle, because the view can be destroyed while the fragment lives. And repeatOnLifecycle,
because the collection must stop when the view is not visible.
What "clean architecture" added#
The Uncle Bob-derived "clean architecture" that circulated in Android circles from around 2016 added a layer vocabulary — entities, use cases, interface adapters — and one durable idea: dependencies point inward. The domain layer should not know about Retrofit or Room.
The official guide adopted the useful half and dropped the ceremony. Its version has three layers and makes the domain layer optional, which is a pragmatic correction: a use case that only forwards a call adds a file and no value.
What every era got right#
Reading the sequence, three ideas survived every rewrite:
- Separate state from the view. MVP's presenter, MVVM's ViewModel, Compose's state holder — the name changed, the idea did not.
- Keep the data layer independent of the UI. A repository that knows about a
TextViewcannot be tested. - Tie work to a lifetime. Loaders,
LiveData,viewModelScope,repeatOnLifecycle— each is an answer to "when should this stop?"
Those three are the actual content of "Android architecture". Everything else is syntax.
What the eras share#
Every era solved the same underlying problem: the platform's components are created and destroyed by
the system, and state must survive that. The solutions changed — retained fragments, ViewModel,
SavedStateHandle, Compose's rememberSaveable — but the constraint never did.
That is the useful way to read the history. If a new pattern appears, ask which lifecycle it is trying to survive. If it does not answer that question, it is probably a style preference rather than an architectural advance.
Why the cycle keeps turning#
Every era made the previous era's implicit assumptions explicit and then removed them. The Activity
assumed it lived forever; ViewModel made lifetime explicit. Fragments assumed a view existed; Compose
made existence a function of state. The next era will make some assumption of ours explicit — probably
about synchronisation across devices, or about the boundary between app and AI agent.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 4 | 1.6 Donut |
|
| 5 | 2.0 Eclair |
|
| 8 | 2.2 Froyo |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 13 | 3.2 Honeycomb |
|
| 14 | 4.0 Ice Cream Sandwich |
|
| 16 | 4.1 Jelly Bean |
|
| 17 | 4.2 Jelly Bean |
|
| 18 | 4.3 Jelly Bean |
|
| 19 | 4.4 KitKat |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 22 | 5.1 Lollipop |
|
| 23 | 6.0 Marshmallow |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 27 | 8.1 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 32 | 12L |
|
| 33 | 13 Tiramisu |
|
| 34 | 14 Upside Down Cake |
|
| 35 | 15 Vanilla Ice Cream |
|
| 36 | 16 Baklava |
|
| 37 | 17 Cinnamon Bun |
|
Each row in that table corresponds to an era above. The pattern is that the platform makes fewer promises over time about how an app is structured, and the libraries make more.
Under the hood#
- Activity.javathe class the first era put everything into
- Fragment.javathe platform fragment, now superseded by AndroidX's
- Application.javathe only process-wide hook the platform offers
- Bundle.javathe saved-state container every era has had to work around
Curiosity corner#
- The God Activity has a name in the docs: "the Activity does everything" is the anti-pattern the architecture guide opens with.
- Data Binding's decline was caused by its own success. It proved that declarative UI was the right direction; Compose did it in Kotlin, where it could be tested.
ViewModelis not an Android class. It is in AndroidX, which is why it can be used from a plain JVM test with a fake lifecycle.
Try it#
find . -name "*Activity.kt" -exec wc -l ; | sort -rn | head
grep -rn ": ViewModel()" --include=*.kt . | wc -l
Check yourself#
Check yourself
Why model a screen as one immutable `UiState` rather than several independent `StateFlow`s?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- blogAndroid 17 is hereAndroid Developers Blogaccessed 2026-10-27↩
- docsJetpackdeveloper.android.comaccessed 2026-10-27↩
- docsGuide to app architecturedeveloper.android.comaccessed 2026-10-27↩
- docsMigrar para o AndroidX | Jetpackdeveloper.android.comaccessed 2026-10-27↩
- docsAndroid 10 features and APIsdeveloper.android.comaccessed 2026-10-27↩
- docsFeatures and APIs overview | Android 12developer.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 17developer.android.comaccessed 2026-10-27↩