Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南

一、 基本 ViewModel 使用

1.1 最简单的 ViewModel 获取

kotlin 复制代码
@Composable
fun SimpleScreen() {
    val viewModel: MyViewModel = viewModel()
    // 使用 viewModel
}

class MyViewModel : ViewModel() {
    private val _state = mutableStateOf(0)
    val state = _state.asStateFlow()
    
    fun increment() {
        _state.value++
    }
}

1.2 带参数的 ViewModel

kotlin 复制代码
@Composable
fun ScreenWithParam(userId: String) {
    val viewModel: UserViewModel = viewModel(
        factory = object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return UserViewModel(userId) as T
            }
        }
    )
}

二、 ViewModel 作用域策略

2.1 不同层级的作用域

Activity 级作用域
kotlin 复制代码
@Composable
fun ActivityScopedViewModel() {
    val activity = LocalContext.current as ComponentActivity
    val viewModel: SharedViewModel = viewModel(
        viewModelStoreOwner = activity
    )
}
kotlin 复制代码
@Composable
fun NavGraphScopedViewModel(navController: NavHostController) {
    // 使用 navigation 图作为作用域
    val viewModel: GraphViewModel = hiltViewModel() // 使用 Hilt
    // 或者
    val viewModel: GraphViewModel = viewModel(
        viewModelStoreOwner = navController
            .currentBackStackEntryAsState().value
            ?.destination
            ?.parent
            ?.let { navController.getBackStackEntry(it.id) }
    )
}
Destination 级作用域(默认)
kotlin 复制代码
@Composable
fun ScreenWithViewModel() {
    // 默认作用域是当前的 Composable(通常是 NavDestination)
    val viewModel: ScreenViewModel = viewModel()
}

2.2 自定义作用域

kotlin 复制代码
// 创建自定义的 ViewModelStoreOwner
class CustomScope : ViewModelStoreOwner {
    private val viewModelStore = ViewModelStore()
    
    override fun getViewModelStore(): ViewModelStore = viewModelStore
    
    fun clear() {
        viewModelStore.clear()
    }
}

@Composable
fun rememberCustomScope(): CustomScope {
    return remember { CustomScope() }
}

@Composable
fun CustomScopedScreen() {
    val customScope = rememberCustomScope()
    val viewModel: CustomViewModel = viewModel(
        viewModelStoreOwner = customScope
    )
}

三、 高级作用域管理

kotlin 复制代码
// Navigation 设置
NavHost(navController, startDestination = "home") {
    navigation(startDestination = "list", route = "users") {
        composable("list") {
            // 这个 ViewModel 在 users 图内共享
            val sharedViewModel: UsersSharedViewModel = hiltViewModel()
            UserListScreen(sharedViewModel)
        }
        composable("detail/{userId}") {
            // 可以访问同一个 UsersSharedViewModel
            val sharedViewModel: UsersSharedViewModel = hiltViewModel()
            UserDetailScreen(sharedViewModel)
        }
    }
    
    composable("settings") {
        // 不同的作用域,不同的 ViewModel 实例
        val settingsViewModel: SettingsViewModel = hiltViewModel()
        SettingsScreen(settingsViewModel)
    }
}
kotlin 复制代码
@Composable
fun NestedNavigationExample() {
    val navController = rememberNavController()
    
    NavHost(navController, startDestination = "main") {
        navigation(
            startDestination = "dashboard",
            route = "main"
        ) {
            composable("dashboard") {
                // 作用域:main 图
                val mainViewModel: MainViewModel = hiltViewModel()
                DashboardScreen(mainViewModel)
            }
        }
        
        navigation(
            startDestination = "profile",
            route = "user"
        ) {
            composable("profile") {
                // 作用域:user 图(与 main 图隔离)
                val userViewModel: UserViewModel = hiltViewModel()
                ProfileScreen(userViewModel)
            }
        }
    }
}

四、 精确的生命周期控制

4.1 手动管理 ViewModel 生命周期

kotlin 复制代码
@Composable
fun ManualLifecycleControl() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    
    // 使用 remember 和 DisposableEffect 精确控制
    val viewModel = remember {
        ViewModelProvider(
            ViewModelStore(),
            ViewModelProvider.NewInstanceFactory()
        ).get(ManualViewModel::class.java)
    }
    
    DisposableEffect(Unit) {
        // 连接生命周期
        lifecycleOwner.lifecycle.addObserver(viewModel)
        
        onDispose {
            // 清理资源
            lifecycleOwner.lifecycle.removeObserver(viewModel)
            viewModel.onCleared()
        }
    }
}

4.2 使用 ViewModel 清理策略

kotlin 复制代码
class ManagedViewModel(
    private val scope: CoroutineScope
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(UiState())
    val uiState = _uiState.asStateFlow()
    
    private var job: Job? = null
    
    fun loadData() {
        job?.cancel()
        job = scope.launch {
            // 加载数据
            _uiState.value = UiState(loading = true)
            delay(1000)
            _uiState.value = UiState(data = "Loaded")
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        job?.cancel()
        // 清理其他资源
    }
}

@Composable
fun rememberManagedViewModel(): ManagedViewModel {
    val scope = rememberCoroutineScope()
    val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    }
    
    return remember {
        ViewModelProvider(
            viewModelStoreOwner,
            object : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel> create(modelClass: Class<T>): T {
                    return ManagedViewModel(scope) as T
                }
            }
        ).get(ManagedViewModel::class.java)
    }
}

五、 状态保存与恢复

5.1 使用 SavedStateHandle

