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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 17:15:30