安卓的ViewModel

一、ViewModel

下面把 Android Jetpack ViewModel 从"它是什么"到"它怎么活"一口气讲透,读完你就能在代码里顺手捏出一个既不死、也不泄漏的 ViewModel。


  1. 定位一句话
    ViewModel = 专为 UI 层存数据、扛生命周期、自动续命的对象。
    它能在 配置变更(横竖屏、语言切换、深色模式、窗口大小调整) 导致 Activity/Fragment 被销毁重建时 原地复活 ,因此你把 网络数据、列表缓存、选中状态 扔进去就再也不用 onSaveInstanceState 里拆拆装装了。

  1. 生命周期图(官方原图简化)
scss 复制代码
Activity 创建 ─┬──> 旋转 ──> 重建 ──> ... ──> 真正 finish() ──> 销毁
               │                                 ↑
               └─ ViewModel 存活范围 ─────────────┘

只要 Activity 处于 "将被重建" 而不是 "永久销毁" ,ViewModel 就 不会走 onCleared(),里面的数据、协程、回调统统活着。


  1. 基本用法(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 时新建,后续配置变更 直接复用 同一对象。


  1. 进阶:状态容器化(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 时收集,不会白跑流量


  1. 在 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


  1. 保存/恢复 系统杀死后 的数据(超内存限制被 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


  1. 常见误区 & 最佳实践
错误姿势 正确做法
把 Activity/View/Context 传进去 只传 Application(AndroidViewModel 构造里已有)
在 ViewModel 里直接 findViewById ViewModel 零引用 UI,数据驱动 UI
GlobalScope.launch viewModelScope跟随 onCleared 自动取消
把大量 Bitmap 放 ViewModel 内存紧张时仍可能被杀,大图走磁盘缓存

  1. 一句话总结
    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 ,一个 官方托管

下面把 内存生命周期、旋转存活、协程作用域、代码量、翻车风险 逐条对比,一眼就能决定用谁。


  1. 写法对比
方式 声明 实例化时机 谁负责存
DIY 手动 lateinit var viewModel: MainVm 你在 onCreate()= ViewModelProvider(this)[MainVm::class.java] 你自己,Activity/Fragment 字段
官方托管 private val vm: MainVm by viewModels() 第一次访问 vm 时自动完成 系统 ViewModelStore与 UI 解耦

  1. 旋转屏幕后是否复活
  • by viewModels()

    配置变更时框架把旧实例塞进 新的 Activity/Fragment 实例 里,数据原地复活

  • lateinit var

    你自己 new 的,旋转后 Activity 被销毁重建 ,字段自然清空,ViewModel 重新创建 = 数据归零 (除非你手动写回 onSaveInstanceState,那就回到原始社会)。


  1. 协程作用域会不会泄漏
  • by viewModels()

    内部实例是 ViewModel 子类,自带 viewModelScope跟随系统 onCleared() 自动取消

  • lateinit var ⚠️

    如果你 new MainVm()没把 ViewModelStoreOwner 传进去 ,它就是个 普通对象没有 onCleared() ,协程需要你自己管,极易泄漏


  1. 代码量 & 模板
  • by viewModels()

    一行完成,零模板

  • lateinit var

    至少要写:

    kotlin 复制代码
    lateinit var vm: MainVm
    override fun onCreate(...) {
        super.onCreate(...)
        vm = ViewModelProvider(this).get(MainVm::class.java)
    }

    还多一次 !! 风险。


  1. Fragment 共享场景
  • by viewModels()

    想共享用 activityViewModels()一句话切换

  • lateinit var

    你得自己 by lazy { requireActivity().viewModel } 或者再 ViewModelProvider(activity)容易拿错作用域


  1. 何时才用 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? + !!


  1. 语法与限制(背下来)
  • 只能修饰 var(val 一次性赋值,无法"稍后")
  • 只能修饰 非 null 类型lateinit var name: String ✅, lateinit var name: String? ❌)
  • 不能用于 Java 基本类型IntLongBoolean... 因为编译后需要一张"是否已初始化"的额外标志位,而 int 本身无法为 null)
  • 不能用在 主构造函数 里;必须在 类体内 声明
  • 不能修饰 局部变量(1.2 起已放开,但很少用)

  1. 最经典场景: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   // 全篇 ? 和 !!

  1. 与依赖注入框架搭档
kotlin 复制代码
@Inject
lateinit var api: GithubApi       // Dagger/Hilt 在 inject() 时才赋值

fun onCreate() {
    (application as MyApp).appComponent.inject(this)  // 这里完成
}

  1. 反射自检:到底初始化没?
kotlin 复制代码
if (::btnLogin.isInitialized) {   // 双冒号语法
    btnLogin.text = "已登录"
}

这在 单元测试 tearDown() 里常用,防止重复赋空。


  1. 翻车现场:忘记赋值
kotlin 复制代码
lateinit var token: String
fun print() = println(token)   // 运行时抛
// UninitializedPropertyAccessException: lateinit property token has not been initialized

编译期不报错,跑起来才炸 ,所以 务必保证初始化路径 100 % 覆盖


  1. 不能用的替代品
需求 能否用 lateinit 推荐替代
Int、Long 等基本类型 by lazy { ... }Delegates.notNull<Int>()
val 只读 by lazy { ... }
需要在构造函数外多次重新赋值 lateinit var 正是为此而生

  1. 一句话口诀

"val 用 lazy,var 先空着" ------

只要你是 var非空类型稍后才能给值 ,就大胆 lateinit

记住 ::xxx.isInitialized 防翻车,基本类型和 val 请转向 lazynotNull()

四、Fragment 共享场景

场景

一个 Activity 里左右并排两个 Fragment:

  • LeftFrag -- 负责输入关键词
  • RightFrag -- 负责展示搜索结果

它们要 共享同一个 SearchViewModel,保证:

  1. 屏幕旋转后数据不丢;
  2. 输入即搜,两个 Fragment 不各自 new 一份 VM。

  1. 先写可共享的 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")
}

  1. 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>

  1. 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())
            }
        }
    }
}

  1. 两个 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)
            }
        }
    }
}

  1. 运行效果

  2. 在左侧输入 "cat" → 右侧立即出现 cat-1, cat-2, cat-3

  3. 旋转屏幕 → Activity + Fragment 全部重建,但 SearchVm 实例被保留 ,输入框文字与列表结果 瞬间恢复0 额外代码


  1. 一句话总结

