从历史的角度看 Android 软件架构

前言

两个月前,公司开辟了一个新的业务,我很高兴地参与了其中,去开发一个新的 APP,毕竟在移动互联网已经熄火多年的大背景下,能有一个开发新 APP 的机会确实不多,大量如我一样的 Android 程序员,不是去做车机,就是去做 Framework 了(而且很多还是外包)。

不过事实证明我高兴的太早了。

新的 APP 就要有新的要求,其中就包括 Compose、Jetpack、MVVM 这一套,这对于我这种一直使用 Java + XML 的老古董来说,机会也意味着挑战,我不得不开始学习这些新的内容。

说实话,我是很排斥学习 Google 提供的新技术的,不是因为个人懒惰,而是因为 Google 对于 Android 开发太坑了。很多技术发布时,被吹得天花乱坠,但当你花费时间认真学习之后,却发现它已经被废弃了。甚至有很多东西的推出明显是没有过 Google 的脑子的,或者说 Google 这家公司对 Android 技术栈这块,一直都缺乏长远规划。

这不是段子,这是每个 Android 开发者的亲身经历。例如,做 Android 比较久的,肯定知道下面这些内容:

  • AsyncTask------每个初学者都写过,11 年后废弃
  • Kotlin Android Extensions------"告别 findViewById",3 年就废弃
  • IntentService------它废了,替代方案 JobIntentService 也废了,一条线废两代
  • DataBinding------学了 9 年,官方 Codelab 标上 Deprecated
  • LiveData------还没正式废弃,官方示例已全面转向 StateFlow
  • Volley(被 OkHttp 替代)、ListView(被 RecyclerView 替代)、ViewPager(ViewPager2、Compose)
  • ......

这些东西,每一个都是发布时迅速成为了 Android 开发的事实标准,但不多久就被官方抛弃,即使有些没有被废弃,但实际上来讲已经成为了历史。

人生苦短,时间宝贵。我们不应该在这些生命短暂如蜉蝣的东西上浪费精力,那些在底层长久不变的东西,才是技术开发者的大道。 但 Google 不像是一个喜欢沉淀的公司,至少在 Android 上不是。它更喜欢在表面上做一些花里胡哨的文章,换一套 API 让你重新学习,如果换不动,就直接废弃。以至于用 Kotlin 替代了 Java,用 Compose 替代了 View 体系------虽然旧的没被正式废弃,但所有人都知道,Kotlin 优先。

不过话说回来,Android 是 Google 的技术,其开发生态也被 Google 紧紧控制着。无论如何,作为一个 Android 开发者(至少现在还是),Kotlin + Compose + Jetpack + MVVM 这一套东西,也得开始学了。

原本我正按照 Jetpack 的库来进行学习,并输出文章。就是这个系列:Android Jetpack - 李斯维的专栏 - 掘金

但当我正尝试输出到 ViewModel 时,我发现了一个问题,ViewModel 的加入会明显改变 APP 架构,与其在写 ViewModel 时粗略地带过软件架构的主题,不如直接写一篇文章将软件架构讲清楚。于是乎便有了这篇文章:从历史的角度来看 Android 软件架构的发展,从 MVC 到 MVP 再到 MVVM,通过一个需求来说明几个架构之间的异同。虽然以 Android 为例,但 MVC、MVP、MVVM 这些架构思想适用于所有 GUI 程序。

没有架构的蛮荒时代

那是 2014年的夏天,北京的天空总是灰蒙蒙的,但一切看起来却那么欣欣向荣,食宝街里摩肩接踵,创业街上人声鼎沸。那是最好的时代,好到只要你能说出 Activity 的生命周期,就能谋一个 Android 程序员的岗位;那也是最坏的时代,坏到所有人都在移动互联网浪潮的裹挟下快速产出,却忽略了软件架构重要性,在不断迭代中积累着技术债务。

这是 Android 开发的蛮荒时代,而蛮荒时代,不需要架构。在此时的软件,顶多就是用一些惯用法,再加上一些设计模式,若你能把 MVC 讲清楚,那么你就具备一个团队 Leader 的潜质。

既然说到这里,我们就解释一下惯用法、设计模式、软件架构这三个名词。

惯用法、设计模式、软件架构

惯用法、设计模式、软件架构,这三者是递进关系,但递进的方向不是越来越复杂,而是其作用范围越来越大。

惯用法是解决某个具体语言的具体问题的固定写法,它和语言强绑定,换一门语言可能就不存在了。其作用范围很小,也就是几行到几十行代码,不过程序员的差别也就体现在这里。

举个常见的惯用法的例子:

kotlin 复制代码
// Kotlin 惯用法:用 let 做 null 安全调用
user?.let { sendEmail(it) }
kotlin 复制代码
// Kotlin 惯用法:用 apply 初始化对象
val dialog = AlertDialog.Builder(this).apply {
    setTitle("提示")
    setMessage("确定删除?")
    setPositiveButton("确定") { _, _ -> delete() }
}
java 复制代码
// Java 惯用法:双重检查锁单例
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;
    }
}

设计模式是解决某一类反复出现的设计问题的通用方案。它与语言无关,描述的是类与类之间的协作关系,也就是怎么组织类,而不是怎么写类中的代码。其作用范围是模块级,涉及几个类的关系。

例如在 Android 中,你可以很容易地见到下面的这些设计模式的运用:

