Android与MVC设计模式

理解 MVC 架构模式在 Android 中的映射关系,用 Kotlin data class 创建 Model,学会分离界面、数据和逻辑。

学习目标

  • 能沿“Android与MVC设计模式”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“理解 MVC 架构模式在 Android 中的映射关系,用 Kotlin data class 创建 Model,学会分离界面、数据和逻辑。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用构建指纹、用户操作、状态快照、原始日志和行为断言独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

为什么代码全写在一起会爆炸

想象你开了一家面包店。做面包、接单、收银、打扫卫生全都你一个人干——刚开始一天 10 个顾客还行。但当顾客涨到 200 个时,你做面包的间隙要去收款,收完款发现烤箱里的面包已经焦了。这就是把所有代码塞进一个文件的真实处境。

设计模式就是帮你分工:把面包师傅(数据逻辑)、收银员和大家看到的面包橱窗(界面)分成三个独立的角色。每个角色只管自己一摊事,换一个橱窗的灯光装饰不用惊动面包师傅。

没有 MVC 会怎样?当你的应用从"一个按钮显示一句话"变成"三个界面、网络请求、数据库读写"时,所有代码纠缠在一个文件里——找一行代码要上下翻几百行,改一个颜色结果把登录逻辑也改坏了。这不是夸张,这是每个 Android 新手必然会踩进去的坑。MVC 就是帮你从一开始就把地盘划清楚。

概念讲解

MVC 三层:每层管什么

把应用想象成一个餐厅,MVC 就是餐厅里的三个角色:

角色MVC 层职责Android 中对应
菜谱和大厨Model管数据——数据长什么样(类定义)、数据从哪来(网络/数据库)、数据怎么变(业务逻辑)Kotlin data class、Repository 类
摆盘和餐桌View管显示——按钮放哪、文字什么颜色、列表怎么排res/layout/*.xml 布局文件
服务员Controller管协调——收到用户点击后找 Model 要数据,再把数据交给 View 显示ActivityFragment

三层不是各自孤立的,而是围成一个闭环:用户的每一次操作,都会沿着 View → Controller → Model → Controller → View 转一整圈。下面这张图把这一圈拆成六步,点播放看数据怎么流:

可交互
Viewres/layout/*.xml 布局ModelKotlin data classControllerActivity / Fragment② 事件⑥ 渲染③ 更新⑤ 读取① 用户点击 ▾

第 1 / 6 步 · ① 用户在 View 上操作(点了「正确」按钮)

点击播放,看一次「用户点一下」在 View / Controller / Model 三层之间流转一整圈;可暂停、单步、拖进度逐帧观察。

MVC 数据流闭环:用户在 View 操作 → Controller 收到事件 → 更新 Model → Model 改变 → Controller 读新值 → 刷回 View 渲染。Controller 居中当枢纽,View 与 Model 从不直接对话。

是最"干净"的一层——它不知道任何关于界面的事。一个 User 数据类不管自己最终是显示在登录页还是设置页,它只管定义"用户有 id、名字、头像 URL 这三个属性"。

在 Android 里就是那些 XML 布局文件。它们定义了界面的骨架——有一个按钮、一个文本框、一个列表……但具体按钮上写什么字、列表里有哪些数据,View 不管,等着 Controller 来填。

是最忙的角色——它不自己做面包也不自己摆盘,但知道"客人点了吐司,该叫面包师傅(Model)做什么"以及"做好了该放在哪个盘子(View)里端上去"。在 Android 里,Activity 和 Fragment 就是 Controller。

Kotlin data class:最轻量的 Model

Kotlin 里最适合做 Model 的就是 。定义一个最简单的"问题"模型:

data class Question(
    val id: Int,
    val text: String,
    val answer: Boolean,  // true = 正确, false = 错误
    val isAnswered: Boolean = false
)

三行代码,就得到了一个完整的 Model:它知道每个问题有一个编号、一段文字、一个正确答案和一个"是否已回答"的状态。val 表示这些属性创建后不可变——这是 Kotlin 推荐的做法:数据模型本身不自己改自己,修改由 Controller 操作。注意 isAnswered 有默认值 false,创建新问题时不用写这个参数。

Model 也可以是会"干活"的——不只是存数据。下面这个 Model 封装了一段判断逻辑:

data class QuizState(
    val questions: List<Question>,
    val currentIndex: Int = 0
) {
    val currentQuestion: Question
        get() = questions[currentIndex]
 
    val isLastQuestion: Boolean
        get() = currentIndex == questions.lastIndex
 
    fun moveToNext(): QuizState =
        copy(currentIndex = currentIndex + 1)
}

copy() 是 data class 自带的方法——它创建一个新对象,只修改你指定的属性,其他属性保持不变。这个模型只管"当前到第几题了"和"下一题怎么走",完全不关心界面怎么显示——这才是干净的 Model。

一个反例:全写在 Activity 里是什么灾难

class MainActivity : AppCompatActivity() {
    // 所有东西全塞在一起——
    private var currentIndex = 0
    private val questions = listOf("1+1=?", "2+2=?")
    private val answers = listOf(true, false)
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        val textView = findViewById<TextView>(R.id.questionText)
        val trueButton = findViewById<Button>(R.id.trueButton)
        val falseButton = findViewById<Button>(R.id.falseButton)
 
        textView.text = questions[currentIndex]
 
        trueButton.setOnClickListener {
            if (answers[currentIndex]) { /* 答对了 */ }
            currentIndex++
            if (currentIndex < questions.size) {
                textView.text = questions[currentIndex]
            }
        }
        // ... falseButton 也要写几乎一样的逻辑
    }
}

