Kotlin ViewModelScope 笔记

1 阅读4分钟

viewModelScope 是 Android Jetpack 中 ViewModel 的一个扩展属性,为 ViewModel 提供了一个生命周期感知的协程作用域,确保协程在 ViewModel 被销毁时自动取消,避免内存泄漏。

基本使用

1. 添加依赖

// build.gradle.kts (Module)
dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
}

// 或者,如果使用 Compose
dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
}

2. 基本用法

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay

class MyViewModel : ViewModel() {
    
    fun loadData() {
        // 在 viewModelScope 中启动协程
        viewModelScope.launch {
            // 模拟网络请求
            delay(2000)
            
            // 更新状态
            _data.value = "加载完成"
        }
    }
    
    fun loadDataWithException() {
        viewModelScope.launch {
            try {
                val result = apiService.getData()
                _data.value = result
            } catch (e: Exception) {
                _error.value = e.message
            }
        }
    }
}

核心特性

1. 自动取消

class UserViewModel : ViewModel() {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()
    
    fun loadUsers() {
        viewModelScope.launch {
            // 长时间运行的任务
            val usersFromApi = userRepository.getUsers()
            _users.value = usersFromApi
            
            // 如果 ViewModel 在 getUsers() 执行期间被销毁,
            // 这个协程会自动取消,避免内存泄漏
        }
    }
    
    // 即使有多个协程,也会在 ViewModel 销毁时全部取消
    fun loadMultipleData() {
        viewModelScope.launch {
            launch { loadProfile() }
            launch { loadFriends() }
            launch { loadMessages() }
        }
    }
    
    private suspend fun loadProfile() {
        // 加载个人资料
        delay(1000)
    }
}

2. 默认使用 Main 调度器

class MyViewModel : ViewModel() {
    
    fun updateUI() {
        viewModelScope.launch {
            // 默认在主线程执行,可以安全更新 UI
            _loading.value = true
            
            // 切换到 IO 线程执行耗时操作
            val data = withContext(Dispatchers.IO) {
                apiService.fetchData()
            }
            
            // 自动切换回主线程
            _data.value = data
            _loading.value = false
        }
    }
    
    // 或者直接指定调度器
    fun fetchData() {
        viewModelScope.launch(Dispatchers.IO) {
            val data = apiService.fetchData()
            
            // 更新 UI 需要切回主线程
            withContext(Dispatchers.Main) {
                _data.value = data
            }
        }
    }
}

高级用法

1. 并发处理

class ProductViewModel : ViewModel() {
    