模式 解决什么问题 Android 中的例子
观察者 一个对象变化时通知多个依赖方 LiveData、Flow、EventBus
单例 全局只需要一个实例 Application、RoomDatabase
工厂 创建对象时不暴露具体类 ViewModelProvider.Factory
策略 算法可以随时替换 RecyclerView.LayoutManager
适配器 接口不兼容时做转换 RecyclerView.Adapter
代理 不直接访问对象,通过代理控制 Retrofit 动态代理

关于设计模式,这里有本非常有用的书籍:设计模式 (豆瓣)(评分9.3)

软件架构决定系统整体如何划分模块、模块之间如何通信,数据如何流动。它是最高层级的结构决策,也是这篇文章要讨论的内容。

软件架构的作用范围是整个系统,一旦确定下来,改动成本极高。通过了解一个系统的软件架构,你可以知道这个系统长什么样子。就像本文中将要讨论的这几个架构:

架构 核心思想
MVC Model/View/Controller 三层分离,View 观察 Model
MVP View 和 Model 完全隔离,Presenter 控制一切
MVVM ViewModel 不持有 View,数据驱动 UI

架构是最高层级的决策,一旦选错了,设计模式和惯用法写得再漂亮也无济于事。反过来,架构选对了,具体用哪个设计模式、哪种惯用法,都可以慢慢优化。

所以架构重要,不是因为它是最高级的,而是因为它是最难改的。

如果用一个类比来解释这三个名词,那么可以考虑建一栋大楼:

  • 惯用法:砌墙时砖怎么交错排列、钢筋怎么绑扎------这是具体工法的固定套路
  • 设计模式:遇到大跨度空间,用拱还是用梁------这是反复出现的结构问题的通用解法
  • 软件架构:这栋楼是写字楼还是住宅,几梯几户,水电走明线还是暗线------这决定了整栋楼的骨架

一个建筑设计师设计失误,那么泥瓦匠刷墙刷得再好,也无济于事。此处必须参考比萨斜塔,自建成之后一直修修补补,都快修不动了。

定义一个需求

回首那个夏天,我也是在那时进入到 Android 开发岗位。那真是移动互联网的黄金时期,满大街都在聊着创业,而创业的第一步似乎都是要做一个 APP。

我仍然记得当时做的一个图片列表的页面,需求很简单:

这个页面显示用户发表过的图片,当进入这个页面时,能以瀑布流的形式展示这些图片。并且点击图片能够放大展示,而点击图片下面的文字则能进入到对应的内容页面中。

我把所有代码写进了 ImageActivity:UI 渲染、按钮点击、网络请求、图片显示,跳转逻辑,全在一个 Activity 里。能跑。

然而随着需求迭代:"加载中要显示 loading"、"加载失败要提示错误并能重试"、"加个缓存别每次都请求"、"图片要支持缩放"、"加个分享按钮"......每次改动都要在那坨代码里翻来翻去,改一处怕坏三处。到后来,这个 ImageActivity 突破了两千行,没人愿意碰它。

这不是我一个人的故事。那几年,几乎每个 Android 开发者都经历过同样的事------在一个巨大的 Activity 里挣扎,在无尽的回调里迷路,在"改个小需求"和"牵一发动全身"之间反复横跳。这就是我们缺少一种组织代码的方式,带来的技术债务。而这个"方式",就是架构。

从 1978 年 MVC 诞生,到 1996 年 MVP 出现,再到 2005 年 MVVM 登场,直到今天 MVI 思想融入日常------每一种架构都不是凭空发明的,它们是对前一种架构痛点的回应,是无数开发者在真实业务中碰壁后的反思。

这篇文章,我想用一个贯穿始终的需求,带你走一遍这条路。但是时间久远,使用多年前的图片列表需求已经不再现实,所以我们就用下面这样的一个类似的需求来演示这些软件架构之间的差异:

页面中存在一个按钮,用户点击这个按钮,将访问服务器拿到一些图片并显示到屏幕上。

这个需求虽然简单,但是足以演示架构之间的差异。在每一次我们实现这个需求后,将得到下面的效果。

应用的右下角,有一个搜索图标,点击之后将通过接口获取内置的几个明星的图片,并通过瀑布流显示出来。

无架构的代码结构

在移动互联网早期,很少有哪个 APP 有架构设计。对于上述的功能,基本就是所有的代码都放到 Activity。之所以是 Activity 而不是其他,是因为 Activity 是 Android 开发中的界面展示类,毕竟是 GUI 程序,基本可以看作所有的界面都从 Activity 开始。

kotlin 复制代码
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))
                        }
                    }
                }
            }
        })
    }
}

上面是这个功能的所有的代码,功能不大,所以不到 100 行代码就能完成。但是你会发现,这里包含了关联不大的代码。例如,界面显示、网络请求,接口解析。虽然目前代码不多,将这些代码揉在一起也不会很乱,但是当项目逐渐增大时,这样的代码将会变成一个灾难。

而现在的这种简洁的代码结构和写法,也只不过是在积累技术债务而已。因此,我们急需一个软件架构,而第一个进入我们视线的架构,就是 MVC。

MVC:架构的起点

蛮荒时代持续了好几年,它带来的技术债务恰好被移动互联网的繁荣所掩盖。但到了 2014、2015 年左右,开发者发现,再不讨论软件架构问题,那么自己项目的崩塌就是迟早的事了。原因很简单:该还债了。

APP 越做越大。一个电商 APP 随便几十个页面,每个 Activity 动辄上千行,需求迭代几个版本后就没人能看懂了。接手别人的代码?那更是噩梦。社区里开始出现大量关于"如何组织 Android 代码"的讨论。