这段代码的问题是:① 数据和逻辑混在 Activity 里——考题数据、答题判断、翻页全在同一个类。② 加一个新功能(比如显示分数)要在这一个文件里改多处。③ 加第二个界面时,整段逻辑没法复用——你只能复制粘贴。

重构:拆出 QuizState Model

// Model — 纯粹的数据 + 逻辑,不碰 Android 任何 API
data class QuizState(
    val questions: List<Question>,
    val currentIndex: Int = 0,
    val score: Int = 0
) {
    val currentQuestion: Question get() = questions[currentIndex]
 
    fun checkAnswer(userAnswer: Boolean): Boolean =
        currentQuestion.answer == userAnswer
 
    fun moveToNext(): QuizState =
        copy(currentIndex = currentIndex + 1)
}
// Controller — Activity 只管"界面事件→调Model→更新View"
class MainActivity : AppCompatActivity() {
    private lateinit var quizState: QuizState
    private lateinit var questionTextView: TextView
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        questionTextView = findViewById(R.id.questionText)
 
        quizState = QuizState(
            questions = listOf(
                Question(1, "Android 用 Kotlin 开发", true),
                Question(2, "Android 用 Swift 开发", false)
            )
        )
        updateQuestion()
    }
 
    private fun updateQuestion() {
        questionTextView.text = quizState.currentQuestion.text
    }
 
    private fun onAnswer(userAnswer: Boolean) {
        quizState.checkAnswer(userAnswer)
        quizState = quizState.moveToNext()
        updateQuestion()
    }
}

对比重构前后:Model 里没有任何 findViewByIdsetOnClickListener——它不依赖 Android SDK,可以在单元测试里直接测。Activity 里只剩"找到控件 → 调 Model → 更新控件"这几步,逻辑清晰到能一眼看懂。

动手:从零搭一个 MVC 答题应用

下面一步步搭一个遵守 MVC 的应用,每步都标注你在写哪一层。

猜一猜:MVC 三层中,哪一层的代码量应该最少?做完下面的练习再看答案。

分步1 / 3

① 创建 Model(Question + QuizState)

java/ 下新建 model/Question.kt

package com.example.myapp.model
 
data class Question(
    val id: Int,
    val text: String,
    val answer: Boolean
)

再新建 model/QuizState.kt

package com.example.myapp.model
 
data class QuizState(
    val questions: List<Question>,
    val currentIndex: Int = 0
) {
    val currentQuestion: Question
        get() = questions[currentIndex]
 
    fun checkAnswer(userAnswer: Boolean): Boolean =
        currentQuestion.answer == userAnswer
 
    fun moveToNext(): QuizState =
        copy(currentIndex = currentIndex + 1)
}

这一步只建了数据结构和规则——不依赖任何 Android API。