kotlin 复制代码
class StatefulViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    companion object {
        private const val COUNTER_KEY = "counter"
        private const TEXT_KEY = "text"
    }
    
    var counter by savedStateHandle.saveable { mutableIntStateOf(0) }
        private set
    
    var text by savedStateHandle.saveable { mutableStateOf("") }
        private set
    
    fun increment() {
        counter++
    }
    
    fun updateText(newText: String) {
        text = newText
    }
}

@Composable
fun StatefulScreen() {
    val viewModel: StatefulViewModel = viewModel()
    // 状态会在配置更改时自动保存
}

5.2 自定义状态保存

kotlin 复制代码
class ComplexViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(ComplexState())
    val uiState = _uiState.asStateFlow()
    
    init {
        // 从 SavedStateHandle 恢复
        savedStateHandle.get<ComplexState>("complex_state")?.let {
            _uiState.value = it
        }
    }
    
    fun updateState(newState: ComplexState) {
        _uiState.value = newState
        // 可以在这里手动保存到 SavedStateHandle
    }
    
    @Suppress("FunctionName")
    fun SavedStateHandle.saveComplexState(state: ComplexState) {
        this["complex_state"] = state
    }
}

六、 最佳实践和模式

6.1 ViewModel 提供者模式

kotlin 复制代码
@Composable
inline fun <reified VM : ViewModel> scopedViewModel(
    scope: ViewModelScope = ViewModelScope.Screen,
    key: String? = null,
    factory: ViewModelProvider.Factory? = null
): VM {
    val owner = when (scope) {
        ViewModelScope.Activity -> {
            LocalContext.current as ComponentActivity
        }
        ViewModelScope.NavGraph -> {
            val navController = LocalNavController.current
            navController.currentBackStackEntryAsState().value
                ?.destination
                ?.parent
                ?.let { navController.getBackStackEntry(it.id) }
                ?: LocalViewModelStoreOwner.current
        }
        ViewModelScope.Screen -> LocalViewModelStoreOwner.current
        ViewModelScope.Custom -> {
            // 使用自定义作用域
            LocalCustomScope.current
        }
    }
    
    return viewModel(
        viewModelStoreOwner = checkNotNull(owner),
        key = key,
        factory = factory
    )
}

enum class ViewModelScope {
    Activity, NavGraph, Screen, Custom
}

6.2 依赖注入集成

kotlin 复制代码
// 使用 Hilt 进行依赖注入
@HiltViewModel
class InjectedViewModel @Inject constructor(
    private val repository: UserRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    // ViewModel 逻辑
}

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun UserScreen() {
    // Hilt 自动处理作用域和依赖
    val viewModel: InjectedViewModel = hiltViewModel()
}

七、 测试策略

7.1 测试不同作用域的 ViewModel

kotlin 复制代码
class ViewModelScopeTest {
    
    @Test
    fun testScreenScopedViewModel() = runTest {
        // 测试屏幕作用域的 ViewModel
        val owner = ViewModelStoreOwner { ViewModelStore() }
        val viewModel = TestViewModel(owner)
        
        // 验证行为
    }
    
    @Test
    fun testActivityScopedViewModel() = runTest {
        // 测试 Activity 作用域
        val activity = Robolectric.buildActivity(ComponentActivity::class.java).get()
        val viewModel = ViewModelProvider(activity).get(TestViewModel::class.java)
        
        // 验证跨屏幕的状态保持
    }
}

7.2 Mocking 作用域

kotlin 复制代码
@Composable
fun TestScreen(
    testViewModel: TestViewModel = mockViewModel()
) {
    // 在测试中注入 mock ViewModel
}

fun mockViewModel(): TestViewModel {
    return mockk<TestViewModel> {
        every { uiState } returns MutableStateFlow(TestUiState())
    }
}

总结

  1. 默认作用域 :在 Compose 中,viewModel() 默认使用当前 LocalViewModelStoreOwner,通常是 NavDestination。

  2. 作用域选择

    • 屏幕级:默认,适合单个屏幕
    • 导航图级:适合共享相关屏幕间的状态
    • Activity 级:适合全局共享状态
  3. 生命周期控制

    • 使用 DisposableEffect 进行精确控制
    • 利用 SavedStateHandle 进行状态持久化
    • onCleared() 中清理资源
  4. 最佳实践

    • 根据状态共享需求选择合适的作用域
    • 使用依赖注入(如 Hilt)简化管理
    • 为不同场景设计测试策略

通过合理使用 ViewModel 作用域,可以有效地管理状态的生命周期,避免内存泄漏,并确保状态在正确的上下文中共享和隔离。

相关推荐
某空m2 小时前
【Android】Glide的使用
android·glide
鹏多多2 小时前
flutter-使用EventBus实现组件间数据通信
android·前端·flutter
ShayneLee83 小时前
Nginx修改请求头响应头
android·运维·nginx
廋到被风吹走3 小时前
【数据库】【MySQL】高可用与扩展方案深度解析
android·数据库·mysql
恋猫de小郭3 小时前
Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题
android·前端·flutter
CaspianSea7 小时前
编译Android 16 TV模拟器(一)
android
廋到被风吹走11 小时前
【数据库】【MySQL】InnoDB外键解析:约束机制、性能影响与最佳实践
android·数据库·mysql
峥嵘life12 小时前
Android16 EDLA 认证测试CTS问题分析解决
android·java·服务器
惟恋惜13 小时前
Jetpack Compose 的状态使用之“界面状态”
android·android jetpack