于是,人们把目光投向了一个已经存在了 50 多年的老概念------MVC。

1978 年,挪威计算机科学家 Trygve Reenskaug 在施乐为 Smalltalk-76 系统设计了 MVC。最初的目的很简单:

把数据和展示分开,让同一份数据可以有多个不同的视图。

这是 GUI 软件架构的开山之作,距今已经过去了半个世纪。

MVC 的核心结构

MVC 把软件分成三层:

角色 职责 在 Android 中对应
Model 数据 + 业务逻辑 网络请求、数据库操作、数据解析
View UI 渲染 XML 布局 / Compose UI
Controller 接收用户输入,调用 Model,决定展示什么 Activity / Fragment

其核心交互流程就是:

rust 复制代码
用户操作 --> Controller --> Model(处理业务)--> Model 通知 View 更新 --> View 读取 Model 数据 --> 刷新 UI

于是,便有了我们常看到的这张图:

这里有一个关键设计:View 可以直接访问 Model。当 Model 发生改变时 View 能感知到,并主动更新自己。这是 MVC 最核心的特点,也是后来最大的争议点。

对比无架构,MVC 的改进是显而易见的:

  • 职责分离:网络请求不再是 Activity 的一部分,而是独立的 Model 层。Activity 不再同时是运动员和裁判员。
  • Model 可复用:图片获取的逻辑抽出来后,其他页面也可以调用。
  • 可测试性提升:Model 不依赖 UI,可以独立写单元测试。

Model、View、Controller 三个词的由来

在这里,我觉得有必要来解释一下 MVC 架构中的这三个词:Model、View、Controller,特别是 Model,这是一个需要理解的词语。

  • View:这个最直观,翻译就是视图的意思,对应 UI。
  • Controller:翻译就是控制器。控制器,其作用为接收输入、决定做什么。

而 Model 这个词,就需要好好解释解释了。

在 MVC 的原始论文中,对 Model 的定义是:

A model is an internal representation of some aspect of the world. 模型是对世界某个方面的内部表示。

我们先想想各行各业中的模型是什么意思:

  • 建筑模型:不是真实的楼,但精确地表示了楼的结构
  • 天气模型:不是天气本身,但用数学方式表示了天气的运行规律
  • 经济模型:不是经济本身,但抽象了经济中的关键要素和关系

一般来说,我们所说的模型,其实就是对真实事物的抽象表示。

在软件中,真实事物就是我们所关注的业务领域,比如说电商系统中的商品、订单、用户;教育系统中的考题、学生、教学。这些业务概念在代码中的表示,就是 Model,也就是模型。

而作为对业务的抽象表示,Model 不仅仅表示数据,它还需要表示业务逻辑。即:

Model = 业务知识的数据表示 + 操作这些数据的业务逻辑

例如电商系统中,订单的ID、金额、状态是数据,而它的业务规则,例如什么状态时可以取消,取消后订单会是什么状态,这些都是属于 Model 中的内容。

如果只有数据没有业务规则,那叫做 DTO(Data Transfer Object)或 Entity,不叫 Model。

但是作为 Android 开发者,我们往往会错误地认为 Model 就是数据层。之所以很多 Android 开发者会有这样的错误认知,是因为在实际项目中,业务逻辑往往和后端 API 强绑定。Android 端拿到的是后端算好的结果,本地不需要太多业务规则。所以 Android 中的 Model 通常退化为:网络请求 + 数据解析 + 数据缓存,简单点来说,在 Android 的语境下,Model 就是负责搞定数据的那一层。

可见,这个错误的认知其实就是开发者仅局限于 Android 开发,才会产生的误解。所以,我们还是要广泛涉猎各种知识啊。

Model 通常被翻译为"模型",这个词在中文里显得有些抽象,翻译得确实不怎么行,太泛了。如果当初翻译成"业务层"或"领域层"(Domain),可能就不会有这个困惑了。实际上,在"领域驱动设计"(DDD)中,类似的概念就叫 Domain Model,中文常翻译为"领域模型",理解起来就清晰很多。而在后续 Google 推荐的 Android 架构中,也引入了 Domain 这一层。

用 MVC 实现需求

现在我们把话题转回来,用 MVC 架构实现我们的需求,看一看 MVC 对软件有什么改进。

按照 MVC 的思路,我们把前面的无架构代码拆开:

  • Model:把网络请求和 JSON 解析抽出来,放到一个独立的类里
  • View:Compose UI 负责渲染,不关心数据从哪来
  • Controller:Activity 负责接收用户点击,调用 Model 拿数据,然后更新 View

Model 层

在此需求中的 Model 对应着对接口的请求与解析,也就是前面 MainActivity 中的 searchImage 方法。这里我们将其抽出来作为一个单独的 Model 类:

kotlin 复制代码
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 层

Controller 独立出来,持有 Model 的引用,负责调用 Model、把结果通过回调传给 View。但注意,用户事件的入口在 View 的 onClick 中,View 拿到事件后调用 Controller------这就是 MVC 在 Android 上的妥协:Controller 没法独立接收事件,只能靠 View 来转发。

kotlin 复制代码
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)
    }
}

Controller 目前看起来只是转发了一下调用,似乎没有存在的必要。但在真实项目中,Controller 会负责更多的事情:协调多个 Model、处理业务逻辑的组合、决定展示哪个 View。在这个简单需求中它的价值不明显,但当需求变复杂时,Controller 的协调作用就会体现出来。

