前言
有一段时间没有去写过结构了,最近新的结构MVI,其实出来有一段时间了,只不过大部分项目还没有切换曩昔,关于公司的老项目来说,之前的MVC、MVP也能用,没有替换的必要,而关于新建的项目来说仍是能够替换成功MVVM、MVI等结构的。本文完成后的效果图:

正文
每逢一个新的结构出来,都会解决掉上一个结构所存在的问题,但同时也会产生新的问题,瑕不掩瑜,能够在实践开发中,解决掉产生的问题,就能够更好的运用结构,那么MVI解决了MVVM的什么问题呢?
MVI同样是依据调查者形式,只不过数据通信方面是单向的,解决了MVVM双向通信所带来的问题,实践上MVVM也能做成单向通讯,可是这样就不是纯粹的MVVM,当然了,仁者见仁,智者见智。MVI结构适用于UI变化许多的项目,经过数据去驱动UI,MVI便是Model、View、Intent。
- Model 这儿的Model有所不同,里边还包括UI的状况。
- View 仍是视图,例如Activity、Fragment等。
- Intent 目的,这个和Activity的目的要区分开,我觉得说成是行为可能更妥当,表明去做什么。
多说无益,咱们仍是进入实操环节吧。
一、创立项目
首要创立一个名为MviDemo的项目

项目创立好了,下面咱们需求先进行项目的根本装备。
① 装备AndroidManifest.xml
文章中会经过一个网络API接口,拿到数据来进行MVI结构的建立与运用,接口地址如下:
http://service.picasso.adesk.com/v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot
经过浏览器翻开能够得到许多数据,如图所示:

这些数据都是JSON格局的,后边咱们还会用到这些数据。由于接口运用的是http,而不是https,所以在xml文件夹下新建一个network_security_config.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
然后在AndroidManifest.xml中的application标签中装备它,如图所示:

从Android 9.0起,默许运用https进行网络拜访,假如要进行http拜访则需求增加这个装备。还需求增加一个网络拜访静态权限:
<uses-permission android:name="android.permission.INTERNET"/>
增加方位如下图所示:

项目正常建立还需求一些依靠库和其他的一些设置,下面咱们装备app模块下的build.gradle。
② 装备app的build.gradle
请注意,这儿是装备app的build.gradle,而不是项目的build.gradle,许多人会装备过错,所以我再次着重一下,将你的项目切换到Android形式,如下图所示:

这儿我标注了一下,你看到有两个build.gradle文件,两个文件的后边有灰色的文字阐明,就很清楚的知道这两个build.gradle分别是项目和模块的。下面翻开app模块下的build.gradle,在里边找到dependencies{}
闭包,闭包中增加如下依靠:
// lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
//glide
implementation 'com.github.bumptech.glide:glide:4.14.2'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//retrofit moshi
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"
//moshi used KotlinJsonAdapterFactory
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
增加方位如下图所示:

然后再翻开viewBinding,在android{}闭包下增加如下代码:
buildFeatures {
viewBinding true
}
增加方位如下图所示:

增加之后你会看到右上角有一个Sync Now,点击它进行依靠的载入装备,装备好之后进入下一步,为了保证你的项目没有问题,你能够现在运行一下看看。
二、网络恳求
当咱们运用Kotlin时,网络拜访就变得更简略了,只需求Retrofit和协程即可,首要咱们在com.llw.mvidemo
包下新建一个data
包,然后在data
包下新建一个model
包,model
包下咱们能够经过方才运用网页拜访API拿到的JSON数据来生成一个数据类。
① 生成数据类
生成数据类,这儿咱们能够运用一个插件,搜索JSON To Kotlin Class,如下图所示:

下载安装之后,假如需求重启,你就重启AS,重启之后,右键点击model → New → Kotlin data class File from JSON,如图所示:

在出现的弹窗中仿制经过网页恳求得到的JSON数据字符串,如图所示:

这儿假如觉得看起来不舒服,点击 Format 进行JSON数据格局化,然后咱们需求设置数据类的名称,这儿输入Wallpaper,由于咱们需求运用Moshi,将JSON数据直接转成数据类,所以这儿咱们点击Advanced,如图所示:

这儿默许是None,选择MoShi(Reflect),其他的不用更改,点击OK,此弹窗封闭,回到之前的弹窗,然后点击 Generate 生成数据类,你会发现有三个数据类,分别是Wallpaper、Res和Vertical,咱们看一下Wallpaper的代码:
package com.llw.mvidemo.data.model
import com.squareup.moshi.Json
data class Wallpaper(
@Json(name = "code")
val code: Int,
@Json(name = "msg")
val msg: String,
@Json(name = "res")
val res: Res
)
这儿每一个字段上都有一个@Json
注解,这儿是MoShi依靠库的注解,主要检查一下导包的问题,这儿还有一个小故事,Google 的Gson库,算是推出比较早的,从事Gson库的开发人员,后边离职去了Square,也便是OkHttp、Retrofit的开发者。Retrofit一开始是支撑Gson转化的,后边增加了MoShi的转化,Moshi具有出色的Kotlin支撑以及编译时代码生成功能,能够使应用程序更快更小。这个故事我也是传闻的,你能够自己去求证,下面持续。
② 接口类
现在数据类有了,那么咱们就需求依据这个数据类来写一个接口类,在com.llw.mvidemo
包下新建一个network
包,network
包下创立一个接口类ApiService
,代码如下所示:
interface ApiService {
/**
* 获取壁纸
*/
@GET("v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot")
suspend fun getWallPaper(): Wallpaper
}
这儿归于Retrofit的运用方式,增加了协程的运用而已,就替代了RxJava的线程调度。
③ 网络恳求东西类
现在有接口,下面咱们来做网络恳求,在network
包下新建一个NetworkUtils
类,代码如下:
package com.llw.mvidemo.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
/**
* 网络东西类
*/
object NetworkUtils {
private const val BASE_URL = "http://service.picasso.adesk.com/"
/**
* 经过Moshi 将JSON转为为 Kotlin 的Data class
*/
private val moshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* 构建Retrofit
*/
private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
/**
* 创立Api网络恳求服务
*/
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
由于担心你看的时分导错包,现在贴代码我会将导包的信息也贴出来,这样你总不会再导错包了吧。下面简略阐明一下这个类,首要我定义了一个常量BASE_URL。作为网络接口恳求的地址头,然后构建了MoShi,经过MoShi去进行JSON转Kotlin数据类的处理,之后便是构建Retrofit,将MoShi设置进去,最后便是经过Retrofit创立一个网络恳求服务。
三、目的与状况
之前咱们说MVI的I 是Intent,表明目的或行为,和ViewModel一样,咱们在运用Intent的时分,也是一个Intent对应一个Activity/Fragment。
① 创立目的
在data
包下创立一个intent
包,intent
包下新建一个MainIntent
类,代码如下所示:
package com.llw.mvidemo.data.intent
/**
* 页面目的
*/
sealed class MainIntent {
/**
* 获取壁纸
*/
object GetWallpaper : MainIntent()
}
这儿只要一个GetWallpaper,表明获取壁纸的动作,你还能够增加其他的,例如保存图片、下载图片等,现在目的有了,下面来创立状况,一个目的有用多个状况。
② 创立状况
在data
包下创立一个state
包,state
包下新建一个MainState
类,代码如下:
package com.llw.mvidemo.data.state
import com.llw.mvidemo.data.model.Wallpaper
/**
* 页面状况
*/
sealed class MainState {
/**
* 空闲
*/
object Idle : MainState()
/**
* 加载
*/
object Loading : MainState()
/**
* 获取壁纸
*/
data class Wallpapers(val wallpaper: Wallpaper) : MainState()
/**
* 过错信息
*/
data class Error(val error: String) : MainState()
}
这儿能够看到四个状况,获取壁纸归于其中的一个状况,经过状况能够去更改页面中的UI,后边咱们会看到这一点,这儿的状况你还能够再进行细分,例如每一个网络恳求你能够增加一个恳求中、恳求成功、恳求失利。
四、ViewModel
在MVI形式中,ViewModel的重要性又提高了,不过咱们同样要增加Repository,作为数据存储库。
① 创立存储库
在data
包下创立一个repository
包,repository
包下新建一个MainRepository
类,代码如下:
package com.llw.mvidemo.data.repository
import com.llw.mvidemo.network.ApiService
/**
* 数据存储库
*/
class MainRepository(private val apiService: ApiService) {
/**
* 获取壁纸
*/
suspend fun getWallPaper() = apiService.getWallPaper()
}
这儿的代码就没什么好说的,下面咱们写ViewModel,和MVVM形式中没什么两样的。
② 创立ViewModel
下面在com.llw.mvidemo
包下新建一个ui
包,ui
包下新建一个adapter
包,adapter
包下新建一个MainViewModel
类,代码如下:
package com.llw.mvidemo.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.llw.mvidemo.data.repository.MainRepository
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
/**
* @link MainActivity
*/
class MainViewModel(private val repository: MainRepository) : ViewModel() {
//创立目的管道,容量无限大
val mainIntentChannel = Channel<MainIntent>(Channel.UNLIMITED)
//可变状况数据流
private val _state = MutableStateFlow<MainState>(MainState.Idle)
//可调查状况数据流
val state: StateFlow<MainState> get() = _state
init {
viewModelScope.launch {
//搜集目的
mainIntentChannel.consumeAsFlow().collect {
when (it) {
//发现目的为获取壁纸
is MainIntent.GetWallpaper -> getWallpaper()
}
}
}
}
/**
* 获取壁纸
*/
private fun getWallpaper() {
viewModelScope.launch {
//修正状况为加载中
_state.value = MainState.Loading
//网络恳求状况
_state.value = try {
//恳求成功
MainState.Wallpapers(repository.getWallPaper())
} catch (e: Exception) {
//恳求失利
MainState.Error(e.localizedMessage ?: "UnKnown Error")
}
}
}
}
这儿首要创立一个目的管道,然后是一个可变的状况数据流和一个不可变调查状况数据流,调查者形式。在初始化的时分就进行目的的搜集,你能够理解为监听,当搜集到目标目的MainIntent.GetWallpaper
时就进行相应的目的处理,调用getWallpaper()
函数,这儿边修正可变的状况_state
,而当_state
发生变化,state
就调查到了,就会进行相应的动作,这个经过是在View中进行,也便是Activity/Fragment中进行。这儿对_state
首要赋值为Loading
,表明加载中,然后进行一个网络恳求,成果便是成功或者失利,假如成功,则赋值Wallpapers
,View中搜集到这个状况后就能够进行页面数据的烘托了,恳求失利,也要更改状况。
③ 创立ViewModel工厂
在viewmodel包下新建一个ViewModelFactory类,代码如下:
package com.llw.mvidemo.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.llw.mvidemo.network.ApiService
import com.llw.mvidemo.data.repository.MainRepository
/**
* ViewModel工厂
*/
class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// 判断 MainViewModel 是不是 modelClass 的父类或接口
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(apiService)) as T
}
throw IllegalArgumentException("UnKnown class")
}
}
五、UI
前面咱们写好根本的结构内容,下面来进行运用,简略来说,恳求数据然后烘托出来,由于这儿恳求的是壁纸数据,所以我需求写一个适配器。
① 列表适配器
在创立适配器之前首要咱们需求创立一个适配器所对应的item布局,在layout下新建一个item_wallpaper_rv.xml
,代码如下图所示:
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.imageview.ShapeableImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/iv_wall_paper"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_margin="4dp"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/roundedImageStyle" />
这儿运用了ShapeableImageView,这个控件的优势就在于能够自己设置圆角,在themes.xml中增加如下代码:
<!-- 圆角图片 -->
<style name="roundedImageStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>
增加方位如下图所示:

