宝子们,咱就是说,去年俺写了篇文章 - Android 如何搭建一个优雅的 MVI 架构,当时是基于传统的布局方式 XML 写的,但是这 Android 开发界的时尚潮流更新真的太快了,Jetpack Compose 闪亮登场,直接在 UI 界 C 位出道,所以,咱也不能落后,这次就来盘一盘基于 MVI 架构的 Jetpack Compose 到底是咋个回事?
简介
在 Jetpack Compose 中,搭配 MVI 架构会更能体现其优势。Model 依旧是存储应用状态的数据结构,如用户信息,列表数据等。View 则是通过 Compose 函数构建的界面,以声明式的方式将 Model 状态展示出来,比如用 Text 组件显示 Model 中的文本数据。Intent 是用户的操作或事件,像点击按钮等动作。当有 Intent 产生,会触发 Model 状态更新,而 Compose 会自动重新组合 UI 来反映 Model 的新状态,它能感知状态变化。
这种架构让 Compose 构建的 UI 数据流向清晰,便于开发者理解和维护,提升了开发效率,同时也能更好地应对复杂的 UI 变化。关于 MVI 架构模式,有兴趣的可以看我之前的文章,那里介绍比较详细,这里不再赘述。
定义 UI 状态
创建一个数据类来表示页面的状态,这里包含了是否正在加载数据,获取到的实际数据以及可能出现的错误信息等状态字段。
data class UiState(
val isLoading: Boolean = false,
val data: List<NetDataBean>? = null,
val errorMessage: String? = null
)
定义用户意图
定义代表不同操作意图的类,常见的做法是使用密封类,这里定义了名为 FetchNetData 和 FetchDefaultData 的意图,分别代表从网络获取数据和获取默认本地数据两个操作。
sealed class UiIntent {
data object FetchNetData : UiIntent()
data object FetchDefaultData : UiIntent()
}
定义 ViewModel
ViewModel 负责协调 Model 和 View 之间的交互,处理业务逻辑。这里通过 mutableStateOf 创建了可观察的状态,遵循 Compose 的状态管理原则,这样界面可以自动根据状态变化进行重绘,dispatch 用于接收各种意图,根据不同的 Intent 来更新状态。
class ContentViewModel : ViewModel() {
var uiStates by mutableStateOf(UiState())
private set
init {
dispatch(UiIntent.FetchDefaultData)
}
fun dispatch(intent: UiIntent) {
when (intent) {
is UiIntent.FetchNetData -> getHttpContent()
is UiIntent.FetchDefaultData -> getDefaultData()
}
}
private fun getDefaultData() {
val list = arrayListOf<NetDataBean>()
repeat(10) {
list.add(NetDataBean(it.toString(), "No.$it"))
}
uiStates = uiStates.copy(isLoading = false, data = list, errorMessage = null)
}
private fun getHttpContent() = netRequest {
request {
uiStates = uiStates.copy(isLoading = true)
val hashMap = hashMapOf<String, String>()
hashMap["param1"] = "param1"
hashMap["param2"] = "param2"
RequestHelper.instance.getNetData(hashMap)
}
success {
uiStates = uiStates.copy(isLoading = false, data = it, errorMessage = null)
}
error {
uiStates = uiStates.copy(isLoading = false, data = emptyList(), errorMessage = it)
}
}
}
mutableStateOf 是线程安全的,也能够保证状态的更新能通知到观察者,而且 mutableStateOf 在 ViewModel 中使用不需要搭配 remember 来保持状态,因为 ViewModel 本身就可以缓存状态,并可在配置更改后持久保留相应状态。
其中,getDefaultData 获取默认的测试数据,getHttpContent 用于获取网络数据,网络请求的部分代码如下:
interface HttpApi {
@GET("/hi/hi/hi/test/android")
suspend fun getNetData(@QueryMap params: HashMap<String, String>): BaseResp<List<NetDataBean>>
}
class RequestHelper {
private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)
companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}
suspend fun getNetData(params: HashMap<String, String>) = httpApi.getNetData(params)
}
这里使用的是基于 Retrofit 二次封装的网络请求框架,感兴趣的可以看我的另一篇文章 - 如何让 Android 网络请求像诗一样优雅,里面详细讲解了我是如何一步一步封装这个网络请求框架的,这里不再赘述。
创建 Compose 界面
在 Compose 函数中构建界面,并与 ViewModel 的状态进行关联来展示相应的内容。这样一旦 UI 状态发生变化,界面就会自动重组更新。
@Composable
fun ContentScreen(viewModel: ContentViewModel = viewModel()) {
val states = viewModel.uiStates
Column(modifier = Modifier.fillMaxSize()) {
Button(onClick = {
viewModel.dispatch(UiIntent.FetchNetData)
}) { Text("FetchNetData") }
if (states.isLoading) {
CircularProgressIndicator()
} else {
val dataList = states.data
Text(
text = if (dataList.isNullOrEmpty()) (states.errorMessage ?: "")
else dataList.toString(), modifier = Modifier.fillMaxSize()
)
}
}
}
这里使用 viewModel() 来获取与当前组合相关联的 ViewModel 实例,使用这个函数需要额外引入依赖:
implementation (libs.androidx.lifecycle.viewmodel.compose)
viewModel() 是个 Composable 函数,这个函数内部会检查是否已经存在合适的 ViewModel 实例,如果存在,它会返回现有的实例,如果不存在,它会创建一个新的实例并将其与当前组合相关联。当 Composable 函数进行重组时,只要对应的 ViewModelStoreOwner(通常是 Activity 或 Fragment)仍有效,就会复用之前创建的 ViewModel 实例。
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
viewModelStoreOwner.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)
这种机制确保了 ViewModel 的生命周期与所属的 ViewModelStoreOwner(通常是 Activity 或 Fragment)相匹配,而不是随着 Composable 函数的每次重组而重新创建。这样可以有效地保存和管理数据,避免不必要的数据丢失和重复加载,例如在屏幕旋转等配置变化的情况下,ViewModel 中的数据依然能够被保留和复用。
注意:请勿将 ViewModel 实例向下传递到其他可组合函数,这样会导致可组合函数与 ViewModel 类型形成耦合,从而降低可重用性,向下传递允许多个可组合项调用 ViewModel 函数并修改其状态,从而更难调试问题。作为替代方案,我们可以向下传递必要的状态。
总结
在 Jetpack Compose 中,搭配 MVI 架构会更能体现其优势。
- UI 更新机制更适配:在 Jetpack Compose 中,UI 是通过可组合函数构建的,这些函数会根据数据变化自动重新组合。MVI 的单向数据流和状态驱动 UI 更新的方式与 Compose 高度适配,使得 UI 能够高效精准地响应数据变化。而传统 XML 布局主要依赖于手动调用更新方法,在处理复杂数据变化时不够灵活。
- 数据和 UI 分离更清晰:Compose 的函数式编程风格使得它和 MVI 结合时,数据和 UI 的分离更加自然。在传统 XML 中,布局和数据逻辑容易交织在 Activity 或 Fragment 中。MVI 在 Compose 中能让 Model 独立管理数据,Composable 专注于 UI 渲染。
- 复用性更好:MVI 架构下的 Composable 组件基于清晰的状态管理,复用性更强。相比之下,XML 布局复用主要是布局文件复用,在涉及数据和业务逻辑时,复用难度相对较大。