Android 架构实战MVI进阶

MVI架构的原理和流程

MVI架构是一种基于响应式编程的架构模式,它将应用程序分为四个核心组件:模型(Model)、视图(View)、意图(Intent)和状态(State)。 原理:

  • 模型(Model):负责处理数据的状态和逻辑。
  • 视图(View):负责展示数据和用户界面。
  • 意图(Intent):代表用户的操作,如按钮点击、输入等。
  • 状态(State):反映应用程序的当前状态。

流程:

  1. 用户通过视图(View)发起意图(Intent)。
  2. 意图(Intent)被传递给模型(Model)。
  3. 模型(Model)根据意图(Intent)进行状态(State)的更新。
  4. 状态(State)的变化被传递给视图(View),视图(View)进行相应的界面更新。

优点:

  • 单向数据流:通过单向的数据流动,可确保状态的一致性和可预测性。
  • 响应式特性:MVI利用响应式编程的思想,实现了对状态变化的高效处理。
  • 易于测试:由于数据流的清晰性,测试模型的行为变得更加容易。

缺点:

  • 学习曲线较陡:相对于传统的MVC或MVP,MVI架构需要开发者熟悉响应式编程的概念和工具。
  • 增加了一些复杂性:引入状态管理和数据流管理,可能会增加一定的复杂性。

单向数据流

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

一个Sample快速搭建一个MVI架构的项目

代码示例

代码结构如下:

Sample中的依赖库

// Added Dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"

代码中使用以下API进行请求

https://reqres.in/api/users

将得到结果:

1. 数据层

1.1 User

定义User的data class

package com.my.mvi.data.modeldata class User(@Json(name = "id")val id: Int = 0,@Json(name = "first_name")val name: String = "",@Json(name = "email")val email: String = "",@Json(name = "avator")val avator: String = ""
)

1.2 ApiService

定义ApiService,getUsers方法进行数据请求

package com.my.mvi.data.apiinterface ApiService {@GET("users")suspend fun getUsers(): List<User>
}

1.3 Retrofit

创建Retrofit实例

object RetrofitBuilder {private const val BASE_URL = "https://reqres.in/api/user/1"private fun getRetrofit() = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(MoshiConverterFactory.create()).build()val apiService: ApiService = getRetrofit().create(ApiService::class.java)}

1.4 Repository

定义Repository,封装API请求的具体实现

package com.my.mvi.data.repositoryclass MainRepository(private val apiService: ApiService) {suspend fun getUsers() = apiService.getUsers()}

2. UI层

Model定义完毕后,开始定义UI层,包括View、ViewModel以及Intent的定义

2.1 RecyclerView.Adapter

首先,需要一个RecyclerView来呈现列表结果,定义MainAdapter如下:

package com.my.mvi.ui.main.adapterclass MainAdapter(private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {fun bind(user: User) {itemView.textViewUserName.text = user.nameitemView.textViewUserEmail.text = user.emailGlide.with(itemView.imageViewAvatar.context).load(user.avatar).into(itemView.imageViewAvatar)}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =DataViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent,false))override fun getItemCount(): Int = users.sizeoverride fun onBindViewHolder(holder: DataViewHolder, position: Int) =holder.bind(users[position])fun addData(list: List<User>) {users.addAll(list)}}

item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/container"android:layout_width="match_parent"android:layout_height="60dp"><ImageViewandroid:id="@+id/imageViewAvatar"android:layout_width="60dp"android:layout_height="0dp"android:padding="4dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/textViewUserName"style="@style/TextAppearance.AppCompat.Large"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="8dp"android:layout_marginTop="4dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"app:layout_constraintTop_toTopOf="parent"/><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/textViewUserEmail"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="@+id/textViewUserName"app:layout_constraintTop_toBottomOf="@+id/textViewUserName" /></androidx.constraintlayout.widget.ConstraintLayout>

2.2 Intent

定义Intent用来包装用户Action

package com.my.mvi.ui.main.intentsealed class MainIntent {object FetchUser : MainIntent()}

2.3 State

定义UI层的State结构体

sealed class MainState {object Idle : MainState()object Loading : MainState()data class Users(val user: List<User>) : MainState()data class Error(val error: String?) : MainState()}

2.4 ViewModel

ViewModel是MVI的核心,存放和管理State,同时接受Intent并进行数据请求

package com.my.mvi.ui.main.viewmodelclass MainViewModel(private val repository: MainRepository
) : ViewModel() {val userIntent = Channel<MainIntent>(Channel.UNLIMITED)private val _state = MutableStateFlow<MainState>(MainState.Idle)val state: StateFlow<MainState>get() = _stateinit {handleIntent()}private fun handleIntent() {viewModelScope.launch {userIntent.consumeAsFlow().collect {when (it) {is MainIntent.FetchUser -> fetchUser()}}}}private fun fetchUser() {viewModelScope.launch {_state.value = MainState.Loading_state.value = try {MainState.Users(repository.getUsers())} catch (e: Exception) {MainState.Error(e.localizedMessage)}}}
}

