You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 04:20:28