MVC, MVP, MVVM: The 50-Year Evolution of Android Architecture
Foreword
Two months ago, the company started a new business, and I was happy to participate in it, developing a new APP. After all, against the backdrop of the mobile internet having fizzled out for many years, opportunities to develop a new APP are indeed rare. A large number of Android programmers like me have either moved to in-vehicle systems or Framework development (and many are outsourced).
However, it turned out I was happy too soon.
A new APP comes with new requirements, including the Compose, Jetpack, and MVVM stack. For an old-timer like me who has always used Java + XML, opportunity also means challenge, and I had to start learning these new things.
To be honest, I am very resistant to learning new technologies provided by Google, not out of personal laziness, but because Google has been too much of a pitfall for Android development. Many technologies, when released, are hyped to the skies, but after you spend time seriously learning them, you find they have already been deprecated. Many things were clearly launched without Google thinking them through, or rather, Google has always lacked a long-term plan for the Android technology stack.
This is not a joke; it's the personal experience of every Android developer. For example, those who have been doing Android for a while will definitely know the following:
- AsyncTask — every beginner wrote it, deprecated after 11 years
- Kotlin Android Extensions — "Say goodbye to findViewById", deprecated after 3 years
- IntentService — it's deprecated, and its replacement JobIntentService is also deprecated, a whole lineage scrapped
- DataBinding — learned for 9 years, the official Codelab is marked as Deprecated
- LiveData — not officially deprecated yet, but official examples have fully switched to StateFlow
- Volley (replaced by OkHttp), ListView (replaced by RecyclerView), ViewPager (ViewPager2, Compose)
- ......
Every single one of these quickly became a de facto standard for Android development upon release, but was soon abandoned by the official side. Even if some haven't been officially deprecated, they have practically become history.
Life is short, time is precious. We shouldn't waste energy on things with lifespans as fleeting as mayflies. Those underlying, long-lasting things are the true path for technology developers. But Google doesn't seem like a company that likes to settle, at least not on Android. It prefers to do flashy work on the surface, swapping out a set of APIs for you to relearn, and if that doesn't work, just deprecate it. To the point of replacing Java with Kotlin, and the View system with Compose — although the old ones aren't officially deprecated, everyone knows Kotlin is the priority.
That being said, Android is Google's technology, and its development ecosystem is tightly controlled by Google. Regardless, as an Android developer (at least for now), I have to start learning the Kotlin + Compose + Jetpack + MVVM stack.
I originally started learning based on the Jetpack libraries and outputting articles. This is the series: Android Jetpack - Li Siwei's Column - Juejin.
But when I was trying to output content on ViewModel, I discovered a problem: the addition of ViewModel significantly changes the APP architecture. Rather than roughly covering the topic of software architecture when writing about ViewModel, it's better to write an article directly explaining software architecture clearly. Thus, this article was born: looking at the development of Android software architecture from a historical perspective, from MVC to MVP to MVVM, using a single requirement to illustrate the similarities and differences between these architectures. Although Android is used as an example, the architectural ideas of MVC, MVP, and MVVM apply to all GUI programs.
The Barbaric Era Without Architecture
It was the summer of 2014. The sky in Beijing was always gray, but everything looked so thriving. The Food Treasure Street was crowded, and the Startup Street was bustling with noise. It was the best of times, so good that as long as you could describe the Activity lifecycle, you could secure a position as an Android programmer; it was also the worst of times, so bad that everyone was rapidly producing under the sweep of the mobile internet wave, ignoring the importance of software architecture and accumulating technical debt through constant iteration.
This was the barbaric era of Android development, and the barbaric era needed no architecture. Software at this time, at most, used some idioms and a few design patterns. If you could clearly explain MVC, you had the potential to be a team leader.
Since we're on this topic, let's explain the three terms: idioms, design patterns, and software architecture.
Idioms, Design Patterns, Software Architecture
Idioms, design patterns, and software architecture are progressive, but the progression is not towards increasing complexity, but rather an increasing scope of influence.
An idiom is a fixed way of writing code to solve a specific problem in a specific language. It is strongly bound to the language and may not exist if you switch languages. Its scope is small, just a few to a few dozen lines of code, but the difference between programmers is reflected here.
Here are some common examples of idioms:
// Kotlin idiom: using let for null-safe calls
user?.let { sendEmail(it) }
// Kotlin idiom: using apply to initialize an object
val dialog = AlertDialog.Builder(this).apply {
setTitle("提示")
setMessage("确定删除?")
setPositiveButton("确定") { _, _ -> delete() }
}
// Java idiom: Double-checked locking singleton
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
A design pattern is a general solution for solving a recurring design problem. It is language-independent and describes the collaboration relationships between classes, i.e., how to organize classes, not how to write the code within them. Its scope is at the module level, involving the relationships between several classes.
For example, in Android, you can easily see the application of the following design patterns:
| Pattern | Problem Solved | Example in Android |
|---|---|---|
| Observer | Notifying multiple dependents when one object changes | LiveData, Flow, EventBus |
| Singleton | Only one instance is needed globally | Application, RoomDatabase |
| Factory | Creating objects without exposing the concrete class | ViewModelProvider.Factory |
| Strategy | Algorithms can be swapped at any time | RecyclerView.LayoutManager |
| Adapter | Converting when interfaces are incompatible | RecyclerView.Adapter |
| Proxy | Controlling access to an object through a proxy instead of direct access | Retrofit dynamic proxy |
Regarding design patterns, there is a very useful book here: Design Patterns (Douban) (rating 9.3).
Software architecture determines how the entire system is divided into modules, how modules communicate, and how data flows. It is the highest-level structural decision and the content this article will discuss.
The scope of software architecture is the entire system. Once determined, the cost of changing it is extremely high. By understanding a system's software architecture, you can know what the system looks like. Just like the architectures to be discussed in this article:
| Architecture | Core Idea |
|---|---|
| MVC | Three-layer separation of Model/View/Controller, View observes Model |
| MVP | View and Model are completely isolated, Presenter controls everything |
| MVVM | ViewModel does not hold a View, data drives the UI |
Architecture is the highest-level decision. If you choose the wrong one, no matter how beautifully written the design patterns and idioms are, it's useless. Conversely, if the architecture is chosen correctly, the specific design patterns and idioms used can be optimized slowly later.
So architecture is important not because it is the highest level, but because it is the hardest to change.
If we use an analogy to explain these three terms, consider building a skyscraper:
- Idioms: How bricks are staggered when building a wall, how rebar is tied — these are fixed routines of specific construction methods
- Design Patterns: When encountering a large-span space, whether to use an arch or a beam — these are general solutions to recurring structural problems
- Software Architecture: Whether the building is an office or a residence, how many units per floor, whether utilities run through exposed or concealed conduits — this determines the skeleton of the entire building
If an architect makes a design mistake, no matter how well the bricklayer plasters the wall, it's useless. Here, one must refer to the Leaning Tower of Pisa, which has been patched up continuously since its construction and is almost beyond repair.
Defining a Requirement
Looking back at that summer, I also entered an Android development position at that time. It was truly the golden age of the mobile internet; everyone on the street was talking about startups, and the first step of a startup always seemed to be making an APP.
I still remember a page for an image list I made back then. The requirement was simple:
This page displays images posted by the user. When entering this page, it can display these images in a waterfall flow format. Clicking an image enlarges it, and clicking the text below the image navigates to the corresponding content page.
I wrote all the code into ImageActivity: UI rendering, button clicks, network requests, image display, navigation logic — all in one Activity. It worked.
However, as requirements iterated: "Show loading while fetching", "Prompt error and allow retry on failure", "Add a cache so it doesn't request every time", "Images should support zooming", "Add a share button"... Every change required rummaging through that pile of code, afraid that changing one part would break three others. Eventually, this ImageActivity exceeded two thousand lines, and no one wanted to touch it.
This is not just my story. In those years, almost every Android developer experienced the same thing — struggling in a giant Activity, getting lost in endless callbacks, oscillating between "changing a small requirement" and "one change affects everything". This is the technical debt brought by our lack of a way to organize code. And this "way" is architecture.
From the birth of MVC in 1978, to the emergence of MVP in 1996, to the debut of MVVM in 2005, and until today when MVI ideas are integrated into daily life — no architecture was invented out of thin air. They are responses to the pain points of the previous architecture, reflections of countless developers after hitting walls in real business.
In this article, I want to use a consistent requirement to walk you through this path. But time has passed, and using the image list requirement from years ago is no longer realistic, so we will use a similar requirement like the one below to demonstrate the differences between these software architectures:
There is a button on the page. When the user clicks this button, it will access the server to get some images and display them on the screen.
Although this requirement is simple, it is sufficient to demonstrate the differences between architectures. After implementing this requirement each time, we will get the following effect.
In the lower right corner of the application, there is a search icon. After clicking it, it will fetch images of several built-in celebrities through an API and display them in a waterfall flow.
Code Structure Without Architecture
In the early days of the mobile internet, very few APPs had an architectural design. For the functionality described above, basically all the code was placed in the Activity. The reason it was Activity and not something else is that Activity is the interface display class in Android development. After all, it's a GUI program, and you can basically consider all interfaces to start from Activity.
class MainActivity : ComponentActivity() {
private val okHttpClient: OkHttpClient = OkHttpClient()
private var keyWordList: List<String> = listOf("张含韵", "刘亦菲", "鞠婧祎", "赵丽颖", "迪丽热巴", "唐嫣", "BY2", "杨超越")
private val imageList = mutableStateListOf<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SwiftArchitectureTheme {
var keyWordIndexState by remember { mutableIntStateOf(0) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Text(
text = keyWordList[keyWordIndexState],
modifier = Modifier
.statusBarsPadding()
.fillMaxWidth()
.height(60.dp)
.wrapContentSize(Alignment.Center)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
keyWordIndexState = Random.nextInt(0, keyWordList.size)
searchImage(keyWordList[keyWordIndexState])
}
) {
Icon(
painter = painterResource(android.R.drawable.ic_search_category_default),
contentDescription = null,
)
}
},
content = { innerPadding ->
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = innerPadding,
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalItemSpacing = 2.dp,
modifier = Modifier.fillMaxSize()
) {
items(imageList.size, key = { imageList[it] }) {
AsyncImage(
model = imageList[it],
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth
)
}
}
})
}
}
}
private fun searchImage(keyWord: String) {
val url = HttpUrl.Builder()
.scheme("https")
.host("cn.apihz.cn")
.addPathSegments("/api/img/apihzimgsougou.php")
.addQueryParameter("id", "YOUR_API_ID")
.addQueryParameter("key", "YOUR_API_KEY")
.addQueryParameter("words", keyWord)
.build()
val request = Request.Builder().url(url).build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("avril", e.localizedMessage)
}
override fun onResponse(call: Call, response: Response) {
val responseObject = JSONObject(response.body.string())
val resImageList = responseObject.getJSONArray("res")
runOnUiThread {
if (resImageList == null)
Toast.makeText(this@MainActivity, "服务器返回失败", Toast.LENGTH_SHORT).show()
else {
imageList.clear()
for (index in 0 until resImageList.length()) {
imageList.add(resImageList.getString(index))
}
}
}
}
})
}
}
Above is all the code for this functionality. The feature is small, so it can be completed in less than 100 lines of code. But you will find that it contains code with little correlation. For example, UI display, network requests, and API parsing. Although the code isn't much right now and mixing these together isn't very messy, when the project gradually grows, such code will become a disaster.
And this current concise code structure and writing style is merely accumulating technical debt. Therefore, we urgently need a software architecture, and the first architecture to enter our sight is MVC.
MVC: The Starting Point of Architecture
The barbaric era lasted for several years, and the technical debt it brought was precisely masked by the prosperity of the mobile internet. But around 2014 and 2015, developers realized that if they didn't discuss software architecture issues, the collapse of their projects was just a matter of time. The reason was simple: it was time to pay the debt.
APPs were getting bigger and bigger. An e-commerce APP casually had dozens of pages, each Activity often had thousands of lines, and after a few iterations of requirements, no one could understand it anymore. Taking over someone else's code? That was even more of a nightmare. The community began to see a lot of discussion on "how to organize Android code".
Thus, people turned their attention to an old concept that had existed for over 50 years — MVC.
In 1978, Norwegian computer scientist Trygve Reenskaug designed MVC at Xerox for the Smalltalk-76 system. The original purpose was simple:
Separate data from presentation, so that the same data can have multiple different views.
This was the pioneering work of GUI software architecture, and half a century has passed since then.
The Core Structure of MVC
MVC divides software into three layers:
| Role | Responsibility | Corresponding in Android |
|---|---|---|
| Model | Data + Business Logic | Network requests, database operations, data parsing |
| View | UI Rendering | XML Layout / Compose UI |
| Controller | Receives user input, calls Model, decides what to display | Activity / Fragment |
The core interaction flow is:
User Action --> Controller --> Model (processes business) --> Model notifies View to update --> View reads Model data --> Refreshes UI
Thus, we have the diagram we often see:
There is a key design here: the View can directly access the Model. When the Model changes, the View can perceive it and actively update itself. This is the core characteristic of MVC, and also the biggest point of contention later on.
Compared to no architecture, the improvements of MVC are obvious:
- Separation of Concerns: Network requests are no longer part of the Activity but an independent Model layer. The Activity is no longer both the player and the referee.
- Model Reusability: After extracting the image fetching logic, other pages can also call it.
- Improved Testability: The Model does not depend on the UI and can be independently unit tested.
The Origin of the Three Words: Model, View, Controller
Here, I think it's necessary to explain the three words in the MVC architecture: Model, View, Controller, especially Model, which is a word that needs understanding.
- View: This is the most intuitive, meaning the visual representation, corresponding to the UI.
- Controller: This translates to controller. A controller's role is to receive input and decide what to do.
And the word Model needs a good explanation.
In the original MVC paper, the definition of Model is:
A model is an internal representation of some aspect of the world.
Let's first think about what "model" means in various industries:
- Architectural Model: Not a real building, but accurately represents the structure of the building
- Weather Model: Not the weather itself, but represents the operational laws of weather mathematically
- Economic Model: Not the economy itself, but abstracts the key elements and relationships within the economy
Generally speaking, what we call a model is actually an abstract representation of real things.
In software, the real things are the business domains we focus on, such as products, orders, and users in an e-commerce system; exam questions, students, and teaching in an education system. The representation of these business concepts in code is the Model.
And as an abstract representation of the business, the Model represents not just data, but also business logic. That is:
Model = Data representation of business knowledge + Business logic for operating on this data
For example, in an e-commerce system, the order's ID, amount, and status are data, while its business rules, such as when it can be canceled and what status it will be in after cancellation, are all part of the Model.
If there is only data without business rules, it's called a DTO (Data Transfer Object) or Entity, not a Model.
But as Android developers, we often mistakenly think that the Model is just the data layer. The reason many Android developers have this misconception is that in actual projects, business logic is often tightly bound to backend APIs. The Android side receives results already calculated by the backend and doesn't need much local business rules. Therefore, the Model in Android usually degenerates into: network requests + data parsing + data caching. Simply put, in the context of Android, the Model is the layer responsible for handling data.
It can be seen that this misconception actually arises from developers being limited to Android development. So, we still need to dabble in a wide range of knowledge.
Model is usually translated as "模型" (model). This word is somewhat abstract in Chinese, and the translation is indeed not very good; it's too vague. If it had been translated as "业务层" (business layer) or "领域层" (domain layer), this confusion might not exist. In fact, in "Domain-Driven Design" (DDD), a similar concept is called Domain Model, often translated as "领域模型" (domain model), which is much clearer to understand. And in the Android architecture later recommended by Google, a Domain layer was also introduced.
Implementing the Requirement with MVC
Now let's turn the topic back and implement our requirement using the MVC architecture to see what improvements MVC brings to the software.
Following the MVC approach, we split the previous architecture-less code:
- Model: Extract the network request and JSON parsing into an independent class
- View: Compose UI is responsible for rendering, not caring where the data comes from
- Controller: Activity is responsible for receiving user clicks, calling the Model to get data, and then updating the View
Model Layer
The Model in this requirement corresponds to the API request and parsing, which is the searchImage method in the previous MainActivity. Here, we extract it as a separate Model class:
class ImageModel {
private val okHttpClient: OkHttpClient = OkHttpClient()
val keyWordList: List<String> = listOf("张含韵", "刘亦菲", "鞠婧祎", "赵丽颖", "迪丽热巴", "唐嫣", "BY2", "杨超越")
fun searchImage(
keyWord: String,
onSuccess: (List<String>) -> Unit,
onError: (String) -> Unit
) {
val url = HttpUrl.Builder()
.scheme("https")
.host("cn.apihz.cn")
.addPathSegments("/api/img/apihzimgsougou.php")
.addQueryParameter("id", "YOUR_API_ID")
.addQueryParameter("key", "YOUR_API_KEY")
.addQueryParameter("words", keyWord)
.build()
val request = Request.Builder().url(url).build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
onError(e.localizedMessage ?: "网络请求失败")
}
override fun onResponse(call: Call, response: Response) {
try {
val body = response.body?.string()
if (body.isNullOrEmpty()) {
onError("服务器返回为空")
return
}
val responseObject = JSONObject(body)
val resImageList = responseObject.optJSONArray("res")
if (resImageList == null) {
onError("服务器返回格式异常")
return
}
val imageUrls = mutableListOf<String>()
for (index in 0 until resImageList.length()) {
imageUrls.add(resImageList.getString(index))
}
onSuccess(imageUrls)
} catch (e: Exception) {
onError("数据解析失败: ${e.message}")
}
}
})
}
}
Controller Layer
The Controller is independent, holds a reference to the Model, and is responsible for calling the Model and passing results back to the View via callbacks. But note, the entry point for user events is in the View's onClick. The View gets the event and then calls the Controller — this is the compromise of MVC on Android: the Controller cannot independently receive events and must rely on the View to forward them.
class ImageController {
private val imageModel = ImageModel()
val keyWordList: List<String> get() = imageModel.keyWordList
fun searchImage(
keyWord: String,
onSuccess: (List<String>) -> Unit,
onError: (String) -> Unit
) {
imageModel.searchImage(keyWord, onSuccess, onError)
}
}
The Controller currently seems to just forward the call, appearing unnecessary. But in a real project, the Controller would be responsible for more things: coordinating multiple Models, handling combinations of business logic, and deciding which View to display. Its value is not obvious in this simple requirement, but when requirements become complex, the Controller's coordinating role will be evident.
View Layer
In the ideal MVC model, the View should be independent, solely responsible for rendering data onto the interface. User input events go first to the Controller, which decides who to call.
But in Android, this is obviously impossible. Activity is designed as a full steward — it's the interface container, the event entry point, and the lifecycle manager. MVC requires the separation of View and Controller, but Activity's design does not allow it to play only one role. So MVC on Android is essentially a variant: Activity is forced to play both View and Controller simultaneously. The View's role is to render images on the interface, and the Controller's role is to receive user events and call the Model. In some MVC projects, you might not even see an independent Controller class; it's directly merged into the Activity.
class MainActivity : ComponentActivity() {
private val controller = ImageController()
private val imageList = mutableStateListOf<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SwiftArchitectureTheme {
var keyWordIndexState by remember { mutableIntStateOf(0) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Text(
text = controller.keyWordList[keyWordIndexState],
modifier = Modifier
.statusBarsPadding()
.fillMaxWidth()
.height(60.dp)
.wrapContentSize(Alignment.Center)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
keyWordIndexState = Random.nextInt(0, controller.keyWordList.size)
val keyWord = controller.keyWordList[keyWordIndexState]
controller.searchImage(
keyWord = keyWord,
onSuccess = { imageUrlList ->
runOnUiThread {
imageList.clear()
imageList.addAll(imageUrlList)
}
},
onError = { message ->
runOnUiThread {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
)
}
) {
Icon(
painter = painterResource(android.R.drawable.ic_search_category_default),
contentDescription = null,
)
}
},
content = { innerPadding ->
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = innerPadding,
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalItemSpacing = 2.dp,
modifier = Modifier.fillMaxSize()
) {
items(imageList.size, key = { imageList[it] }) {
AsyncImage(
model = imageList[it],
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth
)
}
}
})
}
}
}
}
After splitting with the MVC architecture, we divided the structure where all code was kneaded into MainActivity into three layers. The interaction between these three layers is: the user clicks the View, the View calls the Controller, the Controller calls the Model, and finally, the Model's result is returned level by level and ultimately displayed on the interface.
Thus, we get the dependency diagram for these three, as follows:
The Pain Points of MVC
It seems MVC solved the problem of no architecture — the code is split into three layers, each with its own responsibilities. But if you look closely at the code above, you'll find two problems:
- First, the Activity is both the View and the Controller. UI rendering is the View's job, and receiving events, calling the Model, and handling callbacks is the Controller's job, all in
MainActivity. - Second, the View and Model are not truly isolated. The Controller passes the
List<String>returned by the Model directly to the Activity (View), so the View knows the Model's data structure. Once the Model's return format changes, the View has to change too.
These two problems are not obvious in simple requirements, but when pages become complex and requirements iterate repeatedly, the Activity will bloat again, and the coupling between View and Model will become tighter and tighter. This is the inherent flaw of MVC on Android, and the direct reason for the birth of MVP.
MVP
MVC was the first widely used architecture on Android. In the early days of the mobile internet, Android development was basically in this state — piling code into Activities, at most encapsulating network requests separately as a Model layer to isolate from the Activity. But MVC didn't perform well on Android; the Activity was still bloated, and the View and Controller were still coupled. So around 2016, a new word began to appear in the Android community: MVP.
The period when I was most exposed to Android architecture was those years when MVP was all the rage. I remember working on a series of education-related APPs at that time. MVC on Android was already starting to give way to MVP. Logic control began migrating from Controller to Presenter, Activities were no longer so bloated, and the entire application's code started to be built around the Presenter. The Presenter called the Model to get data and simultaneously held an interface reference to the View, but the View had to implement the interface defined by the Presenter, such as showLoading() and showImages().
It was precisely because of using MVP that the projects I did in those years didn't become unchangeable. Compared to the awkward performance of MVC on Android, MVP's improvement to architecture was truly what everyone wanted, and MVP had become the de facto standard for Android architecture at that time.
The Structure of MVP
But to talk about the origin of MVP, we need to go back another 30 years. In 1996, Mike Potel proposed MVP at Taligent (a joint venture between IBM and Apple). At that time, MVC was just 18 years old.
Potel's motivation for proposing MVP was very direct: in MVC, the View could directly access the Model, which was too messy. The View knew the Model's data structure, and if the Model changed, the View had to change too. He wanted to solve this problem, and the method was simple — completely separate the View and Model.
In MVP, the View and Model cannot communicate directly; all interactions must go through the Presenter. The Presenter becomes the true hub, holding a reference to the Model to get data and a reference to the View's interface to control the UI.
You may have noticed that the difference between MVP and MVC is not "one more role" — the roles are still three. The difference lies in the changed communication rules between the roles:
| MVC | MVP | |
|---|---|---|
| Can View access Model? | ✅ Yes | ❌ No |
| Who controls the View? | Controller (optional) | Presenter (mandatory) |
| Relationship between View and middle layer | View directly observes Model | View implements the interface defined by Presenter |
In one sentence: MVP is MVC with a wall added, completely separating the View and Model. And this wall, for Android, came at just the right time.
In MVC, the Activity was both View and Controller, and the View and Model were not isolated. MVP solves both problems simultaneously with a clever design — introducing a View interface. In actual code, MVP requires abstracting the UI operations needed by the Activity into an interface. The Presenter controls the Activity through this interface, rather than directly manipulating the Activity itself. This way:
- Activity only acts as the View: It implements the interface defined by the Presenter, is only responsible for UI rendering, and no longer handles business logic.
- Presenter exists independently: It holds a reference to the View interface and a reference to the Model, responsible for all logic scheduling.
- View and Model are completely isolated: After the Presenter gets data from the Model, it tells the View "display these images" through the View interface. The View doesn't know where the data comes from or what the original format is.
This is the core improvement of MVP over MVC: the Activity can finally just be the View, and the Controller's responsibilities are completely handed over to the Presenter. However, MVP is not without cost. The Presenter holds a reference to the View interface, which means:
- Interface explosion: Every page needs a View interface defined, filled with methods like
showLoading(),hideLoading(),showImages(),showError(), etc. - Lifecycle issues: The Presenter holds a View reference. If it's not unbound when the Activity is destroyed, it will cause a memory leak.
- Manual UI synchronization: Every time the Presenter gets data, it must manually call
view.showXxx(), which is a lot of code.
But these costs, compared to the problem of Activities bloating to thousands of lines in MVC, were already much better. So around 2016, MVP became the most mainstream architectural choice in the Android community, adopted by a large number of open-source projects and commercial APPs.
It wasn't until 2017, when Google released Architecture Components and officially recommended moving to MVVM, that MVP gradually withdrew from the center stage. But even today, many old projects are still dominated by MVP.
Implementing the Requirement with MVP
To implement this requirement with MVP, we need to first see what structures are needed:
- Model layer, which is exactly the same as the Model in MVC, used here for API method access and parsing
- Presenter layer, the key point, controlling the entire functionality, newly added in MVP to replace the Controller in MVC
- View interface, newly added in MVP, the Presenter will hold this interface and call its methods when necessary to control the View layer
- View implementation, which is our
MainActivity, used to implement the above View interface, only responsible for UI
Below, we implement these codes one by one.
Model Layer
The Model layer is exactly the same as the Model in MVC, which is ImageModel.kt. The code is not repeated here.
View Layer
This layer needs two structures: the View interface and the View implementation.
View Interface
For a simple illustration here, we extract two methods:
interface ViewInterface {
fun showImages(imageUrlList: List<String>): Unit
fun loadError(message: String): Unit
}
View Implementation
The View implementation refers to the class that implements the ViewInterface defined above, which here is MainActivity:
class MainActivity : ComponentActivity(), ViewInterface {
private val presenter = ImagePresenter()
private val imageList = mutableStateListOf<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Bind the View so the Presenter can control the Activity through the interface
presenter.attachView(this)
enableEdgeToEdge()
setContent {
SwiftArchitectureTheme {
var keyWordIndexState by remember { mutableIntStateOf(0) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Text(
text = presenter.keyWordList[keyWordIndexState],
modifier = Modifier
.statusBarsPadding()
.fillMaxWidth()
.height(60.dp)
.wrapContentSize(Alignment.Center)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
// View only forwards the event to the Presenter. How to search and handle results is completely ignored by the View.
keyWordIndexState = Random.nextInt(0, presenter.keyWordList.size)
val keyWord = presenter.keyWordList[keyWordIndexState]
presenter.searchImage(keyWord)
}
) {
Icon(
painter = painterResource(android.R.drawable.ic_search_category_default),
contentDescription = null,
)
}
},
content = { innerPadding ->
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = innerPadding,
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalItemSpacing = 2.dp,
modifier = Modifier.fillMaxSize()
) {
items(imageList, key = { it }) { imageUrl ->
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth
)
}
}
}
)
}
}
}
override fun onDestroy() {
super.onDestroy()
// Unbind the View to prevent memory leaks
// This is the lifecycle problem of MVP: Presenter holds a View reference,
// and must be manually unbound when the Activity is destroyed.
presenter.detachView()
}
// ===== ViewInterface Implementation =====
override fun showImages(imageUrlList: List<String>) { // View only cares about "how to display", not where the data comes from
runOnUiThread {
imageList.clear()
imageList.addAll(imageUrlList)
}
}
override fun loadError(message: String) { // View only cares about "how to display errors"
runOnUiThread {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
}
Presenter Layer
The Presenter is the most important layer in MVP. Upwards, it holds the View interface, controlling the UI display; downwards, it holds the Model implementation, controlling the flow of data. The Presenter in this implementation is as follows:
class ImagePresenter {
private val imageModel = ImageModel()
private var view: ViewInterface? = null
val keyWordList: List<String> get() = imageModel.keyWordList
// Bind View
fun attachView(view: ViewInterface) {
this.view = view
}
// Unbind View
fun detachView() {
this.view = null
}
// View calls this method to trigger a search, Presenter is responsible for scheduling:
fun searchImage(keyWord: String) {
imageModel.searchImage(
keyWord = keyWord,
onSuccess = { imageUrlList ->
view?.showImages(imageUrlList)
},
onError = { message ->
view?.loadError(message)
}
)
}
}
Compared to MVC, the improvements of MVP are tangible. Let's look at a diagram:
The most critical changes are three:
First, the Activity finally only acts as the View. In MVC, the Activity was both View and Controller, with UI rendering and event handling mixed together. MVP, by introducing a View interface, lets the Activity only be responsible for implementing the interface and rendering the UI, while all business logic is handed over to the Presenter.
Second, the View and Model are completely isolated. In MVC, the Controller threw the List<String> returned by the Model directly to the Activity, so the Activity knew the data's format and structure. In MVP, the Presenter gets the data and controls the View through view.showImages(imageUrlList). The View only receives the command "display these images" and doesn't know which API the data came from or its original format.
Third, the Presenter can be independently tested. The Presenter holds a View interface, not an Activity. When writing unit tests, you only need to mock an object that implements ViewInterface, without needing to start the Android environment. This is impossible in MVC.
The difference between these two architectures can be seen in the following diagram:
The Pain Points of MVP
Every developer who uses MVP has the first impression of the MVP architecture: why write so many interfaces? This is the first pain point of MVP: interface explosion. Not only that, because the Presenter also needs to hold a reference to the View, and this View is often an object with a lifecycle like an Activity, a slight misoperation can easily cause a memory leak.
Simply put, MVP has three pain points:
- Interface explosion. Every page needs a View interface defined, filled with methods like
showLoading(),showImages(),showError(),showEmpty(), etc. A medium-sized project with dozens of pages means dozens of View interfaces and hundreds of interface methods. Most of these interface methods are mechanical boilerplate code, boring to write and annoying to maintain. - Lifecycle leaks. The Presenter holds a reference to the View. When the Activity is destroyed,
detachView()must be manually called to unbind. Forgetting to call it is a memory leak, and in asynchronous callbacks, if the View has been destroyed,view?.showImages()is either ineffective or may cause a crash. - Manual UI synchronization. Every time the Presenter gets data, it must manually call
view.showXXX(), which means the Presenter must know "which method should be called now".showImages()on success,loadError()on failure,showLoading()while loading... Theseif-elsebranches become more complex as states increase, and the Presenter itself will bloat.
MVP solved MVC's problems but left three new problems of its own. And these three problems, especially "manual UI synchronization", directly gave birth to the next generation of architecture: MVVM.
MVVM: The Official Answer
In the years when MVP dominated the Android community, Google didn't do much. The Android official documentation's description of architecture was a blank slate for a long time. Google neither recommended MVC nor MVP, basically taking a "you guys figure it out" attitude.
This is very Google. On the matter of architecture, Google's strategy has always been "let the community wade through first, then officially endorse it." So when Google finally stepped in in 2017, it didn't choose MVP but directly recommended MVVM.
Why not MVP? Because Google saw all three pain points of MVP (interface explosion, lifecycle leaks, manual UI synchronization), and at the same time, MVVM's data-driven UI approach aligned with the declarative direction Google wanted to push, so they chose MVVM.
Like MVC and MVP, MVVM also has three roles:
| Role | Responsibility | Corresponding in Android |
|---|---|---|
| Model | Data + Business Logic | Network requests, database operations, data parsing |
| View | UI Rendering + User Interaction | XML Layout / Compose UI |
| ViewModel | State Management + Business Scheduling | Jetpack ViewModel |
The biggest difference in MVVM is the ViewModel. From the name, ViewModel is the "Model of the View" — it is a Model serving the View, containing the data and state the View needs. But it is not the View, not responsible for UI rendering; it is also not the Model, not responsible for network requests and databases. It stands in the middle, managing all the state the View needs and calling the Model to get data.
But before continuing with more details of MVVM, we need to look at what Google did at that time.
Google's Architectural Exploration
2017 was a turning point. At that year's Google I/O conference, Google released Architecture Components (which later became part of Jetpack), a set of official component libraries specifically designed for Android architecture. These included:
- ViewModel: Preserves data during configuration changes (like screen rotation), not lost when the Activity is recreated
- LiveData: An observable data holder that automatically perceives the lifecycle, avoiding memory leaks
- Room: An ORM abstraction layer over SQLite, simplifying local data persistence
- Lifecycle: Allows non-lifecycle components (like Presenters) to perceive the lifecycle of Activities/Fragments
The core purpose of this set of components is one: to make the ViewModel not hold a View, and for the UI to automatically update when data changes. In MVP, the Presenter had to manually call view.showImages(); in MVVM, the ViewModel only needs to update the data, and the View automatically refreshes by observing LiveData. Therefore, LiveData is considered the foundational component of the MVVM architecture. Without Jetpack's set, MVVM couldn't be implemented on Android.
For more on Jetpack, you can read this article: Introduction to Android Jetpack: Origin and Evolution - Juejin
From MVVM to MVI
But Google's architectural evolution didn't stop at MVVM. With the release of Compose and the popularization of declarative UI, Google's architectural suggestions were quietly changing again:
- 2017: Released Architecture Components, recommending MVVM (ViewModel + LiveData + Lifecycle)
- 2019: Released Jetpack Compose, declarative UI entered the field of view
- 2021: Compose 1.0 officially released, Google began recommending the UDF (Unidirectional Data Flow) pattern
- 2023 to present: In the official architecture guide, StateFlow replaces LiveData, MVI ideas are gradually integrated
You will find that MVVM is the first Android architecture officially recommended by Google, but it is not the end. As Compose matured, Google found that declarative UI naturally suits the UDF (Unidirectional Data Flow) pattern — state flows down, events flow up. This is the core idea of MVI.
But MVI is not a completely new architecture; it's more like the evolved form of MVVM in the declarative UI era. In Google's official architecture guide, you won't even see the word "MVI" — Google uses concepts like "UDF" and "State Hoisting" to describe the same idea.
With Google's push, Android software architecture gradually shifted from the community-led MVP to the officially recommended MVVM. And after Jetpack and Compose became prevalent, the concepts of UDF and MVI also gradually penetrated into Android development.
The Origin of MVVM
Everything on Android changed rapidly in those years. I had used the MVP architecture extensively at the time. Although I couldn't say I had mastered it perfectly, I could say I knew it like the back of my hand. But Google told me that my experience was meaningless, because starting from 2020, after three years of dormancy, MVVM finally began to sweep through the Android development field like a storm.
I remember that Zhongguancun was no longer so bustling at that time, new APP projects were fewer and fewer, and the Android community gradually returned to calm. Otherwise, MVVM wouldn't have taken 3 or 4 years to become popular. When we started trying to use MVVM, the official documentation was already basically mature, but we were still very confused about some of the terms, such as two-way data binding. And this name is so important in MVVM, so we need to turn our attention to another company: Microsoft.
Android developers might think MVVM was proposed by Google and is an architectural pattern applied in the Android development field. Actually, it's not. This is another drawback of Android developers focusing only on a single field for a long time.
Born at Microsoft
MVVM is actually an invention of Microsoft. In 2005, Microsoft engineer John Gossman first proposed MVVM (Model-View-ViewModel) on his blog, originally designed for WPF (Windows Presentation Foundation). WPF has a powerful feature called Data Binding, and MVVM was designed around this feature:
- ViewModel exposes data properties, and the View automatically subscribes to these properties through data binding
- ViewModel does not need to hold a reference to the View; the UI automatically updates when data changes
- User input in the View is also automatically written back to the ViewModel through two-way binding
So MVVM has been bound to one thing since its birth: a data binding framework. Without data binding, MVVM degenerates into MVP — the ViewModel has to manually call the View's methods to update the UI, which is no different from a Presenter.
Ported by Google
Just when Android lacked an official guiding architecture, Google did one thing: port the MVVM architecture, invented by Microsoft, to Android, and implemented the data binding capability using Jetpack components.
Microsoft's WPF inherently has Data Binding, but Android does not. So for Google to land MVVM on Android, it had to build the data binding infrastructure itself. You could even say that Google launched the Architecture Components suite just to make MVVM run on Android. That is, using ViewModel + LiveData (later StateFlow) to implement the data binding capability. In other words, Google didn't invent MVVM, but built the infrastructure needed to implement MVVM for Android.
Why MVVM Must Rely on Two-Way Binding
To learn MVVM, one must understand the concept of two-way binding, which is the foundation and core of MVVM.
In MVP, the communication between Presenter and View is imperative: the Presenter calls view.showXXX, and then the View executes the related display method. This is manual UI synchronization. The Presenter must know which method to call now; every step is an explicit command.
MVVM takes a different approach: No commands, only manage data.
The ViewModel doesn't call any methods; it only maintains various states (loading state, list of images returned by the server, error information, etc.). The View observes these states, and when a state changes, the View updates automatically. The ViewModel is completely unaware of the View's existence. This requires a mechanism: when the ViewModel's data changes, the View can automatically perceive and update. This mechanism is two-way data binding (Data Binding).
The "two-way" here means:
- ViewModel --> View: Data changes, UI updates automatically
- View --> ViewModel: User inputs, data is automatically written back to the ViewModel
The core reason for this design is one thing: decoupling.
In the imperative style (MVP), the Presenter must know what methods the View has and when to call which one. The Presenter and View are tightly coupled — if the View adds a feature, the Presenter must change, and the View interface must change too.
Here we need to reach a consensus: separating code doesn't count as decoupling. Decoupling means both parties are independent of each other and can work independently without understanding any content of the other. Obviously, in MVP, the Presenter and View are not actually decoupled.
In the declarative style (MVVM), the ViewModel only cares about what the current state is, not how the View displays it. How the View renders is the View's own business. The ViewModel doesn't care, thus the ViewModel and View are decoupled.
Therefore, data binding is the prerequisite for MVVM's existence. Without data binding, the ViewModel is forced to degenerate into a Presenter, and MVVM degenerates into MVP. In one sentence: MVVM replaces the Presenter in MVP with a ViewModel, and replaces the manual "command View to update" with automatic "data-driven View update". And the mechanism to achieve this "automatic" is data binding.
Implementing the Requirement with MVVM
Model Layer
For our code, the code for the related class in the Model layer in MVVM is as follows:
class ImageRepository {
private val okHttpClient: OkHttpClient = OkHttpClient()
val keyWordList: List<String> = listOf("张含韵", "刘亦菲", "鞠婧祎", "赵丽颖", "迪丽热巴", "唐嫣", "BY2", "杨超越")
fun searchImage(
keyWord: String,
onSuccess: (List<String>) -> Unit,
onError: (String) -> Unit
) {
val url = HttpUrl.Builder()
.scheme("https")
.host("cn.apihz.cn")
.addPathSegments("/api/img/apihzimgsougou.php")
.addQueryParameter("id", "YOUR_API_ID")
.addQueryParameter("key", "YOUR_API_KEY")
.addQueryParameter("words", keyWord)
.build()
val request = Request.Builder().url(url).build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
onError(e.localizedMessage ?: "网络请求失败")
}
override fun onResponse(call: Call, response: Response) {
try {
val body = response.body?.string()
if (body.isNullOrEmpty()) {
onError("服务器返回为空")
return
}
val responseObject = JSONObject(body)
val resImageList = responseObject.optJSONArray("res")
if (resImageList == null) {
onError("服务器返回格式异常")
return
}
val imageUrls = mutableListOf<String>()
for (index in 0 until resImageList.length()) {
imageUrls.add(resImageList.getString(index))
}
onSuccess(imageUrls)
} catch (e: Exception) {
onError("数据解析失败: ${e.message}")
}
}
})
}
}
Astute readers may have noticed that we didn't use ImageModel here, but instead ImageRepository. This is not just a name change.
In Google's official architecture, the Repository is the entry point of the Model layer. Its responsibility is to encapsulate all data sources — network requests, databases, caches — and decide where to get data from. The ViewModel does not directly initiate network requests but gets data through the Repository.
Why have this layer? Because in real projects, data sources are complex: the same data might be read from the local database first, requested from the network if not present, and written to the database once fetched. If the ViewModel directly called the network request, adding a cache would require changing the ViewModel; if the ViewModel directly called the database, changing the API would require changing the ViewModel. The Repository encapsulates these complex logics, so the ViewModel just calls repository.getImages() and doesn't care where the data comes from.
In our simple requirement, the Repository only does network requests, seeming no different from the ImageModel in MVC. But when requirements become complex, the value of the Repository will be evident.
So, the Repository is the Model layer in Android's MVVM — it inherits the responsibilities of the Model in MVC (handling data), just with a more precise name. Repository, literally translated, means "warehouse", i.e., a data warehouse.
View Layer
In MVVM, the View layer is the Activity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SwiftArchitectureTheme {
// Get the ViewModel instance via viewModel()
val viewModel: ImageViewModel = viewModel()
ImageScreen(viewModel)
}
}
}
}
@Composable
fun ImageScreen(viewModel: ImageViewModel) {
// Observe the ViewModel's state; Compose automatically recomposes when state changes
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val keyWord by viewModel.keyWord.collectAsStateWithLifecycle()
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Text(
text = keyWord,
modifier = Modifier
.statusBarsPadding()
.fillMaxWidth()
.height(60.dp)
.wrapContentSize(Alignment.Center)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
// View only passes user intent to ViewModel. How to search and handle results is completely ignored by the View.
val keyWord = viewModel.getRandomKeyWord()
viewModel.searchImage(keyWord)
}
) {
Icon(
painter = painterResource(android.R.drawable.ic_search_category_default),
contentDescription = null,
)
}
}
) { innerPadding ->
// Render different UI based on state. In MVVM, this is centralized in a when expression, making all states clear at a glance and preventing omissions.
when (val state = uiState) {
is ImageViewModel.UiState.Idle -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("点击搜索按钮开始")
}
}
is ImageViewModel.UiState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is ImageViewModel.UiState.Success -> {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = innerPadding,
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalItemSpacing = 2.dp,
modifier = Modifier.fillMaxSize()
) {
items(state.imageUrlList, key = { it }) { imageUrl ->
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth
)
}
}
}
is ImageViewModel.UiState.Error -> {
val context = LocalContext.current
Toast.makeText(context, state.message, Toast.LENGTH_SHORT).show()
}
}
}
}
ViewModel Layer
This is the core of the entire MVVM. It does not directly call network requests but gets data through the Repository. It also doesn't handle any UI-related code and is mainly a container for various states:
class ImageViewModel : ViewModel() {
// ViewModel gets data through Repository, does not directly initiate network requests. In MVVM, ViewModel directly holds ImageRepository.
private val repository = ImageRepository()
private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// Current search keyword, used by View to display in the title bar
private val _keyWord = MutableStateFlow("")
val keyWord: StateFlow<String> = _keyWord.asStateFlow()
// Search for images. View calls this method to trigger a search, but View doesn't care how the result is handled.
fun searchImage(keyWord: String) {
_keyWord.value = keyWord
_uiState.value = UiState.Loading
// Launch a coroutine with viewModelScope, automatically cancelled when ViewModel is destroyed. This solves MVP's lifecycle problem: no need to manually manage async tasks.
viewModelScope.launch {
repository.searchImage(
keyWord = keyWord,
onSuccess = { imageUrlList ->
_uiState.value = UiState.Success(imageUrlList)
},
onError = { message ->
_uiState.value = UiState.Error(message)
}
)
}
}
fun getRandomKeyWord(): String {
return repository.keyWordList.random()
}
// UI State sealed class
sealed class UiState {
object Idle : UiState()
object Loading : UiState()
data class Success(val imageUrlList: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
}
Summary
Writing up to here, the three architectures MVC, MVP, and MVVM have been covered. From 1978 to today, nearly 50 years, the evolution logic of the three architectures is actually very simple:
- MVC split the code into three parts, but the View and Model weren't cleanly separated
- MVP used an interface to completely separate the View and Model, but the Presenter took on the heavy burden of manual UI synchronization
- MVVM used data binding to turn manual synchronization into automatic driving, the Presenter exited the stage, and the ViewModel came on
No architecture is perfect; each one solves the problems of the previous one while leaving new problems behind. This is not a failure of software engineering; this is the essence of software engineering — there is no silver bullet.
From MVC to MVP to MVVM, the changes in software architecture on Android seem to mirror the wave of China's mobile internet. From a hundred schools of thought contending at the beginning, to the official norms unifying the landscape in the end, discussions on architecture have also fallen silent, just like the current Android market.
And MVVM is not the end. When Compose replaced XML as Android's UI framework, declarative UI made "state-driven" unprecedentedly natural. Google began recommending UDF (Unidirectional Data Flow) — state flows down, events flow up. The community calls this pattern MVI, but Google itself never uses this term; they prefer to call it "state hoisting" and "unidirectional data flow".
The relationship between MVI and MVVM is like the relationship between MVP and MVC — not an overthrow, but an evolution. The ViewModel is still there, the Repository is still there, it's just that the way of state management is more rigorous: all state changes must go through a unified entry point, and all UI changes are driven by a single source of truth. But that is another story.
What the future development of Android software architecture will be like, we cannot know, just as we cannot predict the future of the Android market — whether it will prosper again or become historical ruins, we can only wait and see.
But no matter how things change, the content in this article is still useful for us developers. Not just for the Android platform, but for any GUI software, the three architectures or their variants in this article are needed. Technology becomes obsolete, but the thinking behind solving problems does not. MVC taught me layering, MVP taught me isolation, MVVM taught me data-driven thinking. These ways of thinking, even if Compose is eliminated one day, even if Google invents some new framework, will still be useful. Just like the sentence at the beginning:
Those underlying, long-lasting things are the true path for technology developers.