MVVM迁移MVI:通用搜索栏意图状态基类实现及调用时机问题
从MVVM迁移MVI时通用搜索栏的实现问题
核心问题
- 现有MVI架构中,Intent和State均为密封类,无法为多页面创建通用密封类,导致两处代码错误:
SearchViewModel的dispatchIntent方法无法获取intent的keyWord属性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
相关产品推荐
相关产品推荐

