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 作用域,可以有效地管理状态的生命周期,避免内存泄漏,并确保状态在正确的上下文中共享和隔离。

相关推荐
千里马学框架21 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work1 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work1 天前
ch21 签名、校验与发版
kotlin
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui