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

MVVM迁移MVI:通用搜索栏意图状态基类实现及调用时机问题

从MVVM迁移MVI时通用搜索栏的实现问题

核心问题

  • 现有MVI架构中,Intent和State均为密封类,无法为多页面创建通用密封类,导致两处代码错误:
    1. SearchViewModel的dispatchIntent方法无法获取intent的keyWord属性
    2. SearchToolbarFragment的OnQueryTextListener中无法直接实例化泛型INTENT并传入query调用dispatchIntentAsync
  • 不确定initSearch(menu)的调用时机:该方法同时依赖menu对象和当前的keyWord

现有代码

SearchViewModel

abstract class SearchViewModel<INTENT : MviIntent, STATE : MviState>(defaultState: STATE) :
    BaseSubscriptionViewModel<INTENT, STATE>(defaultState) {
    private var keyWord: String = ""

    override suspend fun dispatchIntent(intent: INTENT) {
        this.keyWord = intent.keyWord ?: ""  // <= Error
    }
    /**
     * Changed key word async.
     *
     * @param keyWord Key word
     */
    fun updateKeyWordAsync(keyWord: String?) = launchInBackground { // <= Should be replaced by dispatchIntent.
        keyWordWasChanged(keyWord)
    }

    /**
     * Key word was changed.
     *
     * @param keyWord Key word
     */
    protected open suspend fun keyWordWasChanged(keyWord: String?) {
        this.keyWord = keyWord ?: ""
    }
}

SearchToolbarFragment

abstract class SearchToolbarFragment<INTENT : MviIntent, STATE : MviState, VM : SearchViewModel<INTENT, STATE>>(
    private val vmKClass: KClass<VM>
) : ToolbarFragment<INTENT, STATE, VM>() {
    open val menuId: Int = R.menu.menu_search

    private val queryTextListener = object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            viewModel.dispatchIntentAsync(INTENT(query)) // <= Error. How to resolve it?
            // viewModel.updateKeyWordAsync(query)            
            return true
        }

        override fun onQueryTextChange(newText: String?): Boolean {
            viewModel.dispatchIntentAsync(INTENT(newText)) // <= Error. How to resolve it?
            // viewModel.updateKeyWordAsync(query)   
            return true
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProvider(
            store = viewModelStore,
            factory = viewModelFactory,
            defaultCreationExtras = defaultViewModelCreationExtras
        )[vmKClass.java]
    }

    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        super.onCreateMenu(menu, menuInflater)
        menuInflater.inflate(menuId, menu)
        initSearch(menu)
    }

    /**
     * Init search.
     *
     * @param menu Menu
     */
    private fun initSearch(menu: Menu) {
        // ....
    }
}

解决方案

1. 定义通用搜索Intent接口

让所有包含搜索逻辑的Intent类实现统一接口,使父类能安全处理搜索相关Intent:

interface SearchIntent {
    val keyWord: String?
}

修改SearchViewModel的泛型约束,限制INTENT必须实现SearchIntent,并拆分Intent处理逻辑:

abstract class SearchViewModel<INTENT : MviIntent & SearchIntent, STATE : MviState>(defaultState: STATE) :
    BaseSubscriptionViewModel<INTENT, STATE>(defaultState) {
    protected var keyWord: String = ""

    override suspend fun dispatchIntent(intent: INTENT) {
        // 统一处理搜索Intent
        if (intent is SearchIntent) {
            this.keyWord = intent.keyWord ?: ""
            keyWordWasChanged(intent.keyWord)
        }
        // 留给子类处理非搜索相关Intent
        handleSpecificIntent(intent)
    }

    // 抽象方法:子类实现具体Intent的处理
    protected abstract suspend fun handleSpecificIntent(intent: INTENT)

    protected open suspend fun keyWordWasChanged(keyWord: String?) {
        this.keyWord = keyWord ?: ""
    }
}

2. 抽象Fragment的Intent创建逻辑

在SearchToolbarFragment中定义抽象方法,由子类提供具体的搜索Intent实例化逻辑,避免直接操作泛型类:

abstract class SearchToolbarFragment<INTENT : MviIntent & SearchIntent, STATE : MviState, VM : SearchViewModel<INTENT, STATE>>(
    private val vmKClass: KClass<VM>
) : ToolbarFragment<INTENT, STATE, VM>() {
    open val menuId: Int = R.menu.menu_search

    private val queryTextListener = object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            viewModel.dispatchIntentAsync(createSearchIntent(query))
            return true
        }

        override fun onQueryTextChange(newText: String?): Boolean {
            viewModel.dispatchIntentAsync(createSearchIntent(newText))
            return true
        }
    }

    // 抽象方法:子类实现搜索Intent的创建
    protected abstract fun createSearchIntent(keyWord: String?): INTENT

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProvider(
            store = viewModelStore,
            factory = viewModelFactory,
            defaultCreationExtras = defaultViewModelCreationExtras
        )[vmKClass.java]
    }

    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
        super.onCreateMenu(menu, menuInflater)
        menuInflater.inflate(menuId, menu)
        initSearch(menu)
    }

    private fun initSearch(menu: Menu) {
        val searchItem = menu.findItem(R.id.action_search)
        val searchView = searchItem.actionView as SearchView
        searchView.setOnQueryTextListener(queryTextListener)

        // 监听ViewModel的keyWord变化,同步到SearchView
        lifecycleScope.launch {
            // 若State中包含搜索关键字,建议监听State而非直接访问ViewModel属性
            searchView.setQuery(viewModel.keyWord, false)
        }
    }
}

3. 处理initSearch的调用时机

initSearch放在onCreateMenu中是合理的,此时menu已填充完成,可正常获取SearchView。要同步当前keyWord,可通过监听ViewModel的State(推荐)或直接访问ViewModel的keyWord属性,在协程中更新SearchView的显示内容。

子类实现示例

以商品搜索页面为例:

// 具体密封Intent,实现SearchIntent接口
sealed class ProductSearchIntent : MviIntent, SearchIntent {
    data class UpdateSearchKeyWord(override val keyWord: String?) : ProductSearchIntent()
    // 其他业务Intent
}

// 具体ViewModel
class ProductSearchViewModel : SearchViewModel<ProductSearchIntent, ProductSearchState>(ProductSearchState()) {
    override suspend fun handleSpecificIntent(intent: ProductSearchIntent) {
        when (intent) {
            is ProductSearchIntent.UpdateSearchKeyWord -> {
                // 更新State中的搜索关键字,触发UI更新
                updateState { it.copy(searchKeyWord = intent.keyWord ?: "") }
            }
            // 处理其他业务Intent
        }
    }
}

// 具体Fragment
class ProductSearchFragment : SearchToolbarFragment<ProductSearchIntent, ProductSearchState, ProductSearchViewModel>(ProductSearchViewModel::class) {
    override fun createSearchIntent(keyWord: String?): ProductSearchIntent {
        return ProductSearchIntent.UpdateSearchKeyWord(keyWord)
    }
}

内容的提问来源于stack exchange,提问作者Georgiy Chebotarev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:17:51