简单实现MVI模式

作者: 奔跑吧李博 | 来源:发表于2023-11-06 18:52 被阅读0次

MVI 就像是 MVC 的一种衍生产物, 我们知道 MVP 也是 MVC 的衍生产物,MVVM 也是 MVP 的一种衍生

MVI 的愿景是能让 View 触发刷新的状态只有一个。

MVI 各层

MVI 将架构分成了三个部分:

Model: 与MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态
View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新
Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求

MVI强调数据的单向流动,主要分为以下几步:

用户操作以Intent的形式通知Model
Model基于Intent更新State
View接收到State变化刷新UI。
数据永远在一个环形结构中单向流动,不能反向流动:

示例:

在 Activity / Fragment 这种第一层级的视图中,定义触发 Intent 的逻辑,一般是通过点击事件等操作,和 MVVM 中的触发逻辑一样,不过这里要在协程中触发,并且使用 Channel 或其它 Flow 工具,因为这样做是一种响应式编程的逻辑。

// 新闻界面所有的用户操作, 基本类
class MainActivity : AppCompatActivity() {

    // 新闻界面所有的用户操作, 基本类
    sealed class NewsIntent

    class UserClickNewsIntent(val url: String): NewsIntent() // 用户点击具体某个新闻Item的交互
    object RefreshNewsIntent : NewsIntent()  // 用户刷新新闻列表的交互

    private val viewModel: NewsViewModel = ViewModelProvider(this)[NewsViewModel::class.java]

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initListener()
        observerUiState()
    }

    private fun initListener() {
        findViewById<Button>(R.id.refreshButton).setOnClickListener {
            sendIntent(RefreshNewsIntent)
        }
    }

    private fun observerUiState() {
        CoroutineScope(Dispatchers.Default).launch {
            viewModel.newsUiState.collect {
                when(it) {
                    is NewsUiStateInitial -> {

                    }
                    is Loading -> {

                    }
                    is LoadingSuccess -> {
                        Toast.makeText(applicationContext, "加载数据成功", Toast.LENGTH_SHORT).show()
                    }
                    is LoadingFail -> {

                    }
                }
            }
        }
    }

    private fun sendIntent(intent: NewsIntent) {
        CoroutineScope(Dispatchers.Default).launch {
            viewModel.newsIntent.send(intent)
        }
    }
}

并且在 VieawModel 中定义数据流的开端:

class NewsViewModel : ViewModel(){
    // 定义接收意图的通道
    val newsIntent = Channel<MainActivity.NewsIntent>()

    private val _newsUiState = MutableStateFlow<NewsUiState>(NewsUiStateInitial)
    val newsUiState: StateFlow<NewsUiState> = _newsUiState

    init {
        viewModelScope.launch {
            handleIntent()
        }
    }

    private suspend fun handleIntent() {
        newsIntent.consumeAsFlow().collect {
            when (it) {
                is MainActivity.UserClickNewsIntent -> intoNewsItem(it.url)  // 处理 UserClickNewsIntent 意图
                is MainActivity.RefreshNewsIntent -> fetchNews()  // 处理 RefreshNewsIntent 意图
            }
        }
    }

    private fun intoNewsItem(url: String) {

    }

    private fun fetchNews() {
        //模拟耗时操作并获取到数据
        _newsUiState.value = LoadingSuccess(ArrayList<NewsItem>())
    }
}
定义 UiState 作为 View 的唯一数据源

我们通过归纳新闻页的页面状态,可以分成 初始态、加载中、加载成功、加载失败 四个状态,那么我们将状态收归到 UiState 中去,使用 功能名+UiState 来命名:

sealed class NewsUiState

object NewsUiStateInitial: NewsUiState() // 初始状态
object Loading: NewsUiState() // 正在加载
class LoadingSuccess(val newsList: List<NewsItem>): NewsUiState()  // 加载成功
class LoadingFail(val errorMessage: String): NewsUiState()  // 加载失败

ViewModel 持有 NewsUiState,并暴露出去,类似于 LiveData 那样子 :
Activity 依赖 ViewModel 持有的 UiState, 用于进行视图刷新:

CoroutineScope(Dispatchers.Default).launch {
            viewModel.newsUiState.collect {
                when(it) {
                    is NewsUiStateInitial -> { }
                    is Loading -> { }
                    is LoadingSuccess -> {
                        Toast.makeText(applicationContext, "加载数据成功", Toast.LENGTH_SHORT).show()
                    }
                    is LoadingFail -> { }
                }
            }
        }

参考:
https://blog.csdn.net/rikkatheworld/article/details/126472940
https://github.com/HuJianChong/MyWanAndroid

相关文章

网友评论

    本文标题:简单实现MVI模式

    本文链接:https://www.haomeiwen.com/subject/hyzrldtx.html