5、ViewModel KTX:viewModelScope与by viewModels()委托,优雅管理生命周期

ViewModel 这个东西,说白了就是帮你在屏幕旋转时保住那点数据。我早年做 Android 开发时,最头疼的就是 Activity 重建——用户转个手机,输入框里的内容全没了,气得想摔手机。后来 Google 推出了 ViewModel,算是把这个问题给治了。

但光有 ViewModel 还不够。你想想看,每次都要手动写 ViewModelProvider,还要处理协程的生命周期,代码量不小。KTX 扩展库就是来解决这些痛点的。今天咱们就聊聊 ViewModel KTX 里的两个核心工具:viewModelScopeby viewModels() 委托。

5.1 为什么需要 ViewModel KTX?

先看一段原生写法,感受一下:

// 没有 KTX 的日子
class MyActivity : AppCompatActivity() {
    private lateinit var viewModel: MyViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
    }
}

这段代码有什么问题?嗯,其实也没大毛病,就是啰嗦。每次都要写 ViewModelProvider,而且容易忘记初始化。更麻烦的是协程管理——如果你在 ViewModel 里启动协程,得自己处理取消逻辑。

我个人习惯用 KTX 之后,再也不想回到这种写法了。它带来的不仅是代码量减少,更重要的是——帮你规避了生命周期管理的坑

5.2 by viewModels() 委托:告别模板代码

Kotlin 的委托属性在这里派上了大用场。by viewModels() 本质上是一个属性委托,它会在你第一次访问这个属性时,自动创建 ViewModel 实例。

// 使用 KTX 委托
class MyActivity : AppCompatActivity() {
    val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 直接用,不用管初始化
        viewModel.loadData()
    }
}

简洁吧?一行搞定。而且它内部已经处理好了 ViewModel 的缓存和生命周期绑定。你在 Fragment 里也能用:

class MyFragment : Fragment() {
    // 获取 Activity 范围内的 ViewModel,适合多个 Fragment 共享数据
    val sharedViewModel: SharedViewModel by viewModels(
        ownerProducer = { requireActivity() }
    )
    
    // 获取 Fragment 自己的 ViewModel
    val myViewModel: MyViewModel by viewModels()
}
小提示:如果你在 Fragment 里用 by viewModels(),默认是 Fragment 作用域。想跟兄弟 Fragment 共享数据?记得传 ownerProducer = { requireActivity() }

我曾经在项目里遇到过一个坑:两个 Fragment 各自用 by viewModels() 获取同一个 ViewModel 类,结果数据不共享。排查了半天才发现,它们各自创建了不同的实例。后来改成 by activityViewModels() 才解决。

5.3 viewModelScope:协程的自动取消

协程在 ViewModel 里用得很频繁——网络请求、数据库操作、耗时计算。但有个问题:ViewModel 被清除时,这些协程还在跑怎么办?

原生写法你得这样:

class MyViewModel : ViewModel() {
    private var job: Job? = null
    
    fun loadData() {
        job = CoroutineScope(Dispatchers.IO).launch {
            // 网络请求...
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        job?.cancel() // 手动取消,容易忘
    }
}

说实话,我刚开始写的时候经常忘记在 onCleared() 里取消协程。结果就是 Activity 销毁了,网络请求还在后台跑,甚至可能回调时操作已销毁的 View,直接崩掉。

viewModelScope 就是来解决这个问题的。它是 ViewModel 的一个扩展属性,绑定的是 Dispatchers.Main,并且会在 ViewModel 的 onCleared() 时自动取消所有子协程。

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // 自动绑定生命周期,不用手动取消
            val result = repository.fetchData()
            _liveData.value = result
        }
    }
}
核心要点:viewModelScope 的生命周期 = ViewModel 的生命周期。ViewModel 被清除时,所有通过 viewModelScope 启动的协程都会被自动取消。你不需要写任何取消代码。

这里有个细节要注意:viewModelScope 默认使用 Dispatchers.Main。如果你要做 IO 操作,记得切线程:

viewModelScope.launch {
    // 主线程
    showLoading(true)
    
    // 切到 IO 线程
    val data = withContext(Dispatchers.IO) {
        repository.fetchDataFromNetwork()
    }
    
    // 回到主线程更新 UI
    showLoading(false)
    _liveData.value = data
}

5.4 实战:一个完整的例子

把上面两个工具结合起来,写一个用户信息加载的 ViewModel:

class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User?>()
    val user: LiveData<User?> = _user
    
    private val _loading = MutableLiveData<Boolean>()
    val loading: LiveData<Boolean> = _loading
    
    fun loadUser(userId: String) {
        viewModelScope.launch {
            _loading.value = true
            try {
                val result = withContext(Dispatchers.IO) {
                    userRepository.getUser(userId)
                }
                _user.value = result
            } catch (e: Exception) {
                _user.value = null
            } finally {
                _loading.value = false
            }
        }
    }
}

// Activity 中使用
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.user.observe(this) { user ->
            // 更新 UI
            showUserInfo(user)
        }
        
        viewModel.loadUser("12345")
    }
}

这段代码里,by viewModels() 帮我们省去了 ViewModelProvider 的模板代码,viewModelScope 保证了协程在 ViewModel 销毁时自动取消。你想想看,如果不用 KTX,同样的功能你得写多少行?

5.5 避坑指南

用这两个工具时,有几个坑我踩过,分享给你:

  • 不要在 viewModelScope 里启动全局协程——比如用 GlobalScope.launch,那生命周期就失控了。
  • by viewModels() 只能在 Activity/Fragment 里用——不能在自定义 View 或普通类里用,因为需要 LifecycleOwner。
  • Fragment 里用 by viewModels() 默认是 Fragment 作用域——想共享数据记得用 by activityViewModels()
  • viewModelScope 的协程默认跑在主线程——做耗时操作一定要切线程,否则会 ANR。
警告:我曾经在项目里看到有人直接在 viewModelScope 里做网络请求,没切线程。结果 UI 卡死,用户反馈说「点一下按钮等三秒才有反应」。排查后发现是主线程被网络请求阻塞了。记住:viewModelScope.launch 默认是 Dispatchers.Main,IO 操作必须用 withContext(Dispatchers.IO)

5.6 知识体系总览

下面这张图帮你理清 ViewModel KTX 的核心逻辑:

ViewModel KTX 核心知识体系 ViewModel by viewModels() 委托 自动创建 ViewModel 实例 绑定 Activity/Fragment 生命周期 viewModelScope 协程自动取消 默认 Dispatchers.Main 实际应用效果 ✅ 代码量减少 50%+ ✅ 生命周期安全,无内存泄漏 ✅ 协程自动管理,无需手动取消

说白了,ViewModel KTX 就是帮你把那些重复的、容易出错的生命周期管理代码给封装掉了。你只需要关注业务逻辑,剩下的交给 KTX 去处理。

我个人觉得,by viewModels()viewModelScope 是 Jetpack KTX 里最实用的两个工具。它们不改变你的架构,只是让代码写起来更舒服、更安全。如果你还没用上,我建议你从下一个项目开始就引入它们——相信我,你会爱上这种简洁的写法。


公众号:蓝海资料掘金营,微信deep3321