小结

  • MVC 把应用拆为三层:Model(数据+逻辑)、View(布局XML)、Controller(Activity)——各管一层,互不越界
  • Kotlin data class 是 Model 的最佳载体——copy() 创建不可变对象,业务逻辑封装在 Model 内部
  • Controller 是协调者:事件来了→调 Model→拿结果→更新 View,本身不存状态
  • 判断标准:不含 android.* 的代码属于 Model 层,应移出 Activity
  • 好处:Model 可单元测试、View 可换布局不动代码、Controller 短小易读

练习

问题 1:“第2章 Android and Model-View-Controller”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

本章出现的专业名词,用大白话再讲一遍。

MVC

Model-View-Controller,将应用拆成三层:Model 管数据与业务逻辑,View 管界面显示,Controller 协调二者。在 Android 中,Model 对应 Kotlin data class,View 对应 layout XML,Controller 对应 Activity/Fragment。详见本章"MVC 三层"一节。

Model

MVC 中负责数据结构和业务逻辑的层。在 Android 里用 Kotlin data class 实现,不依赖任何 Android SDK API(无 ViewContext 等引用)。可以被单元测试直接测试——这是 MVC 分层最大的好处。详见本章"Kotlin data class:最轻量的 Model"一节。

View(MVC中的)

MVC 中负责界面显示的层。在 Android 里特指 res/layout/ 下的 XML 布局文件,定义控件的位置、尺寸、样式。View 层只管怎么显示,不管显示什么——数据由 Controller 填入。详见本章"MVC 三层"一节。

Controller

MVC 中连接 Model 和 View 的协调者。在 Android 里 Activity 和 Fragment 承担此角色。Controller 接收用户操作 → 调用 Model 更新数据 → 将新数据写入 View。Controller 本身不应存业务状态。详见本章"重构:拆出 QuizState Model"一节。

data class

Kotlin 的一种特殊类,编译器自动生成 equals()hashCode()toString()copy() 等方法。最适合做 Model——三行代码得到一个功能完整的数据容器。详见本章"Kotlin data class"一节。

“Android与MVC设计模式”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“Android与MVC设计模式”必须回到可观察状态

“Android与MVC设计模式”的学习结果不是记住类名,而是能预测“理解 MVC 架构模式在 Android 中的映射关系,用 Kotlin data class 创建 Model,学会分离界面、数据和逻辑。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

2. Android and Model-View-Controller

在“Android与MVC设计模式”中,验证“2. Android and Model-View-Controller”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Creating a New Class

在“Android与MVC设计模式”中,验证“Creating a New Class”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Model-View-Controller and Android

在“Android与MVC设计模式”中,验证“Model-View-Controller and Android”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Updating the View Layer

在“Android与MVC设计模式”中,分析“Updating the View Layer”先冻结设备API、targetSdk和输入,再沿回调与数据流寻找首个状态分叉,不能只描述最终页面。

Updating the Controller Layer

在“Android与MVC设计模式”中,验证“Updating the Controller Layer”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Adding an Icon

在“Android与MVC设计模式”中,“Adding an Icon”的复用不能吞掉状态语义;pressed、focused、disabled与checked都要有可辨反馈。

Screen Pixel Densities

在“Android与MVC设计模式”中,验证“Screen Pixel Densities”在日夜主题、不同密度和字体缩放下比较像素边界、对比度与状态反馈。

Running on a Device

在“Android与MVC设计模式”中,“Running on a Device”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。

Challenge: Add a Listener to the TextView

在“Android与MVC设计模式”中,“Challenge: Add a Listener to the TextView”用于推翻“理解 MVC 架构模式在 Android 中的映射关系,用 Kotlin data class 创建 Model,学会分离界面、数据和逻辑。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

Challenge: Add a Previous Button

在“Android与MVC设计模式”中,“Challenge: Add a Previous Button”用于推翻“理解 MVC 架构模式在 Android 中的映射关系,用 Kotlin data class 创建 Model,学会分离界面、数据和逻辑。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

Challenge: From Button to ImageButton

在“Android与MVC设计模式”中,“Challenge: From Button to ImageButton”用于推翻“理解 MVC 架构模式在 Android 中的映射关系,用 Kotlin data class 创建 Model,学会分离界面、数据和逻辑。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

“Android与MVC设计模式”验收回顾

“Android与MVC设计模式”只有在构建指纹、用户操作、状态快照、原始日志和行为断言能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:Android开发初体验 · 下一页:activity的生命周期 →

讨论

评论区加载中…