我们在handleIntent中订阅userIntent并根据Action类型执行相应操作。本case中当出现FetchUser的Action时,调用fetchUser方法请求用户数据。用户数据返回后,会更新State,MainActivity订阅此State并刷新界面。

2.5 ViewModelFactory

构造ViewModel需要Repository,所以通过ViewModelFactory注入必要的依赖

class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {override fun <T : ViewModel?> create(modelClass: Class<T>): T {if (modelClass.isAssignableFrom(MainViewModel::class.java)) {return MainViewModel(MainRepository(apiService)) as T}throw IllegalArgumentException("Unknown class name")}}

2.6 定义MainActivity

package com.my.mvi.ui.main.viewclass MainActivity : AppCompatActivity() {private lateinit var mainViewModel: MainViewModelprivate var adapter = MainAdapter(arrayListOf())override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)setupUI()setupViewModel()observeViewModel()setupClicks()}private fun setupClicks() {buttonFetchUser.setOnClickListener {lifecycleScope.launch {mainViewModel.userIntent.send(MainIntent.FetchUser)}}}private fun setupUI() {recyclerView.layoutManager = LinearLayoutManager(this)recyclerView.run {addItemDecoration(DividerItemDecoration(recyclerView.context,(recyclerView.layoutManager as LinearLayoutManager).orientation))}recyclerView.adapter = adapter}private fun setupViewModel() {mainViewModel = ViewModelProviders.of(this,ViewModelFactory(ApiHelperImpl(RetrofitBuilder.apiService))).get(MainViewModel::class.java)}private fun observeViewModel() {lifecycleScope.launch {mainViewModel.state.collect {when (it) {is MainState.Idle -> {}is MainState.Loading -> {buttonFetchUser.visibility = View.GONEprogressBar.visibility = View.VISIBLE}is MainState.Users -> {progressBar.visibility = View.GONEbuttonFetchUser.visibility = View.GONErenderList(it.user)}is MainState.Error -> {progressBar.visibility = View.GONEbuttonFetchUser.visibility = View.VISIBLEToast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()}}}}}private fun renderList(users: List<User>) {recyclerView.visibility = View.VISIBLEusers.let { listOfUsers -> listOfUsers.let { adapter.addData(it) } }adapter.notifyDataSetChanged()}
}

MainActivity中订阅mainViewModel.state,根据State处理各种UI显示和刷新。

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".ui.main.view.MainActivity"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="match_parent"android:visibility="gone" /><ProgressBarandroid:id="@+id/progressBar"android:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"android:visibility="gone"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><Buttonandroid:id="@+id/buttonFetchUser"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/fetch_user"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>

如上,一个完整的MVI项目完成了。

实战讲解和代码示例

为了更好地理解MVI架构,让我们通过一个例子进行实战演示。我们将创建一个天气预报应用,展示当前天气和未来几天的天气预报信息。 在代码示例中,我们会用到以下库:

  • RxJava:用于处理响应式数据流。
  • LiveData:用于将数据流连接到视图。
 首先,我们定义模型(Model)的状态(State)类,包含天气预报的相关信息,例如温度、湿度和天气状况等。
data class WeatherState(val temperature: Float,val humidity: Float,val condition: String
)

接下来,我们创建视图(View)界面,展示天气信息,并提供一个按钮用于刷新数据。


class WeatherActivity : AppCompatActivity() {// 初始化ViewModelprivate val viewModel: WeatherViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_weather)// 监听状态变化,更新UIviewModel.weatherState.observe(this, Observer { state ->// 更新温度、湿度和天气状况的显示temperatureTextView.text = state.temperature.toString()humidityTextView.text = state.humidity.toString()conditionTextView.text = state.condition})// 刷新按钮点击事件refreshButton.setOnClickListener {// 发送刷新数据的意图viewModel.processIntent(RefreshIntent)}}
}

然后,我们创建意图(Intent)类,代表用户操作的动作。在这个例子中,我们只有一个刷新数据的意图。


object RefreshIntent : WeatherIntent

接下来,我们实现模型(Model)部分,包括状态管理和数据流的处理。


class WeatherViewModel : ViewModel() {// 状态管理private val _weatherState = MutableLiveData<WeatherState>()val weatherState: LiveData<WeatherState> = _weatherState// 处理意图fun processIntent(intent: WeatherIntent) {when (intent) {RefreshIntent -> fetchWeatherData()}}// 获取天气数据private fun fetchWeatherData() {// 发起网络请求或其他数据获取逻辑// 更新状态val weatherData = // 获取的天气数据val newState = WeatherState(temperature = weatherData.temperature,humidity = weatherData.humidity,condition = weatherData.condition)_weatherState.value = newState}
}