View 层

在 MVC 的理想模型中,View 应该是独立的,仅仅负责将数据渲染到界面上。用户的输入事件先到 Controller,由 Controller 决定调用谁。

但在 Android 中,这显然是不可能的。Activity 在设计上就是一个全权管家------它是界面容器,又是事件入口,又是生命周期管理者。MVC 要求 View 和 Controller 分离,但 Activity 的设计不允许它只扮演一个角色。所以 Android 上的 MVC 本质上是一个变种:Activity 被迫同时扮演 View 和 Controller,View 的角色是将图片渲染到界面上,Controller 的角色是接收用户事件并调用 Model。有些 MVC 项目中甚至看不到独立的 Controller 类,那就是直接把它合并到 Activity 中了。

kotlin 复制代码
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
                                )
                            }
                        }
                    })
            }
        }
    }
}

在经过 MVC 架构拆分后,我们将所有代码都揉在 MainActivity 的结构分为三层,这三层之间的交互为:用户点击 View,View 调用 Controller,由 Controller 调用 Model,最后 Model 的结果逐级返回,最终展示在界面上的。

于是我们得到了这三者的依赖关系图,如下:

MVC 的痛点

看起来 MVC 解决了无架构的问题------代码分了三层,各司其职。但如果你仔细看上面的代码,会发现两个问题:

  • 第一,Activity 既是 View 又是 Controller。 UI 渲染是 View 的活,接收事件、调用 Model、处理回调是 Controller 的活,全在 MainActivity 里。
  • 第二,View 和 Model 没有真正隔离。 Controller 把 Model 返回的 List<String> 直接传给了 Activity(View),View 知道了 Model 的数据结构。一旦 Model 的返回格式变了,View 就得跟着改。

这两个问题,在简单需求中不明显,但当页面变复杂、需求反复迭代时,Activity 会再次膨胀,View 和 Model 的耦合会越来越紧。这就是 MVC 在 Android 上的先天缺陷,也是 MVP 诞生的直接原因。

MVP

MVC 是最先在 Android 上广泛运用的架构。在移动互联网早期,Android 开发基本就是这个状态------Activity 里堆代码,顶多把网络请求单独封装一下,作为 Model 层与 Activity 进行隔离。但 MVC 在 Android 上表现得并不好,Activity 还是臃肿的,View 和 Controller 还是耦合的。于是到了 2016 年左右,Android 社区开始出现一个词:MVP。

我接触 Android 架构最多的时期,就是 MVP 大行其道那几年。我记得当时在做一系列教育相关的 APP,那时 MVC 在 Android 上已经开始让位于 MVP。逻辑控制开始从 Controller 向 Presenter 迁移,Activity 不再那么臃肿,整个应用的代码开始围绕着 Presenter 构建。Presenter 调用 Model 获取数据,同时持有 View 的接口引用,但 View 必须实现 Presenter 规定的接口,比如 showLoading() 和 showImages() 等。

也正是因为使用了 MVP,我那些年做的项目也没出现改不动的情况,相比于 MVC 在 Android 上扭扭捏捏的表现,MVP 对于架构的改进真的是众望所归, 而 MVP 在那时已经成为了 Android 架构的事实标准。

MVP 的结构

不过要说起 MVP 的起源,却要再往前推30年。1996 年,Mike Potel 在 Taligent(IBM 和 Apple 的合资公司)提出了 MVP。那时候 MVC 才刚满 18 岁。

Potel 提出 MVP 的动机很直接:MVC 中 View 可以直接访问 Model,这太乱了。View 知道 Model 的数据结构,Model 一变 View 就得跟着改。他想解决这个问题,方法也很简单------把 View 和 Model 彻底隔开。

在 MVP 中,View 和 Model 不能直接通信,所有交互都必须经过 Presenter。Presenter 成了真正的中枢,它持有 Model 的引用来获取数据,同时持有 View 的接口引用来控制 UI。

你可能注意到了,MVP 和 MVC 的区别不在于"多了一个角色"------角色还是三个。区别在于角色之间的通信规则变了:

MVC MVP
View 能否访问 Model ✅ 可以 ❌ 不可以
谁控制 View Controller(可选) Presenter(必须)
View 和中间层的关系 View 直接观察 Model View 实现 Presenter 定义的接口

一句话总结:MVP 就是 MVC 加了一道墙,把 View 和 Model 彻底隔开。而这道墙,对 Android 来说,来得正是时候。

在 MVC 中,Activity 既是 View 又是 Controller,View 和 Model 没有隔离。MVP 用一个巧妙的设计同时解决这两个问题 ------ 引入 View 接口。在实际代码中,MVP 需要把 Activity 需要的 UI 操作抽象成一个接口,Presenter 通过这个接口控制 Activity,而不是直接操作 Activity 本身。这样一来:

  • Activity 只当 View:实现 Presenter 定义的接口,只负责 UI 渲染,不再处理业务逻辑
  • Presenter 独立存在:持有 View 接口引用和 Model 引用,负责所有逻辑调度
  • View 和 Model 完全隔离:Presenter 拿到 Model 的数据后,通过 View 接口告诉 View "显示这些图片",View 不知道数据从哪来、格式是什么

