Android:玩转Jetpack Compose之MVI架构——基类中使用页面UiState

系列文章目录

架构一(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)

官方推荐架构示意图


一、基类中使用页面UiState

在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 类似。

二、改造UiState基类

如果只通过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架构的思想。

最后的最后…可能…还会有新的架构出现…

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,我来回答你的问题。 首先,Kotlin 是一种基于 JVM 的静态类型编程语言,它的语法简洁易懂,支持函数式编程和面向对象编程。 协程是 Kotlin 的一种轻量级线程,可以实现异步编程和并发执行。Retrofit 是一款网络请求库,它可以帮助我们轻松地实现网络请求和数据解析。MVVM 是一种软件架构模式,可以将应用程序分为三个部分:模型、视图和视图模型。 下面是一个基于 Kotlin + 协程 + Retrofit + MVVM 的网络请求的优雅实现: 1. 定义 API 接口 首先定义 API 接口,使用 Retrofit 注解来描述请求方法和参数。 ```kotlin interface ApiService { @GET("api/news") suspend fun getNews(@Query("category") category: String): NewsResponse } ``` 2. 创建数据模型 根据 API 接口的返回数据,我们可以创建一个数据模型。 ```kotlin data class News(val title: String, val content: String) data class NewsResponse(val code: Int, val message: String, val newsList: List<News>) ``` 3. 创建 ViewModel ViewModel 是连接数据模型和视图的间层,它处理数据逻辑并提供可观察的数据。 ```kotlin class NewsViewModel : ViewModel() { private val _newsList = MutableLiveData<List<News>>() val newsList: LiveData<List<News>> = _newsList fun loadNews(category: String) { viewModelScope.launch { val response = retrofit.create(ApiService::class.java).getNews(category) if (response.code == 200) { _newsList.value = response.newsList } } } } ``` 4. 创建视图 视图负责展示数据,并与用户交互。 ```kotlin class NewsActivity : AppCompatActivity() { private val viewModel by viewModels<NewsViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_news) viewModel.newsList.observe(this, Observer { newsList -> // 更新视图 }) viewModel.loadNews("tech") } } ``` 通过使用 Kotlin + 协程 + Retrofit + MVVM,我们可以实现优雅地网络请求,代码简洁易懂,逻辑清晰。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

两块三刀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值