下面进行咱们在ui包下新建一个adapter
包,adapter
包下新建一个WallpaperAdapter
类,里边的代码如下所示:
package com.llw.mvidemo.ui.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.llw.mvidemo.data.model.Vertical
import com.llw.mvidemo.databinding.ItemWallpaperRvBinding
/**
* 壁纸适配器
*/
class WallpaperAdapter(private val verticals: ArrayList<Vertical>) :
RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() {
fun addData(data: List<Vertical>) {
verticals.addAll(data)
}
class ViewHolder(itemWallPaperRvBinding: ItemWallpaperRvBinding) :
RecyclerView.ViewHolder(itemWallPaperRvBinding.root) {
var binding: ItemWallpaperRvBinding
init {
binding = itemWallPaperRvBinding
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ViewHolder(ItemWallpaperRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun getItemCount() = verticals.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
//加载图片
verticals[position].priview.let {
Glide.with(holder.itemView.context).load(it).into(holder.binding.ivWallPaper)
}
}
}
这儿的代码相对比较简略,就不做阐明晰,归于适配器的根本操作了。
② 数据烘托
适配器写好之后,咱们需求修正一下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.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_wallpaper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:visibility="gone" />
<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_get_wallpaper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取壁纸"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
下面咱们进入MainActivity
,修正里边的代码如下所示:
package com.llw.mvidemo.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.llw.mvidemo.network.NetworkUtils
import com.llw.mvidemo.databinding.ActivityMainBinding
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import com.llw.mvidemo.ui.adapter.WallpaperAdapter
import com.llw.mvidemo.ui.viewmodel.MainViewModel
import com.llw.mvidemo.ui.viewmodel.ViewModelFactory
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
private var wallPaperAdapter = WallpaperAdapter(arrayListOf())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//运用ViewBinding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//绑定ViewModel
mainViewModel = ViewModelProvider(this, ViewModelFactory(NetworkUtils.apiService))[MainViewModel::class.java]
//初始化
initView()
//调查ViewModel
observeViewModel()
}
/**
* 调查ViewModel
*/
private fun observeViewModel() {
lifecycleScope.launch {
//状况搜集
mainViewModel.state.collect {
when(it) {
is MainState.Idle -> {
}
is MainState.Loading -> {
binding.btnGetWallpaper.visibility = View.GONE
binding.pbLoading.visibility = View.VISIBLE
}
is MainState.Wallpapers -> { //数据回来
binding.btnGetWallpaper.visibility = View.GONE
binding.pbLoading.visibility = View.GONE
binding.rvWallpaper.visibility = View.VISIBLE
it.wallpaper.let { paper ->
wallPaperAdapter.addData(paper.res.vertical)
}
wallPaperAdapter.notifyDataSetChanged()
}
is MainState.Error -> {
binding.pbLoading.visibility = View.GONE
binding.btnGetWallpaper.visibility = View.VISIBLE
Log.d("TAG", "observeViewModel: $it.error")
Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}
/**
* 初始化
*/
private fun initView() {
//RV装备
binding.rvWallpaper.apply {
layoutManager = GridLayoutManager(this@MainActivity, 2)
adapter = wallPaperAdapter
}
//按钮点击
binding.btnGetWallpaper.setOnClickListener {
lifecycleScope.launch{
//发送目的
mainViewModel.mainIntentChannel.send(MainIntent.GetWallpaper)
}
}
}
}
阐明一下,首要声明变量并在onCreate()
中进行初始化,这儿绑定ViewModel
采用的是ViewModelProvider()
,而不是ViewModelProviders.of
,这是由于这个API已经被移除了,在之前的版别中是过时弃用,在最新的版别中你都找不到这个API了,所以运用ViewModelProvider()
,然后经过ViewModelFactory
去创立对应的MainViewModel
。
initView()
函数中是控件的一些装备,比如给RecyclerView增加布局管理器和设置适配器,给按钮增加点击事情,在点击的时分发送目的,发送的目的被MainViewModel中mainIntentChannel
搜集到,然后履行网络恳求操作,此刻目的的状况为Loading
。
observeViewModel()
函数中是对状况的搜集,在状况为Loading
,躲藏按钮,显现加载条,然后网络恳求会有成果,假如是成功,则在UI上躲藏按钮和加载条,显现列表控件,并增加数据到适配器中,然后改写适配器,数据就会烘托出来;假如是失利则显现按钮,躲藏加载条,打印过错信息并提示一下。这样就完成了经过状况更新UI的环节,MVI的结构便是这样规划的。
页面UI(点击事情发送目的) → ViewModel搜集目的(确认内容) →
ViewModel更新状况(修正_state) → 页面调查ViewModel状况(搜集state,履行相关的UI)
这是一个环,从UI页面出发,最终回到UI页面中进行数据烘托,咱们看看效果。

六、源码
欢迎Star 或 Fork,天长地久,后会有期~
源码地址:MviDemo