全文对Android中MVI的架构讲解,其中包括原理、项目演示以及实战演练。有关更多的Android架构学习进阶可以参考《Android核心技术手册》文档,点击可以查看详细的内容板块。

总结

MVI架构通过响应式数据流和单向数据流的特性,提供了一种可维护、可测试且具备响应式特性的架构模式。尽管学习曲线较陡,但在大型复杂应用开发中,MVI架构能够更好地管理状态和响应用户操作。通过合理设计状态模型和注意副作用管理,我们可以充分发挥MVI架构的优势,提升应用的可维护性和用户体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/235701.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Course1-Week3-分类问题

Course1-Week3-分类问题 文章目录 Course1-Week3-分类问题1. 逻辑回归1.1 线性回归不适用于分类问题1.2 逻辑回归模型1.3 决策边界 2. 逻辑回归的代价函数3. 实现梯度下降4. 过拟合与正则化4.1 线性回归和逻辑回归中的过拟合4.2 解决过拟合的三种方法4.3 正则化4.4 用于线性回归…

【JAVA面向对象编程】--- 探索子类如何继承父类

&#x1f308;个人主页: Aileen_0v0&#x1f525;学习专栏: Java学习系列专栏 &#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 目录 继承 继承的普通成员方法调用 及 普通成员变量修改 构造方法的调用 子类构造方法 继承 package Inherit;class Animal …

前端:实现div的隐藏与显示

效果 完整代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-widt…

WSL2 docker GUI 界面

在 WSL2 docker 中运行GUI界面。 具体流程和远程显示Ubuntu界面类似&#xff0c;链接, 更简单一点&#xff0c; 少了 ssh 的部分。 安装好wsl2 和 docker wsl2 运行GUI程序&#xff0c;windows 会默认弹出窗口。 可以安装 gedit 测试一下 windows 下载并运行 Xlaunch. 运行 d…

Word 小知识之 docx 和 doc 的区别

下面我们从4个方面为大家总结了有关于docx和doc的区别&#xff0c;一起来看一看&#xff1a; 1. 文件格式 doc和docx的区别中较大的区别就是文件格式不同&#xff0c;一个是二进制一个为XML格式。doc&#xff1a;是早期的Word文档格式&#xff0c;采用二进制文件格式。这种…

最新Midjourney绘画提示词Prompt

最新Midjourney绘画提示词Prompt 一、AI绘画工具 SparkAi【无需魔法使用】&#xff1a; SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧&#xff01;本系统使用NestjsVueTypescript框架技术&am…

建堆的时间复杂度和堆排序

文章目录 建堆的时间复杂度向下调整建堆向上调整建堆 堆排序实现 建堆的时间复杂度 下面都以建大堆演示 向下调整建堆 void Adjustdown(HPDataType* a, int size,int parent) {int child parent * 2 1;while (child < size){if (child1<size&&a[child 1] &…

树莓派 SPI操作

一、 开启树莓派spi功能 1sudo raspi-config 选择 Advanced Options -> SPI -> yes 启动SPI内核驱动 运行lsmod命令&#xff0c;可以看到spi模块已启动 在/dev路径下面&#xff0c;我们可以发现两个spi设备 spi管脚如下如所有&#xff0c;左边方框的管脚分别为MOS…

旋转框(obb)目标检测计算iou的方法

首先先定义一组多边形&#xff0c;这里的数据来自前后帧的检测结果 pre [[[860.0, 374.0], [823.38, 435.23], [716.38, 371.23], [753.0, 310.0]],[[829.0, 465.0], [826.22, 544.01], [684.0, 539.0], [686.78, 459.99]],[[885.72, 574.95], [891.0, 648.0], [725.0, 660.0]…

RocketMQ-RocketMQ快速实战及集群原理

一、MQ简介 ​ MQ&#xff1a;MessageQueue&#xff0c;消息队列。是在互联网中使用非常广泛的一系列服务中间件。 这个词可以分两个部分来看&#xff0c;一是Message&#xff1a;消息。消息是在不同进程之间传递的数据。这些进程可以部署在同一台机器上&#xff0c;也可以分布…

python动态加载内容抓取问题的解决实例

问题背景 在网页抓取过程中&#xff0c;动态加载的内容通常无法通过传统的爬虫工具直接获取&#xff0c;这给爬虫程序的编写带来了一定的技术挑战。腾讯新闻&#xff08;https://news.qq.com/&#xff09;作为一个典型的动态网页&#xff0c;展现了这一挑战。 问题分析 动态…

LeetCode刷题---打家劫舍问题

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、打家劫舍 题目链接&#xff1a;打家劫舍 题目描述 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定…