架构一(MVP):Android:玩转Retrofit+OkHttp+Kotlin协程 网络请求架构
架构二(MVVM):Android:玩转网络请求架构 Retrofit+Kotlin协程简单使用(MVVM架构模式)
架构三(MVI):Android:玩转Jetpack Compose之MVI架构——基类中使用页面UiState
自去年Google发布了Compose正式版后,就开始将其逐渐应用至项目中,页面编码方式在改变,架构也在变化,也就是现在Google官方建议的MVI。
本文不打算再叙述对于架构的理解,官网和其他博主已有许多文章;今天主要讲下我在架构迁移中遇到的一些问题,以及解决方案;
官方应用架构指南: https://developer.android.google.cn/topic/architecture#common-principles
注:本文全部内容均为Kotlin语言。(2202年了,相信伙伴们都早已经掌握了kotlin)
在MVI架构中,每个页面都会有且只有一个UiState数据对象,用以驱动页面UI的更新;
这个UiState对象可以是一个sealed class (密封类),也可以是data class(数据类),当然它也可以是一个class,都可以实现。
我这里是用data class, 在使用中有这么一个场景:对于ViewModel中公共的逻辑,我们会抽出个基类封装,当基类处理业务逻辑的过程中,可能需要更新UI。也就是我们要在viewModel基类中更新UiState对象。
我们一想,这还不好办嘛!把UiState中要修改的属性,抽取出个基类来就可以了。
一顿虎操作后,我们的代码是这样的:
ViewModel基类:BaseMviVM.kt
/**
* UI状态 基类
* @param loadState 页面加载状态, 一般默认为 LoadState.Success()
*/
@Suppress("UNCHECKED_CAST")
abstract class BaseMviUiState<T : BaseMviUiState<T>> {
var loadState: LoadState = LoadState.Success()
/**
* 复制对象
*/
abstract fun copyObject(): T
}
/**
* ViewModel 基类 (MVI 架构)
*/
abstract class BaseMviVM<T : BaseMviUiState<T>>(uiState: T) : ViewModel() {
protected val _uiState = MutableStateFlow(uiState)
val uiState: StateFlow<T> = _uiState.asStateFlow()
/**
* 改变页面加载状态
*/
fun changeLoadState(loadState: LoadState) {
_uiState.update {
it.copyObject().apply { this.loadState = loadState }
}
}
}
由于data class不能继承data class, 它可以继承 abstract class, 或者interface。所以这里在abstract class中增加了页面加载状态属性,并在ViewModel基类中封装了修改更新加载状态方法;利用标准函数apply,修改类内部属性。 后期可以再增加属性,这样也方便调用。
用户信息页ViewModel:UserInfoScreenVM.kt
data class UserInfoUiState(
val name: String = "",
val phone: String = ""
): BaseMviUiState<UserInfoUiState>() {
override fun copyObject(): UserInfoUiState = this.copy()
}
/**
* 用户信息页
*/
@HiltViewModel
class UserInfoScreenVM(savedStateHandle: SavedStateHandle): BaseMviVM<UserInfoUiState>(UserInfoUiState()) {
private val mUserId: String = savedStateHandle.get<String>("key_user_id") ?: ""
init {
getUserInfo(mUserId)
}
/**
* 获取用户信息
* @param userId 用户ID
*/
private fun getUserInfo(userId: String) = launch({
// 显示Loading状态UI
changeLoadState(LoadState.Loading())
withContext(Dispatchers.IO){
// 请求网络Api,耗时任务
// .....
}
// 显示成功状态UI
changeLoadState(LoadState.Success())
},{
// 显示失败状态UI
changeLoadState(LoadState.Fail())
})
}
在UserInfoUiState中实现了复制对象方法, 这是因为,copy()方法是data class 类特有的方法。
用户信息页面UI:UserInfoScreen.kt
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun UserInfoScreen(
vm: UserInfoScreenVM = viewModel()
) {
val uiState by vm.uiState.collectAsStateWithLifecycle()
// 显示加载中UI
if(uiState.loadState is LoadState.Loading){
LoadingView()
}
}
就这样运行后,打开页面,却没有显示加载Loading控件;断点来看,_uiState 的更新确实走了,但页面组合却没有重组,猜测肯定是_uiState 没有更新成功;
尝试换成更新其他属性
// 在子类更新name, 此句更新成功
_uiState.update {
it.copyObject(name = "")
}
// 去除参数,此句更新失败
_uiState.update {
it.copyObject()
}
// 子类更新加载状态,此句更新失败
_uiState.update {
it.copy().apply { this.loadState = loadState }
}
// 子类更新名字不同,加载状态也不同,此句更新成功
_uiState.update {
it.copy(name = "新名字").apply { this.loadState = loadState }
}
// 子类更新名字相同,加载状态不同,此句更新失败
_uiState.update {
it.copy(name = "相同名字").apply { this.loadState = loadState }
}
现在我们基本上可以发现,更新是否成功取决于 copy()方法形参的变化是否不同,这是因为StateFlow内部做了diff对比操作,就像之前我们写RecyclerView的Adapter 进行Different 对比数据,决定是否更新列表Item 类似。
如果只通过apply来更新其他属性,就不合适了。 所以我们要把UiState基类中的属性都放进构造方法参数中才可以,让子类override重写 父类的构造方法参数,然后传给父类即可。改造后的代码如下:
/**
* UI状态 基类
* @param loadState 页面加载状态, 一般默认为 LoadState.Success()
*/
@Suppress("UNCHECKED_CAST")
abstract class BaseMviUiState<T : BaseUiState<T>>(open val loadState: LoadState) {
/**
* 复制对象
*/
abstract fun copyObject(loadState: LoadState): T
}
/**
* ViewModel 基类 (MVI 架构)
* @author ssq
*/
abstract class BaseMviVM<T : BaseUiState<T>>(uiState: T) : ViewModel() {
protected val _uiState = MutableStateFlow(uiState)
val uiState: StateFlow<T> = _uiState.asStateFlow()
/**
* 改变页面加载状态
*/
fun changeLoadState(loadState: LoadState) {
_uiState.update {
it.copyObject(loadState)
}
}
}
基类中主要是构造方法变化,抽象方法变化,调用更新变化。
data class UserInfoUiState(
override val loadState: LoadState = LoadState.Success(),
val name: String = "",
val phone: String = ""
): BaseMviUiState<UserInfoUiState>(loadState) {
override fun copyObject(loadState: LoadState): UserInfoUiState = this.copy(loadState = loadState)
}
在data class 实现类,重写父类的属性,实现copyObject 传递参数,复制对象即可。
以上就是今天要讲的内容,本文仅仅叙述了MVI 迁移中比较有特点的问题,也还会有其他问题,需要逐渐解决,最后会充分理解MVI架构的思想。
最后的最后…可能…还会有新的架构出现…