这就是 MVP 对 MVC 的核心改进:Activity 终于可以只当 View 了,Controller 的职责被彻底移交给了 Presenter。 不过,MVP 也不是没有代价。Presenter 持有 View 的接口引用,这意味着:

  • 接口爆炸:每个页面都要定义一个 View 接口,接口里列满 showLoading()、hideLoading()、showImages()、showError() 等方法
  • 生命周期问题:Presenter 持有 View 引用,Activity 销毁时如果没解绑,就会内存泄漏
  • 手动同步 UI:Presenter 每次拿到数据都要手动调 view.showXxx(),代码量不小

但这些代价,比起 MVC 中 Activity 膨胀成几千行的问题,已经好太多了。所以在 2016 年左右,MVP 成了 Android 社区最主流的架构选择,大量开源项目和商业 APP 都采用了 MVP。

直到 2017 年,Google 发布了 Architecture Components,官方推荐转向 MVVM,MVP 才逐渐退出舞台中心。但即使到今天,很多老项目里仍然是 MVP 的天下。

用 MVP 实现需求

要用 MVP 来实现这个需求,我们需要先看看需要哪些结构:

  • Model 层,这个与 MVC 中的 Model 是完全一样的,在这里用于接口方法访问和解析
  • Presenter 层,重点,把控整个功能,MVP 新增的,用于替代 MVC 中的 Controller
  • View 接口,MVP 新增,Presenter 将持有这个接口并在必要时调用其中方法以控制 View 层
  • View 实现,也就是我们的 MainActivity,用于实现上面的 View 接口,只负责 UI

下面我们在逐一实现这些代码。

Model 层

Model 层与 MVC 中的 Model 完全一样,就是 ImageModel.kt,这里代码就不重复了。

View 层

这一层需要有两个结构:View 接口和 View 实现。

View 接口

这里为了简单的说明,我们就抽出来两个方法:

kotlin 复制代码
interface ViewInterface {
    fun showImages(imageUrlList: List<String>): Unit
    fun loadError(message: String): Unit
}
View 实现

View 实现就是指的是实现了上面定义的 ViewInterface,在这里就是 MainActivity

kotlin 复制代码
class MainActivity : ComponentActivity(), ViewInterface {

    private val presenter = ImagePresenter()
    private val imageList = mutableStateListOf<String>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 绑定 View,让 Presenter 可以通过接口控制 Activity
        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 只负责把事件转发给 Presenter, 具体怎么搜索、怎么处理结果,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()
        // 解绑 View,防止内存泄漏
        // 这就是 MVP 的生命周期问题:Presenter 持有 View 引用,
        // Activity 销毁时必须手动解绑
        presenter.detachView()
    }

    // ===== ViewInterface 实现 =====
    override fun showImages(imageUrlList: List<String>) { // View 只管"怎么显示",不管数据从哪来
        runOnUiThread {
            imageList.clear()
            imageList.addAll(imageUrlList)
        }
    }

    override fun loadError(message: String) { // View 只管"怎么显示错误"
        runOnUiThread {
            Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
        }
    }
}

Presenter 层

Presenter 是 MVP 中最重要的层次,往上其持有 View 接口,控制 UI 显示,往下持有 Model 的实现,控制数据的流动。在这个实现中的 Presenter 如下:

kotlin 复制代码
class ImagePresenter {

    private val imageModel = ImageModel()
    private var view: ViewInterface? = null

    val keyWordList: List<String> get() = imageModel.keyWordList

    // 绑定 View
    fun attachView(view: ViewInterface) {
        this.view = view
    }

    // 解绑 View
    fun detachView() {
        this.view = null
    }

    // View 调用这个方法触发搜索,Presenter 负责调度:
    fun searchImage(keyWord: String) {
        imageModel.searchImage(
            keyWord = keyWord,
            onSuccess = { imageUrlList ->
                view?.showImages(imageUrlList)
            },
            onError = { message ->
                view?.loadError(message)
            }
        )
    }
}

对比 MVC,MVP 的改进是实实在在的。我们用一张图来看:

最关键的变化有三点:

第一,Activity 终于只当 View 了。 在 MVC 中,Activity 既是 View 又是 Controller,UI 渲染和事件处理搅在一起。MVP 通过引入 View 接口,让 Activity 只负责实现接口、渲染 UI,业务逻辑全部移交给了 Presenter。

第二,View 和 Model 彻底隔离了。 在 MVC 中,Controller 把 Model 返回的 List<String> 直接丢给 Activity,Activity 知道数据的格式和结构。而在 MVP 中,Presenter 拿到数据后通过 view.showImages(imageUrlList) 控制 View,View 只收到"显示这些图片"的指令,不知道数据从哪个接口来、原始格式是什么。

第三,Presenter 可独立测试。 Presenter 持有的是 View 接口而非 Activity,写单元测试时只需 mock 一个实现了 ViewInterface 的对象,不需要启动 Android 环境。这在 MVC 中是做不到的。

这两个架构的差别,可以从下图看出:

MVP 的痛点

每一个使用 MVP 的开发者,对 MVP 架构的第一印象就是:为什么要写这么多接口。这就是 MVP 的第一个痛点:接口爆炸。不仅如此,由于 Presenter 还需要持有 View 的引用,而这个 View 往往就是 Activity 这些具有生命周期的对象,操作稍有不对,就容易造成内存泄漏。

简单点来说,MVP 的痛点有三个:

  • 接口爆炸。 每个页面都要定义一个 View 接口,接口里列满了 showLoading()showImages()showError()showEmpty() 等方法。一个中型项目几十个页面,就是几十个 View 接口,几百个接口方法。这些接口方法大多是机械式的样板代码,写起来无聊,维护起来烦人。
  • 生命周期泄漏。 Presenter 持有 View 的引用,Activity 销毁时必须手动调 detachView() 解绑。忘了调就是内存泄漏,而且在异步回调中如果 View 已被销毁,view?.showImages() 要么无效,要么可能导致崩溃。
  • 手动同步 UI。 Presenter 每次拿到数据都要手动调 view.showXXX(),这意味着 Presenter 必须知道"当前应该调哪个方法"。成功了调 showImages(),失败了调 loadError(),加载中调 showLoading()...... 这些 if-else 分支随着状态增多会越来越复杂,Presenter 自己也会膨胀。

MVP 解决了 MVC 的问题,但自己又留下了三个新问题。而这三个问题,尤其是"手动同步 UI",直接催生了下一代架构:MVVM。

MVVM:官方答案

在 MVP 统治 Android 社区的那几年里,Google 一直没什么动作。Android 官方文档里对架构的描述长期是一片空白,Google 既没有推荐 MVC,也没有推荐 MVP,基本上是"你们自己看着办"的态度。

这很 Google。在架构这件事上,Google 的策略一直是"先让社区趟路,趟完了再官方背书"。所以当 2017 年 Google 终于下场的时候,它没有选择 MVP,而是直接推荐了 MVVM。

为什么不是 MVP?因为 MVP 的三个痛点(接口爆炸、生命周期泄漏、手动同步 UI)Google 都看在眼里,而同时 MVVM 的数据驱动 UI 思路和 Google 想推的声明式方向一致,所以选择了 MVVM。

和 MVC、MVP 一样,MVVM 也是三个角色:

角色 职责 在 Android 中对应
Model 数据 + 业务逻辑 网络请求、数据库操作、数据解析
View UI 渲染 + 用户交互 XML 布局 / Compose UI
ViewModel 状态管理 + 业务调度 Jetpack ViewModel

MVVM 中最大的区别就是 ViewModel。从名字上看,ViewModel 就是"View 的 Model"------它是为 View 服务的 Model,里面装的是 View 需要的数据和状态。但它不是 View,不负责 UI 渲染;它也不是 Model,不负责网络请求和数据库。它站在中间,管理着 View 需要的所有状态,并调用 Model 获取数据。

但在继续讲 MVVM 的更多细节之前,我们需要先看看 Google 在当时做了什么。

Google 的架构探索

2017 年是一个转折点。这一年的 Google I/O 大会上,Google 发布了 Architecture Components(后来成为 Jetpack 的一部分),这是一套专门为 Android 架构设计的官方组件库。其中包括:

  • ViewModel:在配置变更(如屏幕旋转)时保留数据,不随 Activity 重建而丢失
  • LiveData:一个可观察的数据持有者,自动感知生命周期,避免内存泄漏
  • Room:在 SQLite 之上的 ORM 抽象层,简化本地数据持久化
  • Lifecycle:让非生命周期组件(如 Presenter)能感知 Activity/Fragment 的生命周期

这套组件的核心目的只有一个:让 ViewModel 不持有 View,数据变化时 UI 自动更新。在 MVP 中,Presenter 要手动调 view.showImages();而在 MVVM 中,ViewModel 只需要更新数据,View 通过观察 LiveData 自动刷新。因此,LiveData 算是 MVVM 架构的基础组件,没有 Jetpack 的那一套,MVVM 就不可能在 Android 上实现。

关于 Jetpack 的更多内容,可以看这篇文章:Android Jetpack 简介:由来和演进 - 掘金

从 MVVM 到 MVI

不过 Google 的架构演进并没有停在 MVVM。随着 Compose 的发布和声明式 UI 的普及,Google 的架构建议又在悄悄变化:

  • 2017 年:发布 Architecture Components,推荐 MVVM(ViewModel + LiveData + Lifecycle)
  • 2019 年:发布 Jetpack Compose,声明式 UI 进入视野
  • 2021 年:Compose 1.0 正式发布,Google 开始推荐 UDF(单向数据流)模式
  • 2023 年至今:官方架构指南中,StateFlow 替代 LiveData,MVI 思想逐渐融入

你会发现,MVVM 是 Google 官方推荐的第一套 Android 架构,但它不是终点。随着 Compose 的成熟,Google 发现声明式 UI 天然适合 UDF(单向数据流)模式------状态向下流动,事件向上传递。这就是 MVI 的核心思想。

但 MVI 并不是一个全新的架构,它更像是 MVVM 在声明式 UI 时代的进化形态。在 Google 的官方架构指南中,你甚至看不到 "MVI" 这个词------Google 用的是 "UDF" 和 "状态提升" 这些概念来描述同样的思想。

随着 Google 的发力,Android 软件架构逐渐从由社区主导的 MVP,转向了官方推荐的 MVVM,而在 Jetpack 和 Compose 大行其道后,UDF和 MVI 的概念也逐渐深入到 Android 开发中。

MVVM 的由来

那些年 Android 上的一切都变换很快,我当时大量使用了 MVP 架构,虽然不能说掌握的炉火纯青,但也可以说了如指掌。但 Google 告诉我,你的这些经验是毫无意义的,因为从 2020 开始,MVVM 经过三年的蛰伏,终于开始摧枯拉朽般地席卷 Android 开发领域。

