前言
两个月前,公司开辟了一个新的业务,我很高兴地参与了其中,去开发一个新的 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 惯用法:用 let 做 null 安全调用
user?.let {
sendEmail(it) }
// Kotlin 惯用法:用 apply 初始化对象
val dialog = AlertDialog.Builder(this).apply {
setTitle("提示")
setMessage("确定删除?")
setPositiveButton("确定") {
_, _ -> delete() }
}
// 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 开始。
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

2554

被折叠的 条评论
为什么被折叠?



