Jetpack Compose中如何复用可组合项及关联业务逻辑?
推荐实现方案
1. 提取共享业务逻辑到Repository/Use Case层
这是最贴合Jetpack架构规范的做法,把两个ViewModel里重复的业务逻辑(比如获取音乐列表、更新收藏状态等)抽离到Repository或Use Case类中,让ScreenAViewModel和ScreenBViewModel共同依赖这个共享组件,既保证业务逻辑复用,又维持MusicList的无状态特性。
示例代码:
// 共享Repository,封装统一业务逻辑 class MusicRepository(private val api: MusicApi) { suspend fun getMusicList(): List<Music> { return api.fetchMusic() } suspend fun toggleMusicFavorite(musicId: String) { api.updateFavoriteStatus(musicId) } } // ScreenA的ViewModel,依赖共享Repository class ScreenAViewModel(private val musicRepo: MusicRepository) : ViewModel() { private val _musicList = MutableStateFlow<List<Music>>(emptyList()) val musicList = _musicList.asStateFlow() init { loadMusic() } private fun loadMusic() { viewModelScope.launch { _musicList.value = musicRepo.getMusicList() } } fun onFavoriteClicked(musicId: String) { viewModelScope.launch { musicRepo.toggleMusicFavorite(musicId) } } } // ScreenB的ViewModel复用同样的Repository逻辑 class ScreenBViewModel(private val musicRepo: MusicRepository) : ViewModel() { private val _musicList = MutableStateFlow<List<Music>>(emptyList()) val musicList = _musicList.asStateFlow() init { loadMusic() } private fun loadMusic() { viewModelScope.launch { _musicList.value = musicRepo.getMusicList() } } fun onFavoriteClicked(musicId: String) { viewModelScope.launch { musicRepo.toggleMusicFavorite(musicId) } } } // 完全无状态的MusicList,只接收状态和事件回调 @Composable fun MusicList( musicItems: List<Music>, onFavoriteClick: (String) -> Unit, onMusicClick: (Music) -> Unit ) { LazyColumn { items(musicItems) { music -> MusicItem( music = music, onFavoriteClick = { onFavoriteClick(music.id) }, onClick = { onMusicClick(music) } ) } } } // ScreenA中调用 @Composable fun ScreenA(viewModel: ScreenAViewModel = hiltViewModel()) { val musicList by viewModel.musicList.collectAsState() MusicList( musicItems = musicList, onFavoriteClick = viewModel::onFavoriteClicked, onMusicClick = { /* ScreenA专属点击逻辑 */ } ) } // ScreenB中调用 @Composable fun ScreenB(viewModel: ScreenBViewModel = hiltViewModel()) { val musicList by viewModel.musicList.collectAsState() MusicList( musicItems = musicList, onFavoriteClick = viewModel::onFavoriteClicked, onMusicClick = { /* ScreenB专属点击逻辑 */ } ) }
2. 导航作用域共享ViewModel(限定关联场景)
如果ScreenA和ScreenB属于同一个导航图(比如同属一个底部标签页、或为父子导航页面),可以使用导航作用域的ViewModel,让两个界面共享同一个ViewModel实例,业务逻辑自然复用。但这种方式仅适合界面关联性极强的场景,不适合完全独立的两个页面。
示例代码(基于Navigation Compose):
val navController = rememberNavController() NavHost(navController = navController, startDestination = "screenA") { // 定义共享导航作用域 navigation(startDestination = "screenA", route = "music_nav_group") { composable("screenA") { backStackEntry -> // 从父导航节点获取共享ViewModel val sharedViewModel = hiltViewModel<SharedMusicViewModel>(backStackEntry.parentEntry!!) ScreenA(sharedViewModel) } composable("screenB") { backStackEntry -> val sharedViewModel = hiltViewModel<SharedMusicViewModel>(backStackEntry.parentEntry!!) ScreenB(sharedViewModel) } } } // 共享ViewModel,封装所有Music相关业务逻辑 class SharedMusicViewModel(private val musicRepo: MusicRepository) : ViewModel() { private val _musicList = MutableStateFlow<List<Music>>(emptyList()) val musicList = _musicList.asStateFlow() init { loadMusic() } private fun loadMusic() { viewModelScope.launch { _musicList.value = musicRepo.getMusicList() } } fun onFavoriteClicked(musicId: String) { viewModelScope.launch { musicRepo.toggleMusicFavorite(musicId) } } }
3. 状态容器模式(简单场景备选)
如果不想引入Repository/Use Case层级,也可以创建独立的State Holder类(非ViewModel)封装状态和业务逻辑,让界面ViewModel持有该实例,或直接在可组合项中使用(需注意用remember绑定重组生命周期,避免内存泄漏)。这种方式更轻量化,但规范性稍弱,适合简单业务场景。
示例代码:
class MusicStateHolder(private val coroutineScope: CoroutineScope, private val api: MusicApi) { private val _musicList = MutableStateFlow<List<Music>>(emptyList()) val musicList = _musicList.asStateFlow() fun loadMusicList() { coroutineScope.launch { _musicList.value = api.fetchMusic() } } fun toggleFavorite(musicId: String) { coroutineScope.launch { api.updateFavoriteStatus(musicId) // 可根据需求更新本地列表状态 } } } // ScreenA中使用 @Composable fun ScreenA() { val coroutineScope = rememberCoroutineScope() val musicStateHolder = remember { MusicStateHolder(coroutineScope, MusicApi.instance) } LaunchedEffect(Unit) { musicStateHolder.loadMusicList() } val musicList by musicStateHolder.musicList.collectAsState() MusicList( musicItems = musicList, onFavoriteClick = musicStateHolder::toggleFavorite, onMusicClick = { /* ... */ } ) }
为什么不推荐给可组合项加独立ViewModel?
谷歌不推荐的核心原因是:ViewModel的生命周期绑定到界面组件(如Activity、Fragment、导航目的地),而可组合项的生命周期粒度更细(会随重组频繁销毁重建),直接在可组合项中创建ViewModel会导致生命周期不匹配,容易出现状态丢失或内存泄漏问题,同时也会破坏可组合项的无状态性,降低复用性。
内容的提问来源于stack exchange,提问作者Hiroku
相关产品推荐
相关产品推荐