我记得那个时候中关村已经没有那么热闹了,新的 APP 项目越来越少,Android 社区也逐渐归于平淡,否则 MVVM 也不会要等3、4年才流行起来。当我们开始尝试使用 MVVM 时,官方文档已经基本成熟,但我们仍然对其中的某些名词表示很困惑,例如双向数据绑定等。 而这个名字在 MVVM 中,是那么重要,因此我们需要把目光对准另一家公司:微软。

Android 开发者可能会觉得 MVVM 是由 Google 提出,运用在 Android 开发领域的架构模式。其实不是的,这又是 Android 开发者长期只关注单一领域的弊端。

诞生于微软

MVVM 其实是微软的发明。2005 年,微软的工程师 John Gossman 在博客上首次提出了 MVVM(Model-View-ViewModel),最初是为 WPF(Windows Presentation Foundation) 设计的。WPF 有一个强大的特性叫 Data Binding(数据绑定),MVVM 就是围绕这个特性设计的:

  • ViewModel 暴露数据属性,View 通过数据绑定自动订阅这些属性
  • ViewModel 不需要持有 View 的引用,数据变了 UI 自动更新
  • View 的用户输入也通过双向绑定自动写回 ViewModel

所以 MVVM 从出生起就和一个东西绑定在一起:数据绑定框架。没有数据绑定,MVVM 就退化为 MVP------ViewModel 不得不手动调 View 的方法来更新 UI,那就和 Presenter 没什么区别了。

被 Google 移植

正当 Android 缺乏官方指导架构时,Google 做了一件事:把 MVVM 这个微软发明的架构,移植到 Android 上,并用 Jetpack 组件实现了数据绑定的能力。

微软的 WPF 天生就有 Data Binding,但 Android 没有。所以 Google 要在 Android 上落地 MVVM,就必须自己打造数据绑定的基础设施。甚至你可以说,Google 为了让 MVVM 能在 Android 上跑起来,才推出了 Architecture Components 这套组件。也就是用 ViewModel + LiveData(后来是 StateFlow)实现了数据绑定的能力。换句话说,Google 不是发明了 MVVM,而是为 Android 打造了实现 MVVM 所需的基础设施。

为什么 MVVM 必须依赖双向绑定

要学习 MVVM,就必须要理解双向绑定这个概念,这是 MVVM 的基础和核心。

在 MVP 中,Presenter 和 View 的通信方式是命令式的:Presenter 调用 view.showXXX,然后 View 执行相关的显示方法。这就是手动同步 UI。Presenter 必须知道现在该调用哪个方法,每一步都是显式的命令。

而 MVVM 则换了一个思路:不命令,只管理数据

ViewModel 不调用任何方法,它只维护各种状态(加载状态,服务器返回的图片列表,错误信息等)。View 观察这些状态,当状态发生改变,View 自动更新。ViewModel 完全不知道 View 的存在。这就需要一个机制:当 ViewModel 的数据变化时,View 能自动感知并更新。 这个机制就是双向数据绑定(Data Binding)。

这里的双向的意思是:

  • ViewModel --> View:数据变了,UI自动更新
  • View --> ViewModel:用户输入了,数据自动写回 ViewModel

之所以需要这么设计,核心原因就一个:解耦。

在命令式(MVP)中,Presenter 必须知道 View 有哪些方法、什么时候该调谁。Presenter 和 View 是紧耦合的------View 加一个功能,Presenter 就要改,View 接口也要改。

这里我们需要达成一个共识:不是说将代码分离就算是解耦,解耦意味着双方相互独立,无需理解对方的任何内容即可独立工作。显然,在 MVP 中,Presenter 和 View 实际是没有解耦的。

在声明式(MVVM)中,ViewModel 只管当前状态是什么,不管 View 怎么展示。View 怎么渲染是 View 自己的事情。ViewModel 不关心,这样 ViewModel 和 View 就解耦了。

因此,数据绑定是 MVVM 的存在前提,没有数据绑定,ViewModel 就被迫退化成了 Presenter,MVVM 就退化为了 MVP。 一句话总结:MVVM 就是把 MVP 中的 Presenter 换成 ViewModel,把手动的"命令 View 更新"换成自动的"数据驱动 View 更新"。 而实现这个"自动"的机制,就是数据绑定。

用 MVVM 实现需求

Model 层

对于我们的这个代码,MVVM 中的 Model 层的相关类的代码如下:

kotlin 复制代码
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}")
                }
            }
        })
    }
}

细心的读者可能注意到了,在这里我们没有使用 ImageModel,取而代之的是 ImageRepository。这不是换个名字那么简单。

在 Google 的官方架构中,Repository 是 Model 层的入口。它的职责是封装所有数据来源------网络请求、数据库、缓存------并决定从哪里获取数据。ViewModel 不直接发起网络请求,而是通过 Repository 获取数据。

为什么要有这一层?因为在真实项目中,数据来源是复杂的:同一个数据可能先从本地数据库读取,没有的话再从网络请求,请求到了再写入数据库。如果 ViewModel 直接调网络请求,那加缓存就要改 ViewModel;如果 ViewModel 直接调数据库,那换 API 就要改 ViewModel。Repository 把这些复杂逻辑封装起来,ViewModel 只管调 repository.getImages(),不关心数据从哪来。

在我们的简单需求中,Repository 只做网络请求,看起来和 MVC 中的 ImageModel 没什么区别。但当需求变复杂时,Repository 的价值就会体现出来。

所以,Repository 就是 Android MVVM 中的 Model 层------它继承了 MVC 中 Model 的职责(搞定数据),只是换了一个更精确的名字。Repository,直译为"仓库",也就是数据仓库的意思。

View 层

