Compose架构实战密码:用MVI重构电商搜索模块的启示录
·
Compose架构实战密码:用MVI重构电商搜索模块的启示录
电商应用的搜索功能看似简单,实则暗藏玄机。当用户输入关键词时,我们需要实时展示联想词;当用户选择筛选项时,搜索结果需要动态更新;当用户滚动列表时,又要处理分页加载。这些看似独立的交互背后,隐藏着复杂的状态流转和业务逻辑。传统的MVVM架构在这种场景下往往力不从心,而MVI(Model-View-Intent)架构则展现出了独特的优势。
1. 电商搜索模块的架构挑战
电商搜索功能通常包含以下几个核心场景:
- 关键词联想:用户输入时实时展示搜索建议
- 筛选项联动:价格区间、品牌等多维度筛选条件相互影响
- 分页加载:滚动到底部自动加载更多结果
- 搜索历史:持久化存储用户搜索记录
这些场景带来的技术挑战包括:
- 状态爆炸:每个交互都可能影响多个UI组件的状态
- 事件竞争:快速输入可能导致多个搜索请求同时进行
- 状态同步:筛选项变化需要立即反映到搜索结果
// 传统MVVM中可能出现的状态定义
class SearchViewModel {
val keyword = MutableStateFlow("")
val suggestions = MutableStateFlow<List<String>>(emptyList())
val filters = MutableStateFlow<Filters>(Filters())
val results = MutableStateFlow<List<Product>>(emptyList())
val isLoading = MutableStateFlow(false)
val error = MutableStateFlow<Throwable?>(null)
// ...更多状态
}
2. MVI架构的核心思想
MVI架构由三个核心概念组成:
- Model:代表应用的状态,通常是不可变的数据类
- View:反映当前状态的UI呈现
- Intent:用户意图的抽象表示
与MVVM相比,MVI的关键区别在于:
| 特性 | MVVM | MVI |
|---|---|---|
| 状态管理 | 多个分散的StateFlow | 单一聚合的状态对象 |
| 数据流方向 | 双向绑定 | 严格单向数据流 |
| 事件处理 | 直接调用ViewModel方法 | 通过Intent发送 |
| 可预测性 | 中等 | 高 |
// MVI中的状态定义
data class SearchState(
val keyword: String = "",
val suggestions: List<String> = emptyList(),
val filters: Filters = Filters(),
val results: List<Product> = emptyList(),
val isLoading: Boolean = false,
val error: Throwable? = null
)
sealed class SearchIntent {
data class UpdateKeyword(val keyword: String) : SearchIntent()
data class UpdateFilters(val filters: Filters) : SearchIntent()
object LoadMore : SearchIntent()
}
3. 电商搜索的MVI实现
3.1 状态建模
电商搜索的状态应该包含所有必要的UI信息:
data class SearchState(
// 用户输入
val keyword: String = "",
// 搜索建议
val suggestions: List<String> = emptyList(),
// 筛选条件
val filters: Filters = Filters(),
// 搜索结果
val results: List<Product> = emptyList(),
val hasMore: Boolean = true,
// 加载状态
val isLoading: Boolean = false,
val isSuggesting: Boolean = false,
// 错误处理
val error: Throwable? = null
) {
// 派生状态,避免重复计算
val filteredResults: List<Product> by lazy {
results.filter { product ->
filters.brands?.contains(product.brand) ?: true &&
filters.priceRange?.contains(product.price) ?: true
}
}
}
3.2 Intent处理
所有用户操作都转化为Intent发送给ViewModel:
class SearchViewModel : ViewModel() {
private val _state = MutableStateFlow(SearchState())
val state: StateFlow<SearchState> = _state.asStateFlow()
private val intentChannel = Channel<SearchIntent>(capacity = Channel.UNLIMITED)
init {
viewModelScope.launch {
intentChannel.consumeAsFlow().collect { intent ->
when (intent) {
is SearchIntent.UpdateKeyword -> updateKeyword(intent.keyword)
is SearchIntent.UpdateFilters -> updateFilters(intent.filters)
SearchIntent.LoadMore -> loadMore()
}
}
}
}
fun processIntent(intent: SearchIntent) {
viewModelScope.launch {
intentChannel.send(intent)
}
}
private suspend fun updateKeyword(keyword: String) {
_state.update { it.copy(keyword = keyword, isSuggesting = true) }
// 防抖处理
delay(300)
if (keyword.isNotEmpty()) {
val suggestions = repository.getSuggestions(keyword)
_state.update { it.copy(suggestions = suggestions, isSuggesting = false) }
}
}
// 其他处理函数...
}
3.3 Compose UI集成
在Compose中,我们可以轻松订阅状态变化:
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
val state by viewModel.state.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
SearchBar(
keyword = state.keyword,
onKeywordChange = { viewModel.processIntent(SearchIntent.UpdateKeyword(it)) }
)
when {
state.isLoading -> LoadingIndicator()
state.error != null -> ErrorMessage(state.error)
else -> ProductGrid(
products = state.filteredResults,
onLoadMore = { viewModel.processIntent(SearchIntent.LoadMore) }
)
}
}
}
4. MVI在电商场景中的优势
4.1 状态管理的统一性
MVI将所有UI状态集中在一个不可变对象中,带来以下好处:
- 单一数据源:不再需要担心状态分散导致的同步问题
- 时间旅行调试:可以记录状态历史,方便复现问题
- 线程安全:不可变状态天然适合多线程环境
4.2 业务逻辑的可测试性
由于所有状态变更都集中在ViewModel中,测试变得非常简单:
@Test
fun `update keyword should trigger suggestions`() = runTest {
val viewModel = SearchViewModel(mockRepository)
viewModel.processIntent(SearchIntent.UpdateKeyword("手机"))
advanceUntilIdle()
val state = viewModel.state.value
assertEquals("手机", state.keyword)
assertTrue(state.suggestions.isNotEmpty())
}
4.3 性能优化空间
MVI架构为性能优化提供了良好基础:
- 防抖处理:在Intent处理层可以轻松实现输入防抖
- 请求取消:当新Intent到达时,可以取消未完成的请求
- 缓存策略:可以在状态管理层实现智能缓存
private var searchJob: Job? = null
private suspend fun updateKeyword(keyword: String) {
// 取消之前的搜索
searchJob?.cancel()
_state.update { it.copy(keyword = keyword) }
searchJob = viewModelScope.launch {
delay(300) // 防抖
val suggestions = repository.getSuggestions(keyword)
_state.update { it.copy(suggestions = suggestions) }
}
}
5. 实战建议与陷阱规避
5.1 状态设计原则
- 最小化原则:只存储必要的状态,派生状态通过计算获得
- 扁平化原则:避免嵌套过深的状态结构
- 不可变原则:所有状态修改都通过copy创建新实例
5.2 常见陷阱
- 过度聚合状态:将完全不相关的状态强行合并
- 忽视中间状态:忘记处理加载中等过渡状态
- Intent设计不当:将多个操作合并到一个Intent中
5.3 性能优化技巧
// 使用派生状态减少重组
val filteredProducts by derivedStateOf {
state.results.filter { it.matches(state.filters) }
}
// 使用rememberSaveable保存UI状态
var expandedFilters by rememberSaveable { mutableStateOf(false) }
在电商搜索这种复杂交互场景下,MVI架构展现出了比传统MVVM更强大的优势。它通过单向数据流和集中状态管理,让代码更易于理解、测试和维护。虽然学习曲线略陡峭,但一旦掌握,将大幅提升复杂界面的开发效率和质量。
更多推荐




所有评论(0)