一、ViewModel
下面把 Android Jetpack ViewModel 从"它是什么"到"它怎么活"一口气讲透,读完你就能在代码里顺手捏出一个既不死、也不泄漏的 ViewModel。
- 定位一句话
ViewModel = 专为 UI 层存数据、扛生命周期、自动续命的对象。
它能在 配置变更(横竖屏、语言切换、深色模式、窗口大小调整) 导致 Activity/Fragment 被销毁重建时 原地复活 ,因此你把 网络数据、列表缓存、选中状态 扔进去就再也不用 onSaveInstanceState 里拆拆装装了。
- 生命周期图(官方原图简化)
scss
Activity 创建 ─┬──> 旋转 ──> 重建 ──> ... ──> 真正 finish() ──> 销毁
│ ↑
└─ ViewModel 存活范围 ─────────────┘
只要 Activity 处于 "将被重建" 而不是 "永久销毁" ,ViewModel 就 不会走 onCleared(),里面的数据、协程、回调统统活着。
- 基本用法(Kotlin 版)
3.1 加依赖(2025 最新)
gradle
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0"
// 如果要用 Compose 收集状态再补
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.9.0"
3.2 写 ViewModel
kotlin
class MainVm(application: Application) : AndroidViewModel(application) {
// 1. 状态持有:一次性请求完的数据
private val repo = UserRepository()
val users: LiveData<List<User>> = repo.loadUsers().asLiveData()
// 2. 协程自动跟随 ViewModel 生命周期
fun refresh() = viewModelScope.launch {
repo.fetchLatest()
}
override fun onCleared() {
// 3. 真正永久销毁时才回调,可在这里手动关闭外部引用
Log.d("Vm", " cleared")
}
}
3.3 在 UI 层拿实例
kotlin
class MainActivity : AppCompatActivity() {
private val vm: MainVm by viewModels() // 导入 lifecycle-ktx
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.act_main)
vm.users.observe(this) { list ->
adapter.submitList(list)
}
}
}
by viewModels()
会在 第一次 onCreate
时新建,后续配置变更 直接复用 同一对象。
- 进阶:状态容器化(StateFlow / Compose)
kotlin
class MainVm : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun onSearch(key: String) = viewModelScope.launch {
_uiState.update { it.copy(loading = true) }
val data = repo.search(key)
_uiState.update { it.copy(loading = false, list = data) }
}
}
@Composable
fun SearchScreen(modifier: Modifier = Modifier) {
val vm: MainVm = viewModel() // compose 版 viewModel()
val state by vm.uiState.collectAsStateWithLifecycle()
// ...
}
collectAsStateWithLifecycle
只在 生命周期 >= STARTED 时收集,不会白跑流量。
- 在 Fragment 间共享同一个 ViewModel
kotlin
class ListFrag : Fragment(R.layout.frag_list) {
private val vm: SharedVm by activityViewModels() // 注意 activityViewModels
}
class DetailFrag : Fragment(R.layout.frag_detail) {
private val vm: SharedVm by activityViewModels() // 同一个宿主 Activity,同一实例
}
只要 Activity 活着,两个 Fragment 就共享同一块数据,无需接口、无需 EventBus。
- 保存/恢复 系统杀死后 的数据(超内存限制被 Kill)
ViewModel 只能扛 配置变更 ,系统级进程回收 还是要靠 SavedStateHandle:
kotlin
class MainVm(private val handle: SavedStateHandle) : ViewModel() {
// 1. 自动持久化
var query by handle.saveable<String?>()
private set
// 2. 用户输入实时写
fun onQueryChange(q: String?) {
query = q
}
}
被杀后重建时 handle
会把值原样喂回来,无需手动读写 Bundle。
- 常见误区 & 最佳实践
错误姿势 | 正确做法 |
---|---|
把 Activity/View/Context 传进去 | 只传 Application(AndroidViewModel 构造里已有) |
在 ViewModel 里直接 findViewById |
ViewModel 零引用 UI,数据驱动 UI |
用 GlobalScope.launch |
用 viewModelScope ,跟随 onCleared 自动取消 |
把大量 Bitmap 放 ViewModel | 内存紧张时仍可能被杀,大图走磁盘缓存 |
- 一句话总结
ViewModel 就是 "配置变更杀不死、系统杀死可恢复、自带协程生命周期" 的 UI 数据仓库;
用它在 横竖屏旋转、深色模式切换、窗口大小调整 时让数据 零流失 ,再配合 StateFlow + collectAsStateWithLifecycle,就能写出 0 泄漏、0 崩溃、0 模板 的现代 Android UI。
二、第一行代码里的写法和用by委托的不同
《第一行代码》 中讲的viewModel是这么用的:
kotlin
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
...
}
lateinit var viewModel: MainVm 这种方式使用ViewModel和private val vm: MainVm by viewModels() 这种方式的有什么区别呢?
这是两种 完全不同层次 的做法,一个 手动 DIY ,一个 官方托管 。
下面把 内存生命周期、旋转存活、协程作用域、代码量、翻车风险 逐条对比,一眼就能决定用谁。
- 写法对比
方式 | 声明 | 实例化时机 | 谁负责存 |
---|---|---|---|
DIY 手动 | lateinit var viewModel: MainVm |
你在 onCreate() 里 = ViewModelProvider(this)[MainVm::class.java] |
你自己,Activity/Fragment 字段 |
官方托管 | private val vm: MainVm by viewModels() |
第一次访问 vm 时自动完成 |
系统 ViewModelStore ,与 UI 解耦 |
- 旋转屏幕后是否复活
-
by viewModels()
✅配置变更时框架把旧实例塞进 新的 Activity/Fragment 实例 里,数据原地复活。
-
lateinit var
❌你自己
new
的,旋转后 Activity 被销毁重建 ,字段自然清空,ViewModel 重新创建 = 数据归零 (除非你手动写回onSaveInstanceState
,那就回到原始社会)。
- 协程作用域会不会泄漏
-
by viewModels()
✅内部实例是
ViewModel
子类,自带viewModelScope
,跟随系统 onCleared() 自动取消。 -
lateinit var
⚠️如果你
new MainVm()
时 没把ViewModelStoreOwner
传进去 ,它就是个 普通对象 ,没有 onCleared() ,协程需要你自己管,极易泄漏。
- 代码量 & 模板
-
by viewModels()
一行完成,零模板。
-
lateinit var
至少要写:
kotlinlateinit var vm: MainVm override fun onCreate(...) { super.onCreate(...) vm = ViewModelProvider(this).get(MainVm::class.java) }
还多一次
!!
风险。
- Fragment 共享场景
-
by viewModels()
想共享用
activityViewModels()
,一句话切换。 -
lateinit var
你得自己
by lazy { requireActivity().viewModel }
或者再ViewModelProvider(activity)
,容易拿错作用域。
- 何时才用
lateinit var
?
只有一种情况:你的类 不是 Activity/Fragment,但又想 临时拿到 一个 已存在 的 ViewModel。
示例:
kotlin
class MyDialogFragment : DialogFragment() {
// 想拿宿主 Activity 的 ViewModel
private val vm: MainVm
get() = (requireActivity() as MainActivity).vm // 宿主已经 by viewModels()
}
这里只是 引用 ,不会重新 new ,所以旋转依旧安全。
除此之外,全部用 by viewModels()
/ activityViewModels()
。
一句话结论
lateinit var
是 "我全都要自己管" 的野路子,旋转就丢、协程易漏、代码还多 ;
by viewModels()
是 官方托管的亲儿子 ,自动复活、自动清协程、零模板 。
除非你在 非 UI 类里临时借引用 ,否则 无脑选 by viewModels()
就对了。
三、lateinit关键字
如果忘记了lateinit关键字
lateinit
是 Kotlin 给 var 属性 开的一个人工"后门":
"我保证稍后一定初始化,先别判我编译错误。"
没有它,Kotlin 的空检查系统会逼你在构造函数里就给出值;有了它,你可以把初始化延迟到 onCreate() 、注入框架 、单元测试 setUp() 等任意时机,同时 仍然保持非 null 类型 ,避免全书都是 String?
+ !!
。
- 语法与限制(背下来)
- 只能修饰 var(val 一次性赋值,无法"稍后")
- 只能修饰 非 null 类型 (
lateinit var name: String
✅,lateinit var name: String?
❌) - 不能用于 Java 基本类型 (
Int
、Long
、Boolean
... 因为编译后需要一张"是否已初始化"的额外标志位,而 int 本身无法为 null) - 不能用在 主构造函数 里;必须在 类体内 声明
- 不能修饰 局部变量(1.2 起已放开,但很少用)
- 最经典场景:Android 的 View 注入
kotlin
class LoginActivity : AppCompatActivity() {
// 布局里一定有,但要在 setContentView 后才能 find
lateinit var btnLogin: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.act_login)
btnLogin = findViewById(R.id.btn_login)
btnLogin.setOnClickListener { ... } // 直接当非空用
}
}
没有 lateinit
你就得:
kotlin
private var btnLogin: Button? = null // 全篇 ? 和 !!
- 与依赖注入框架搭档
kotlin
@Inject
lateinit var api: GithubApi // Dagger/Hilt 在 inject() 时才赋值
fun onCreate() {
(application as MyApp).appComponent.inject(this) // 这里完成
}
- 反射自检:到底初始化没?
kotlin
if (::btnLogin.isInitialized) { // 双冒号语法
btnLogin.text = "已登录"
}
这在 单元测试 tearDown() 里常用,防止重复赋空。
- 翻车现场:忘记赋值
kotlin
lateinit var token: String
fun print() = println(token) // 运行时抛
// UninitializedPropertyAccessException: lateinit property token has not been initialized
编译期不报错,跑起来才炸 ,所以 务必保证初始化路径 100 % 覆盖。
- 不能用的替代品
需求 | 能否用 lateinit | 推荐替代 |
---|---|---|
Int、Long 等基本类型 | ❌ | by lazy { ... } 或 Delegates.notNull<Int>() |
val 只读 | ❌ | by lazy { ... } |
需要在构造函数外多次重新赋值 | ✅ | lateinit var 正是为此而生 |
- 一句话口诀
"val 用 lazy,var 先空着" ------
只要你是 var 、非空类型 、稍后才能给值 ,就大胆
lateinit
;记住 ::xxx.isInitialized 防翻车,基本类型和 val 请转向
lazy
或notNull()
。
四、Fragment 共享场景
场景
一个 Activity 里左右并排两个 Fragment:
- LeftFrag -- 负责输入关键词
- RightFrag -- 负责展示搜索结果
它们要 共享同一个 SearchViewModel,保证:
- 屏幕旋转后数据不丢;
- 输入即搜,两个 Fragment 不各自 new 一份 VM。
- 先写可共享的 ViewModel
kotlin
class SearchVm : ViewModel() {
private val _text = MutableStateFlow("")
val text: StateFlow<String> = _text
private val _result = MutableStateFlow<List<String>>(emptyList())
val result: StateFlow<List<String>> = _result
fun onInput(s: String) {
_text.value = s
viewModelScope.launch {
// 模拟网络请求
_result.value = searchApi(s)
}
}
private suspend fun searchApi(key: String): List<String> =
if (key.isBlank()) emptyList()
else listOf("$key-1", "$key-2", "$key-3")
}
- Activity 布局(仅示意)
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/left_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/right_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
- Activity 把两个 Fragment 挂上
kotlin
class MainActivity : AppCompatActivity(R.layout.act_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
add(R.id.left_container, LeftFrag())
add(R.id.right_container, RightFrag())
}
}
}
}
- 两个 Fragment 各取同一个 VM
核心 API:activityViewModels()
(宿主 Activity 级别 ViewModelStore,旋转后依旧活着)
LeftFrag -- 负责输入
kotlin
class LeftFrag : Fragment(R.layout.frag_left) {
// 1. 拿到与 Activity 绑定的同一个 SearchVm
private val vm: SearchVm by activityViewModels()
override fun onViewCreated(view: View, b: Bundle?) {
val editText = view.findViewById<EditText>(R.id.edit)
// 2. 输入即搜
editText.doAfterTextChanged { editable ->
vm.onInput(editable?.toString() ?: "")
}
}
}
RightFrag -- 负责展示
kotlin
class RightFrag : Fragment(R.layout.frag_right) {
// 1. 同一个 SearchVm
private val vm: SearchVm by activityViewModels()
override fun onViewCreated(view: View, b: Bundle?) {
val listView = view.findViewById<ListView>(R.id.list)
val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_list_item_1)
listView.adapter = adapter
// 2. 观察结果
lifecycleScope.launchWhenStarted {
vm.result.collect { newList ->
adapter.clear()
adapter.addAll(newList)
}
}
}
}
-
运行效果
-
在左侧输入 "cat" → 右侧立即出现 cat-1, cat-2, cat-3
-
旋转屏幕 → Activity + Fragment 全部重建,但 SearchVm 实例被保留 ,输入框文字与列表结果 瞬间恢复 ,0 额外代码。
- 一句话总结
两个 Fragment 只要都用
kotlin
private val vm: SearchVm by activityViewModels()
它们就拿到 Activity 作用域里的唯一 SearchVm ,
旋转不死、数据共享、无需接口、无需 EventBus ------ 这就是官方推荐的 Fragment 级共享方案。
五、activityViewModels 是什么
activityViewModels()
是 fragment-ktx 提供的一个 属性委托 ,用来 在 Fragment 里获取"与宿主 Activity 生命周期一致"的 ViewModel 实例 。
一句话:它让多个 Fragment 共享同一个 ViewModel,并且旋转屏幕不丢数据。
- 本质原理
-
内部等价于
ViewModelProvider(requireActivity()).get(YourVM::class.java)
但写成 委托属性 形式,一行代码搞定。
-
因为用的是 Activity 的 ViewModelStore,所以:
- 同一 Activity 下的 所有 Fragment 拿到的 是同一个对象;
- 配置变更(旋转、语言切换)时 Activity 被重建,但 ViewModelStore 被系统暂存 ,ViewModel 原地复活;
- 只有 Activity 真正 finish 或 进程被系统杀死 时,ViewModel 才会走
onCleared()
。
- 使用姿势(Kotlin)
kotlin
class ListFragment : Fragment(R.layout.frag_list) {
// 1. 引入依赖
// implementation "androidx.fragment:fragment-ktx:1.8.2"
// 2. 获取 Activity 级 ViewModel
private val vm: SearchVm by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.result.observe(viewLifecycleOwner) { list -> adapter.submit(list) }
}
}
- 与
viewModels()
区别速记
委托方式 | 作用域 | 适用场景 |
---|---|---|
viewModels() |
当前 Fragment | 只有自己用,不共享 |
activityViewModels() |
宿主 Activity | 多 Fragment 共享数据、旋转续命 |
- 一句话总结
activityViewModels()
就是 "官方帮你写好的 ViewModelProvider(requireActivity())" ,
让 Fragment 之间零接口、零 EventBus、零模板 地共享数据,并且 配置变更不死。