Android's Architecture Evolution Is a 15-Year War Against the Async Frankenstein
After a few years of work, I realized:
What really tortures people isn't threads.
It's these things:
- Callbacks nested inside callbacks
- Tasks still running after the Activity has exited
- Callbacks suddenly returning after the page is destroyed
- Data update order getting messed up
- Mixing Handler, Rx, LiveData, and Flow together
- Every SDK having its own callback system
Eventually, the project slowly becomes:
A giant asynchronous Frankenstein.
And this is actually the real source of complexity in large Android projects.
1. Android didn't have a unified async solution early on
Looking back at Android's development history, you'll find:
Android never lacked async solutions.
What it lacked was:
A unified async solution.
The Thread + Handler Era
At first, everyone wrote code like this:
Thread {
val data = request()
handler.post {
updateUi(data)
}
}.start()
When first learning, you'd think:
This is so simple.
Later, as the project grew, problems emerged:
- Who creates the thread?
- Who destroys it?
- Who switches to the main thread?
- What happens when the page exits?
- What if the callback returns after the page is gone?
Code started to fill with:
if(activity.isFinishing) return
if(fragment.isAdded)
WeakReference<Activity>
The whole codebase became increasingly strange and harder to maintain.
The AsyncTask Era
Later, Google thought:
Directly manipulating Threads is too hard.
So they introduced:
doInBackground()
onPostExecute()
At the time, it felt like a godsend.
But a few years later, everyone realized:
It simply:
Moved the callback to a different place.
It solved none of the problems.
Eventually, it was even officially deprecated.
The Listener Explosion Era
Later, the Android system became more and more complex.
Various Listeners started proliferating wildly:
- LocationListener
- SensorListener
- BluetoothListener
- MediaPlayerListener
- CameraListener
- SurfaceListener
And then you'd discover:
Every system component has its own way of doing things.
Some:
Callback on the main thread
Some:
Callback on a Binder thread
Some:
Must manually unregister
Some:
Lifecycle ends automatically
And some third-party SDKs:
The docs don't even tell you which thread the callback runs on.
Thus, the entire project entered:
The Era of Everything is a Callback.
2. The real complexity is the collaboration between threads.
Many people think:
Async difficulty = Multi-threading difficulty
That's completely wrong.
Take the most common business flow:
Request user info
↓
Get user ID
↓
Request order list
↓
Write to database
↓
Notify UI update
↓
Auto-cancel when page exits
The hardest part here isn't:
Spawning threads.
It's:
Who manages this entire workflow?
When to cancel?
How to propagate exceptions?
What to do when the page is destroyed?
Who is responsible for switching threads?
Who is responsible for calling back the UI?
These are the truly troublesome parts.
3. The scariest thing is every layer having its own async model
For example, an old project might look like this:
Network layer:
Callback
Database:
RxJava
UI:
LiveData
System events:
Handler
IM:
Listener
Eventually, the business code becomes:
Callback wrapping Rx
Rx converting to LiveData
LiveData converting to Flow
Flow finally being collected
The whole project resembles an asynchronous transfer station.
You have no idea:
Where the data actually came from.
Or when it will come back.
4. RxJava gave everyone hope for the first time
Why was RxJava so popular back then?
Because it told everyone for the first time:
Don't worry about network requests, button clicks, database changes.
They are all streams.
Network requests are streams.
Database changes are streams.
Button clicks are also streams.
Even timers are streams.
Suddenly:
The entire project finally started speaking the same language.
That's also why:
For those few years in Android:
If you didn't know RxJava, you were too embarrassed to send your resume.
5. But RxJava's biggest problem is:
It's ultimately just a library.
In other words:
You can only play if you know Rx.
If you don't?
You basically can't read the project.
Especially code like this:
flatMap()
switchMap()
zip()
combineLatest()
observeOn()
subscribeOn()
Newcomers basically surrender on sight.
And:
The Rx world.
The Callback world.
The Kotlin world.
Often, three systems coexist simultaneously.
The project still slowly becomes:
An asynchronous Frankenstein.
6. The truly powerful thing about Coroutines
The most powerful thing about Kotlin Coroutines is actually:
Async finally became a language capability, and complex async became linear and controllable.
Before:
Async relied on frameworks.
Now:
Async is written directly into the language.
Thus:
val user = api.getUser()
val order = api.getOrder(user.id)
dao.insert(order)
Looks synchronous.
Actually, it's all asynchronous.
Code finally started to look like it was written by humans again.
7. What Flow truly solves is:
Finally, no more converting back and forth everywhere.
Before:
Callback -> Rx -> LiveData -> UI
Now:
Flow -> UI
Network:
Flow<User>
Database:
Flow<List<Article>>
WebSocket:
Flow<Message>
Bluetooth status:
Flow<BluetoothState>
UI State:
StateFlow<UiState>
The entire project finally starts speaking one language.
This is actually much more important than performance optimization.
Because:
What architecture fears most is chaos
8. Why Compose madly embraces Flow
Because the two are from the same family.
Compose:
State changes
↓
UI auto-refreshes
Flow:
Data changes
↓
State auto-updates
So:
val uiState by viewModel.uiState.collectAsState() // This is an example; use the correct withLifecycle one in production.
9. Android architecture upgrades over the years,
have actually been solving the same problem.
On the surface:
MVVM
MVI
Compose
Flow
StateFlow
UDF
Seems like various new terms keep popping up.
But essentially, they are all doing one thing:
Locking runaway async back into rules.
Because what's truly terrifying in large projects has never been:
Too many features.
It's:
Async out of control.
When does it execute?
When does it cancel?
Who comes back first?
Who comes back later?
Who is responsible for updating the state?
If no one manages these questions.
The project will eventually become:
Whoever changes it takes the blame.
Finally
The evolution of Android architecture over the years.
On the surface, it looks like:
Upgrading APIs
Replacing frameworks
Updating architecture
But underneath, it has always been doing the same thing:
Eliminating the asynchronous Frankenstein.
From Thread to Handler.
From AsyncTask to RxJava.
From RxJava to Coroutine.
From LiveData to Flow.
From XML to Compose.
Technology keeps changing.
But the battle between Android engineers and async, for over a decade, has finally reached its end with coroutines.
References
From Callback to Coroutines: The Evolution of Android Async Concurrency Solutions
Stop launch(IO): 3 Hidden Anti-patterns of Coroutine Thread Switching
Getting Started with Android Clean Architecture Using a Small Demo
Modern Android Architecture Doesn't Need an Event Bus
Why I Don't Handle Exceptions Directly in Android ViewModel?
Top 3 of 5 from juejin.cn, machine-translated. The original thread is authoritative.
The perspective of this article is great; it clearly lays out the changes in async handling during Android's architectural evolution. I've felt this deeply in past projects — back when AsyncTask and Handler were everywhere, callback hell was a genuine nightmare. Later, the LiveData plus ViewModel combination really improved things, and now with coroutines and Flow, code readability and maintainability have taken a qualitative leap. From personal experience, architectural upgrades aren't just about technology choices; more importantly, the team's understanding of async lifecycles needs to keep up, otherwise even the best tools are easy to misuse. Thanks for sharing — just my two cents, for reference.
Thanks for the support
Big bro's articles are always so inspiring [thumbs up]
Glad you like it
Spot-on summary