两个 Fragment 只要都用

kotlin 复制代码
private val vm: SearchVm by activityViewModels()

它们就拿到 Activity 作用域里的唯一 SearchVm
旋转不死、数据共享、无需接口、无需 EventBus ------ 这就是官方推荐的 Fragment 级共享方案。

五、activityViewModels 是什么

activityViewModels()fragment-ktx 提供的一个 属性委托 ,用来 在 Fragment 里获取"与宿主 Activity 生命周期一致"的 ViewModel 实例

一句话:它让多个 Fragment 共享同一个 ViewModel,并且旋转屏幕不丢数据


  1. 本质原理
  • 内部等价于
    ViewModelProvider(requireActivity()).get(YourVM::class.java)

    但写成 委托属性 形式,一行代码搞定

  • 因为用的是 Activity 的 ViewModelStore,所以:

    • 同一 Activity 下的 所有 Fragment 拿到的 是同一个对象
    • 配置变更(旋转、语言切换)时 Activity 被重建,但 ViewModelStore 被系统暂存 ,ViewModel 原地复活
    • 只有 Activity 真正 finish进程被系统杀死 时,ViewModel 才会走 onCleared()

  1. 使用姿势(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) }
    }
}

  1. viewModels() 区别速记
委托方式 作用域 适用场景
viewModels() 当前 Fragment 只有自己用,不共享
activityViewModels() 宿主 Activity 多 Fragment 共享数据、旋转续命

  1. 一句话总结

activityViewModels() 就是 "官方帮你写好的 ViewModelProvider(requireActivity())"

Fragment 之间零接口、零 EventBus、零模板 地共享数据,并且 配置变更不死

相关推荐
ace望世界1 小时前
kotlin的委托
android
CYRUS_STUDIO4 小时前
一文搞懂 Frida Stalker:对抗 OLLVM 的算法还原利器
android·逆向·llvm
zcychong4 小时前
ArrayMap、SparseArray和HashMap有什么区别?该如何选择?
android·面试
CYRUS_STUDIO4 小时前
Frida Stalker Trace 实战:指令级跟踪与寄存器变化监控全解析
android·逆向
ace望世界9 小时前
android的Parcelable
android
顾林海9 小时前
Android编译插桩之AspectJ:让代码像特工一样悄悄干活
android·面试·性能优化
叽哥10 小时前
Flutter Riverpod上手指南
android·flutter·ios
循环不息优化不止10 小时前
安卓开发设计模式全解析
android
诺诺Okami10 小时前
Android Framework-WMS-层级结构树
android