    fun loadProductDetails(productId: String) {
        viewModelScope.launch {
            // 并发执行多个请求
            val detailsDeferred = async { productRepo.getDetails(productId) }
            val reviewsDeferred = async { reviewRepo.getReviews(productId) }
            val relatedDeferred = async { productRepo.getRelatedProducts(productId) }
            
            try {
                // 等待所有结果
                val details = detailsDeferred.await()
                val reviews = reviewsDeferred.await()
                val related = relatedDeferred.await()
                
                // 合并结果
                _productState.value = ProductState.Success(
                    ProductData(details, reviews, related)
                )
            } catch (e: Exception) {
                _productState.value = ProductState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

2. 超时处理

class TimeoutViewModel : ViewModel() {
    
    fun loadWithTimeout() {
        viewModelScope.launch {
            try {
                // 设置超时
                val result = withTimeout(5000) {
                    apiService.fetchData()  // 如果超过5秒,会抛出 TimeoutCancellationException
                }
                _data.value = result
            } catch (e: TimeoutCancellationException) {
                _error.value = "请求超时"
            } catch (e: Exception) {
                _error.value = "发生错误: ${e.message}"
            }
        }
    }
}

3. 重试机制

class RetryViewModel : ViewModel() {
    
    fun loadWithRetry() {
        viewModelScope.launch {
            var retryCount = 0
            val maxRetries = 3
            
            while (retryCount < maxRetries) {
                try {
                    val result = apiService.fetchData()
                    _data.value = result
                    break  // 成功则退出循环
                } catch (e: IOException) {
                    retryCount++
                    if (retryCount == maxRetries) {
                        _error.value = "重试次数已达上限"
                    } else {
                        delay(1000 * retryCount)  // 指数退避
                    }
                }
            }
        }
    }
    
    // 使用 retry 库(需要添加依赖)
    fun loadWithRetryLibrary() {
        viewModelScope.launch {
            val result = retry(
                times = 3,
                initialDelay = 1000,
                maxDelay = 3000
            ) {
                apiService.fetchData()
            }
            _data.value = result
        }
    }
}

实际应用场景

1. 表单提交

class RegistrationViewModel : ViewModel() {
    private val _registrationState = MutableStateFlow<RegistrationState>(RegistrationState.Idle)
    val registrationState: StateFlow<RegistrationState> = _registrationState.asStateFlow()
    
    fun register(email: String, password: String) {
        viewModelScope.launch {
            _registrationState.value = RegistrationState.Loading
            
            try {
                val response = authRepository.register(email, password)
                
                if (response.isSuccessful) {
                    _registrationState.value = RegistrationState.Success
                    // 自动登录
                    autoLogin(email, password)
                } else {
                    _registrationState.value = RegistrationState.Error(response.message)
                }
            } catch (e: Exception) {
                _registrationState.value = RegistrationState.Error(e.message ?: "注册失败")
            }
        }
    }
    
    private suspend fun autoLogin(email: String, password: String) {
        try {
            val token = authRepository.login(email, password)
            // 保存 token 等操作
        } catch (e: Exception) {
            // 自动登录失败,但注册已经成功
        }
    }
    
    sealed class RegistrationState {
        object Idle : RegistrationState()
        object Loading : RegistrationState()
        object Success : RegistrationState()
        data class Error(val message: String) : RegistrationState()
    }
}

2. 分页加载

class PagingViewModel : ViewModel() {
    private val _items = MutableStateFlow<List<Item>>(emptyList())
    private val _isLoading = MutableStateFlow(false)
    private val _hasMore = MutableStateFlow(true)
    
    val items: StateFlow<List<Item>> = _items.asStateFlow()
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
    val hasMore: StateFlow<Boolean> = _hasMore.asStateFlow()
    
    private var currentPage = 0
    private val pageSize = 20
    
    fun loadMore() {
        if (_isLoading.value || !_hasMore.value) return
        
        viewModelScope.launch {
            _isLoading.value = true
            
            try {
                val newItems = itemRepository.getItems(currentPage, pageSize)
                
                if (newItems.isNotEmpty()) {
                    _items.value = _items.value + newItems
                    currentPage++
                    
                    if (newItems.size < pageSize) {
                        _hasMore.value = false
                    }
                } else {
                    _hasMore.value = false
                }
            } catch (e: Exception) {
                // 处理错误
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun refresh() {
        viewModelScope.launch {
            currentPage = 0
            _items.value = emptyList()
            _hasMore.value = true
            loadMore()
        }
    }
}

3. 实时数据流处理

class StockViewModel : ViewModel() {
    private val _stockPrices = MutableStateFlow<Map<String, Double>>(emptyMap())
    val stockPrices: StateFlow<Map<String, Double>> = _stockPrices.asStateFlow()
    
    private var stockJob: Job? = null
    
    fun startStockUpdates() {
        stopStockUpdates()  // 停止之前的更新
        
        stockJob = viewModelScope.launch {
            stockRepository.getStockUpdates()
                .catch { e ->
                    // 处理错误
                    _error.value = e.message
                }
                .collect { stockData ->
                    _stockPrices.value = stockData
                }
        }
    }
    
    fun stopStockUpdates() {
        stockJob?.cancel()
        stockJob = null
    }
    
    // ViewModel 销毁时自动取消
    override fun onCleared() {
        super.onCleared()
        stopStockUpdates()
    }
}

结合其他组件使用

1. 与 Room 数据库结合

class TaskViewModel(
    private val taskDao: TaskDao
) : ViewModel() {
    val tasks: LiveData<List<Task>> = taskDao.getAllTasks().asLiveData()
    
    fun addTask(task: Task) {
        viewModelScope.launch(Dispatchers.IO) {
            taskDao.insert(task)
        }
    }
    
    fun deleteTask(task: Task) {
        viewModelScope.launch(Dispatchers.IO) {
            taskDao.delete(task)
        }
    }
    
    // 观察数据库变化
    init {
        viewModelScope.launch {
            taskDao.getTaskCount()
                .flowOn(Dispatchers.IO)
                .collect { count ->
                    _taskCount.value = count
                }
        }
    }
}

2. 与 Retrofit 网络请求结合

class WeatherViewModel(
    private val weatherService: WeatherService
) : ViewModel() {
    private val _weather = MutableStateFlow<Weather?>(null)
    private val _forecast = MutableStateFlow<List<Forecast>>(emptyList())
    
    val weather: StateFlow<Weather?> = _weather.asStateFlow()
    val forecast: StateFlow<List<Forecast>> = _forecast.asStateFlow()
    
    fun loadWeather(city: String) {
        viewModelScope.launch {
            try {
                // 并发请求天气和预报
                val weatherDeferred = async { weatherService.getCurrentWeather(city) }
                val forecastDeferred = async { weatherService.getForecast(city) }
                
                _weather.value = weatherDeferred.await()
                _forecast.value = forecastDeferred.await()
            } catch (e: Exception) {
                _error.value = "获取天气失败: ${e.message}"
            }
        }
    }
}

3. 在 Compose 中使用

@Composable
fun UserProfileScreen(
    viewModel: UserProfileViewModel = viewModel()
) {
    val userState by viewModel.userState.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    
    Column {
        when (val state = userState) {
            is UserState.Loading -> LoadingIndicator()
            is UserState.Success -> UserProfileContent(state.user)
            is UserState.Error -> ErrorMessage(state.message)
        }
    }
    
    // 副作用:加载数据
    LaunchedEffect(Unit) {
        viewModel.loadUserProfile()
    }
}

class UserProfileViewModel : ViewModel() {
    private val _userState = MutableStateFlow<UserState>(UserState.Loading)
    val userState: StateFlow<UserState> = _userState.asStateFlow()
    
    fun loadUserProfile() {
        viewModelScope.launch {
            _userState.value = UserState.Loading
            try {
                val user = userRepository.getUserProfile()
                _userState.value = UserState.Success(user)
            } catch (e: Exception) {
                _userState.value = UserState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

最佳实践

1. 避免重复调用

class SearchViewModel : ViewModel() {
    private var searchJob: Job? = null
    
    fun search(query: String) {
        // 取消之前的搜索
        searchJob?.cancel()
        
        searchJob = viewModelScope.launch {
            delay(300)  // 防抖
            
            if (query.length >= 2) {
                val results = searchRepository.search(query)
                _searchResults.value = results
            }
        }
    }
}

2. 管理多个协程

class DashboardViewModel : ViewModel() {
    private val jobs = mutableListOf<Job>()
    
    fun loadDashboardData() {
        // 清除之前的任务
        jobs.forEach { it.cancel() }
        jobs.clear()
        
        // 启动多个任务
        jobs.add(viewModelScope.launch { loadStats() })
        jobs.add(viewModelScope.launch { loadRecentActivity() })
        jobs.add(viewModelScope.launch { loadNotifications() })
    }
    
    fun cancelAll() {
        jobs.forEach { it.cancel() }
        jobs.clear()
    }
}

3. 错误处理统一管理

abstract class BaseViewModel : ViewModel() {
    protected val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error.asStateFlow()
    
    protected fun <T> tryExecute(
        block: suspend () -> T,
        onSuccess: (T) -> Unit = {},
        onError: (Exception) -> Unit = { _error.value = it.message }
    ) {
        viewModelScope.launch {
            try {
                val result = block()
                onSuccess(result)
            } catch (e: Exception) {
                onError(e)
            }
        }
    }
}

class MyViewModel : BaseViewModel() {
    fun loadData() {
        tryExecute(
            block = { apiService.getData() },
            onSuccess = { data -> _data.value = data }
        )
    }
}

4. 测试 ViewModelScope

// 测试时需要替换 Dispatchers.Main
@ExperimentalCoroutinesApi
class MyViewModelTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()  // 自定义测试规则
    
    @Test
    fun `test data loading`() = runTest {
        val viewModel = MyViewModel()
        
        viewModel.loadData()
        
        // 验证数据加载逻辑
        advanceUntilIdle()  // 等待协程完成
        
        assertEquals(expectedData, viewModel.data.value)
    }
}

@ExperimentalCoroutinesApi
class MainDispatcherRule : TestWatcher() {
    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(StandardTestDispatcher())
    }
    
    override fun finished(description: Description) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}

原理深入

1. ViewModelScope 的实现

// 简化的实现原理
val ViewModel.viewModelScope: CoroutineScope
    get() {
        // 检查是否已有 scope
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        
        // 创建新的 scope
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    
    override fun close() {
        coroutineContext.cancel()
    }
}

// ViewModel 销毁时
override fun onCleared() {
    super.onCleared()
    // 获取 scope 并关闭它
    val scope = getTag(JOB_KEY) as? CloseableCoroutineScope
    scope?.close()
}

2. 生命周期绑定

class MyViewModel : ViewModel() {
    init {
        // 初始化时启动协程
        viewModelScope.launch {
            // 这个协程会在 ViewModel 销毁时自动取消
            collectDataFromFlow()
        }
    }
    
    private suspend fun collectDataFromFlow() {
        dataFlow
            .catch { e -> 
                // 处理错误
            }
            .collect { data ->
                // 处理数据
                _data.value = data
            }
    }
    
    // 手动控制的生命周期
    private var manualScope: CoroutineScope? = null
    
    fun startManualTask() {
        manualScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
        manualScope?.launch {
            // 需要手动管理生命周期
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        manualScope?.cancel()  // 必须手动取消
    }
}

3. SupervisorJob 的作用

// viewModelScope 使用 SupervisorJob,而不是普通的 Job
// 这意味着一个子协程的失败不会影响其他子协程

class SupervisorExampleViewModel : ViewModel() {
    
    fun startMultipleTasks() {
        viewModelScope.launch {
            // 任务1:如果失败,不会影响任务2
            launch {
                try {
                    task1()
                } catch (e: Exception) {
                    println("Task 1 failed: $e")
                }
            }
            
            // 任务2:会继续执行
            launch {
                task2()
            }
            
            // 任务3:也会继续执行
            launch {
                task3()
            }
        }
    }
    
    private suspend fun task1() {
        delay(100)
        throw RuntimeException("Task 1 failed!")
    }
    
    private suspend fun task2() {
        delay(200)
        println("Task 2 completed")
    }
    
    private suspend fun task3() {
        delay(300)
        println("Task 3 completed")
    }
}

常见问题

1. 协程未被取消

class ProblemViewModel : ViewModel() {
    
    fun problematicTask() {
        viewModelScope.launch {
            // 这个循环不会检查协程是否被取消
            while (true) {
                println("Running...")
                delay(1000)  // ✅ delay 是可取消的挂起函数
            }
        }
    }
    
    fun anotherProblematicTask() {
        viewModelScope.launch {
            // 这个循环不会响应取消
            var i = 0
            while (true) {
                i++  // ❌ 纯计算,不会检查取消
        
                // 解决方案:定期检查取消状态
                ensureActive()  // ✅ 检查协程是否活跃
        
                // 或者使用 yield()
                yield()  // ✅ 让出线程,检查取消
            }
        }
    }
}

2. 内存泄漏

class LeakyViewModel : ViewModel() {
    private val someListener = object : SomeListener {
        override fun onEvent(data: String) {
            // 持有 ViewModel 引用,可能导致泄漏
        }
    }
    
    // 解决方案:使用弱引用或 viewModelScope
    private val safeListener = object : SomeListener {
        override fun onEvent(data: String) {
            viewModelScope.launch {
                // 在 ViewModelScope 中处理,安全
                handleEvent(data)
            }
        }
    }
    
    private suspend fun handleEvent(data: String) {
        // 处理事件
    }
}

总结

  • viewModelScope 是 ViewModel 的扩展属性,提供生命周期感知的协程作用域
  • 自动管理:ViewModel 销毁时,所有协程自动取消
  • 默认在主线程:方便更新 UI,但耗时操作需要切到其他调度器
  • 使用 SupervisorJob:子协程失败不影响其他子协程
  • 最佳实践:避免重复调用、统一错误处理、注意协程取消检查
  • 测试:需要替换 Dispatchers.Main 进行单元测试

使用 viewModelScope 可以安全地在 ViewModel 中执行异步操作,是现代 Android 架构中推荐的方式。