Android Jetpack Compose+Dagger-Hilt:Compose中如何使用动态ViewModel?
我正在开发的应用顶部有一个可切换栏,能在Jetpack Compose标准LargeTopAppBar与自定义SearchAppBar间切换。原本无法将当前ViewModel的搜索函数传递给SearchAppBar,于是直接传入ViewModel本身,搜索功能正常运行。但我希望该顶部栏功能能在依赖不同ViewModel(搜索功能实现逻辑一致)的其他页面复用,因此想请教:能否在不指定具体ViewModel类型(如PreviousArticlesViewModel)的情况下,将Hilt ViewModel作为参数传入?
现有代码
控制顶部栏切换的MainAppBar代码
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable fun MainAppBar( searchWidgetState: SearchWidgetState, onCloseClicked: () -> Unit, onSearchTriggered: () -> Unit, titleText: String, scrollBehavior: TopAppBarScrollBehavior ) { AnimatedContent( targetState = searchWidgetState, transitionSpec = {/* TransitionSpecs */ } ) { targetState -> when (targetState) { SearchWidgetState.CLOSED -> { DefaultAppBar( titleText = titleText, onSearchClicked = onSearchTriggered, scrollBehavior = scrollBehavior ) } SearchWidgetState.OPENED -> { SearchAppBar( onCloseClicked = onCloseClicked ) } } } }
SearchAppBar代码(当前传入固定ViewModel,需改为动态)
@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchAppBar( onCloseClicked: () -> Unit, viewModel: PreviousArticlesViewModel = hiltViewModel() ) { val searchText by viewModel.searchText.collectAsState() Surface( modifier = Modifier .fillMaxWidth() .height(60.dp), color = MaterialTheme.colorScheme.surfaceVariant ) { TextField( modifier = Modifier .fillMaxWidth(), value = searchText, // 需从ViewModel传递的搜索函数 // 直接传viewmodel.SearchTextChange(it)无法工作 onValueChange = viewModel::onSearchTextChange /* ......... */ ) } }
PreviousArticlesViewModel代码
@HiltViewModel class PreviousArticlesViewModel @Inject constructor ( private val repository: AphorismRepository ): ViewModel() { // 搜索功能 private val _searchText = MutableStateFlow("") val searchText = _searchText.asStateFlow() private var _previousArticles = MutableStateFlow(listOf<ArticleAndImage>()) val previousArticles = searchText .combine(_previousArticles) { text, Articles -> if(text.isBlank()) { Articles } else { Articles.filter { it.matchesSearchQuery(text) } } } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), _previousArticles.value ) // 搜索功能放在此ViewModel的原因:_previousArticles在init{}从仓库获取 fun onSearchTextChange(text: String) { _searchText.value = text } }
解决方案
通过定义接口抽象搜索功能,让所有需要搜索能力的ViewModel实现该接口,就能实现不绑定具体ViewModel类型的复用。
1. 创建搜索功能接口
定义一个包含搜索所需状态和方法的接口,统一搜索功能的契约:
interface SearchableViewModel { val searchText: StateFlow<String> fun onSearchTextChange(text: String) }
2. 让现有ViewModel实现接口
修改PreviousArticlesViewModel,让它实现上面的接口:
@HiltViewModel class PreviousArticlesViewModel @Inject constructor ( private val repository: AphorismRepository ): ViewModel(), SearchableViewModel { // 搜索功能 private val _searchText = MutableStateFlow("") override val searchText = _searchText.asStateFlow() // ... 原有代码保持不变 override fun onSearchTextChange(text: String) { _searchText.value = text } }
3. 修改SearchAppBar参数类型
把SearchAppBar的ViewModel参数改为接口类型,同时移除默认的hiltViewModel()(由调用方传入对应页面的ViewModel):
@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchAppBar( onCloseClicked: () -> Unit, viewModel: SearchableViewModel ) { val searchText by viewModel.searchText.collectAsState() Surface( modifier = Modifier .fillMaxWidth() .height(60.dp), color = MaterialTheme.colorScheme.surfaceVariant ) { TextField( modifier = Modifier.fillMaxWidth(), value = searchText, onValueChange = viewModel::onSearchTextChange // ... 其他原有参数保留 ) } }
4. 更新MainAppBar传递ViewModel
给MainAppBar添加接口类型的ViewModel参数,传递给SearchAppBar:
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable fun MainAppBar( searchWidgetState: SearchWidgetState, onCloseClicked: () -> Unit, onSearchTriggered: () -> Unit, titleText: String, scrollBehavior: TopAppBarScrollBehavior, searchableViewModel: SearchableViewModel // 添加接口类型参数 ) { AnimatedContent( targetState = searchWidgetState, transitionSpec = {/* TransitionSpecs */} ) { targetState -> when (targetState) { SearchWidgetState.CLOSED -> { DefaultAppBar( titleText = titleText, onSearchClicked = onSearchTriggered, scrollBehavior = scrollBehavior ) } SearchWidgetState.OPENED -> { SearchAppBar( onCloseClicked = onCloseClicked, viewModel = searchableViewModel // 传递ViewModel ) } } } }
5. 在页面中使用MainAppBar
在具体页面中,获取当前页面的ViewModel(比如PreviousArticlesViewModel),并传递给MainAppBar:
@Composable fun PreviousArticlesScreen() { val viewModel: PreviousArticlesViewModel = hiltViewModel() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() // ... 其他页面状态定义 Scaffold( topBar = { MainAppBar( searchWidgetState = // 你的搜索状态变量, onCloseClicked = { /* 关闭搜索栏逻辑 */ }, onSearchTriggered = { /* 触发打开搜索栏逻辑 */ }, titleText = "往期文章", scrollBehavior = scrollBehavior, searchableViewModel = viewModel // 传入当前页面的ViewModel ) } ) { paddingValues -> // 页面内容区域 } }
后续其他需要搜索功能的页面,只需让对应的ViewModel实现SearchableViewModel接口,再把ViewModel传入MainAppBar即可,无需修改顶部栏的核心代码。
内容的提问来源于stack exchange,提问作者zkvvoob