MVVM 中,View 层也就是 Activity:

kotlin 复制代码
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SwiftArchitectureTheme {
                // 通过 viewModel() 获取 ViewModel 实例
                val viewModel: ImageViewModel = viewModel()
                ImageScreen(viewModel)
            }
        }
    }
}

@Composable
fun ImageScreen(viewModel: ImageViewModel) {
    // 观察 ViewModel 的状态,状态变化时 Compose 自动重组
    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 只负责把用户意图传给 ViewModel,具体怎么搜索、怎么处理结果,View 完全不管
                    val keyWord = viewModel.getRandomKeyWord()
                    viewModel.searchImage(keyWord)
                }
            ) {
                Icon(
                    painter = painterResource(android.R.drawable.ic_search_category_default),
                    contentDescription = null,
                )
            }
        }
    ) { innerPadding ->
        // 根据状态渲染不同的 UI,MVVM 中集中在一个 when 表达式里,所有状态一目了然,不会遗漏
        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 层

这个是整个 MVVM 的核心,它不直接调用网络请求,而是通过 Repository 来获取数据。它也不处理任何与 UI 相关的代码,主要就是各种状态的容器:

kotlin 复制代码
class ImageViewModel : ViewModel() {

    // ViewModel 通过 Repository 获取数据,不直接发起网络请求,MVVM 中 ViewModel 直接持有 ImageRepository
    private val repository = ImageRepository()

    private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    // 当前搜索关键词,View 用来显示在标题栏
    private val _keyWord = MutableStateFlow("")
    val keyWord: StateFlow<String> = _keyWord.asStateFlow()

    // 搜索图片, View 调用这个方法触发搜索,但 View 不关心结果怎么处理。
    fun searchImage(keyWord: String) {
        _keyWord.value = keyWord
        _uiState.value = UiState.Loading

        // 用 viewModelScope 启动协程,ViewModel 销毁时自动取消,这解决了 MVP 的生命周期问题:不需要手动管理异步任务
        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 状态密封类
    sealed class UiState {
        object Idle : UiState()
        object Loading : UiState()
        data class Success(val imageUrlList: List<String>) : UiState()
        data class Error(val message: String) : UiState()
    }
}

总结

写到这里,MVC、MVP、MVVM 三种架构就讲完了。从 1978 年到今天,将近 50 年,三种架构的演进逻辑其实很简单:

  • MVC 把代码拆成了三份,但 View 和 Model 没隔干净
  • MVP 用一道接口把 View 和 Model 彻底隔开,但 Presenter 背上了手动同步 UI 的重担
  • MVVM 用数据绑定把手动同步变成了自动驱动,Presenter 退场,ViewModel 上台

每一种架构都不是完美的,每一种架构都在解决上一种的问题,同时又留下新的问题。这不是软件工程的失败,这就是软件工程的本质------没有银弹。

从 MVC 到 MVP 再到 MVVM,Android 上的软件架构的变化似乎也如同中国的移动互联网浪潮一般。从最开始百家争鸣,到最后官方规范一统江湖,对架构的讨论也如同当下的 Android 市场一样归于沉寂。

而 MVVM 也不是终点。当 Compose 取代 XML 成为 Android 的 UI 框架后,声明式 UI 让"状态驱动"变得前所未有的自然。Google 开始推荐 UDF(单向数据流)------状态向下流动,事件向上传递。社区把这种模式叫做 MVI,但 Google 自己从不提这个词,他们更愿意叫它"状态提升"和"单向数据流"。

MVI 和 MVVM 的关系,就像 MVP 和 MVC 的关系一样------不是推翻,是进化。ViewModel 还在,Repository 还在,只是状态管理的方式更严谨了:所有状态变更必须经过一个统一的入口,所有 UI 变化都由唯一的状态源驱动。但那就是另一个故事了。

未来 Android 软件架构的发展是怎么样的,我们不得而知,就好像我们也无法预测未来 Android 市场一样,是重新繁荣还是成为历史废墟,我们只能静观其变。

但无论如何变化,这篇文章中的内容,对于我们这些开发者来说,依旧有用,不仅仅是 Android 平台,对于任何 GUI 软件,都需要本文中的三个架构或是其变种。技术会过时,但解决问题的思路不会。MVC 教会我分层,MVP 教会我隔离,MVVM 教会我数据驱动。这些思维方式,即使有一天 Compose 也被淘汰了,即使 Google 又发明了什么新框架,依然有用。 就像开头的那句话一样:

那些在底层长久不变的东西,才是技术开发者的大道。

相关推荐
JouYY3 小时前
聊一下多 Agent 编排架构的应用实践
架构·llm·agent
plainGeekDev4 小时前
Activity 间传值 → Navigation 参数
android·java·kotlin
用户41659673693554 小时前
Android WebView 加载 file:// 离线页面调试教程
android·前端
plainGeekDev4 小时前
onActivityResult → ActivityResult API
android·java·kotlin
Sunia4 小时前
《AgentX 专栏》10-生产部署:3台2C4G云服务器把企业级Agent真正跑起来的完整方案
java·架构
随遇丿而安8 小时前
第10周:Activity 基础功能与生命周期优化
android
alexhilton21 小时前
Android车载OS中的Remote Compose
android·kotlin·android jetpack
ZhengEnCi1 天前
Q01-高并发点赞系统架构设计
架构
落魄Android在线炒饭1 天前
Android 自定义HAL开发篇之 HIDL篇——从入门到实战(上)
android