前言
本篇文章的閱讀對象是為了感覺好像了解MVI但是又不知道這玩意到底是個啥的讀者
想理解MVI 需要提前理解幾個東西
1.為什么推薦使用MVI,android 的MVI是基于什么提出的
2.android 的MVI是基于什么實現的,為什么要用這些
以上三點我先用最簡短的語言以自己的理解先做一個解答
1,為什么推薦使用MVI,MVI是基于什么提出的
答:主要為了ViewModel層和View層的交互由雙向轉化為單向,并且規范交互數據傳輸
android端由mvc到mvp再到mvvm最后到mvi,每一次的變化都讓代碼分層更加清晰,目前MVVM的缺點是ViewModel和view的交互還是屬于雙向交互,viewModel和Model的處理界限也比較模糊,所以提出MVI,MVI其實是基于MVVM, 在View和ViewModel中增加了Intent來作為中間傳輸,通過響應編程更新UI實現的。這樣不僅規范View與ViewModel交互,且將交互順序由View—>ViewModel->View 的雙向交互變為View->Intent->ViewModel->State->View的環形交互,通過Intent和State來解決ViewModel與Model的界限模糊問題。
也就是說ViewModel現在可以不關心如何被view觸發,如何刷新UI,也不關心當前有多少數據模型,只用來維護Intent和state管理(再直白些就是intent就是view調用viewModel的中間層,state就是viewModel回調view的中間層,model通過intent和state去管理,看起來會更加簡潔)
2,android 的MVI是基于什么實現的
目前android主流的MVI是基于協程+flow+viewModel去實現的
kotlin協程就不說了,省去接口回調,控制代碼執行順序,線程切換kotlin的協程功不可沒
flow:中文翻譯成流和Stream容易混淆,flow是響應式流,會有配備一個生產者和一個消費者(android可以理解成類似handler里的message,處理方式相似但是原理不同)
viewModel:jetpack家族,本來也可以自己寫,但是jetpack提供了可以管理生命周期的viewModel不比自己寫香么?
下面兩個文章看看更加有助理解mvi
kotlin 響應式編程flow
https://juejin.cn/post/7034379406730592269
這篇文字幾乎和官方文檔寫的詳細程度差不多,但是解釋會更加友好
MVVM使用
http://www.lxweimin.com/p/f9d0688b241e
不喜歡看思路的可以通過這篇文章感受mvvm代碼的層次結構
正片
這篇文章看完了能學會啥?
1.flow在UI中簡單用法
2.Intent是個啥
3.state是個啥
4.原來MVI這么簡單
1:flow在UI中簡單用法
為啥我看MVI要先看flow?
因為沒有flow就沒有MVI的I的靈魂(如果你用rxjava或者自己創建監聽者當我沒說)
首先如果不知道flow怎么用的同學,我得說說你了,kotlin好好學學,mvvm都用kotlin寫了,mvi還想著java是不是太過分了!(只針對android)
首先掏出官方例子
//所有的collect方法都是suspend修飾的,所以扔了協程里
runBlocking {
//創建一個流
flow {
//用循環定義一個生產者
for (i in 1..10) {
//生產者發10個數
emit(i)
}
}.collect {//注冊這個流消費者
//消費者打印
println(it)
}
}
這個流很簡單就是創建一個流,然后消費打印,用這段代碼中兩個方法比較重要,emit和collect,源碼就不分析了就是emit是生產者發送數據,collect是消費者接受數據
然后我們把這個例子稍微復雜化一點放到例子里
ViewModel代碼
class EnglishVM : ViewModel() {
var flow=flow<Int> {
for (i in 1..10) {
emit(i)
}
}
}
這是activity代碼
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI學習")
runBlocking {
viewMode.flow.collect {
//將數字打印到textview上
tvClass addText "$it"
}
}
}
//做了個直接打印到textview的快捷方法,可以忽略
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
來看執行結果
現在通過flow將文字展示到了UI上,但是有個問題,我們的業務場景一般是觸發某個事件以后才會刷新UI,而且刷新UI我們只有一個或幾個結果,不是一連串的數字,所以我們在這個基礎上再次升級
首先flow這個方法已經不是那么好用了,我們引入一個新的概念StateFlow(我可以點)
StateFlow由兩個API構成MutableStateFlow和StateFlow,主要用來通過狀態類的變化來發送狀態變化流。原理大體就是通過get,set去監聽狀態state變化,然后發送流,這里就不展開了,可以看各個不同版本的源碼
然后將viewModel中的flow改為StateFlow并加入兩個刷新UI的方法
class EnglishVM : BaseViewModel() {
//MutableStateFlow需要默認傳入一個狀態,我們隨便傳個1代表默認狀態
val state = MutableStateFlow<Int>(1)
//將狀態改為2代表正在加載
fun doLoading(){
state.value = 2
}
//將狀態改為3代表加載完畢
fun finishLoading(){
state.value = 3
}
}
然后給activity增加兩個按鈕,添加點擊事件,分別調用doLoading和finishLoading
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI學習")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被點擊"
viewMode.finishLoading()
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被點擊"
viewMode.doLoading()
}
GlobalScope.launch {
viewMode.state.collect {
tvClass addText "$it"
}
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
運行并分別點擊LOADING和FINISH
好的一個簡單的通過flow更新UI的效果已經完畢了,下面開始實現MVI
2:Intent是個啥
我可以很負責的告訴你,Intent就是個枚舉,而且是個特殊的枚舉,在kotlin中可以通過sealed關鍵字來生成封閉類,這個關鍵字生成的封閉類在when語句中可以不用謝else,而且由于是封閉類,所以可以通過數據對象來實現各種騷操作
比如下面的代碼
//寫個英語的意圖
sealed class EngLishIntent {
//用數據類表示加載英語方法
data class doLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對象表示完成加載方法
object finishLoading:EngLishIntent()
}
但是怎么用這個Intent呢?又涉及到一個kotlin的概念Channel(我可以點)
channel本來是用來做協程之間通訊的,而我們的view層的觸發操作和viewModel層獲取數據這個流程恰巧應該是需要完全分離的,并且channel具備flow的特性,所以用channel來做view和viewModel的通訊非常適合
我們通過再把上面的例子,通過Intent來處理下
意圖代碼如下
sealed class EngLishIntent {
//用數據類表示加載英語方法
data class DoLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對象表示完成加載方法
object FinishLoading:EngLishIntent()
}
viewModel將Intent引入
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
val state = MutableStateFlow<Int>(1)
//初始化的時候將channel的消費者綁定
init {
handleIntent();
}
//注冊消費者
private fun handleIntent() {
viewModelScope.launch {
//將Channel轉化為flow,并且注冊消費者
englishIntent.consumeAsFlow().collect {
//這里的it和Channel<EngLishIntent>泛型保持一致,所以it是封閉類(特殊枚舉類)
when(it){
//判斷是FinishLoading 將state.value=3
is EngLishIntent.FinishLoading->{state.value=3}
//判斷是DoLoadingEnglish 將state.value=1
is EngLishIntent.DoLoadingEnglish->{
//此處可以通過 it. 拿到DoLoadingEnglish的入參 后面會演示
state.value=2}
}
}
}
}
然后再把Activity改改
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI學習")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被點擊"
//協程方法統一提取,方便日后修改
doLaunch{
tvClass addText "send(EngLishIntent.FinishLoading)"
//拿到viewMode的englishIntent去傳遞意圖
viewMode.englishIntent.send(EngLishIntent.FinishLoading)
}
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被點擊"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewMode.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
GlobalScope.launch {
viewMode.state.collect {
tvClass addText "$it"
}
}
}
fun doLaunch(block: suspend CoroutineScope.() -> Unit){
GlobalScope.launch {
block.invoke(this)
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
然后看下點擊兩個按鈕后的運行結果
結果和上次的結果沒什么太大的區別,而且感覺代碼還變復雜了,為什么要這么做?
注意看下面兩個圖
之前是直接使用viewModel提供的方法的,現在變成了傳輸intent里的枚舉,徹底將View和ViewModel解耦了,現在唯一耦合的就是viewModel持有的Intent了,實現了業務解耦,很棒棒
既然知道了通過intent能實現view發起事件對viewModel的解耦,那能不能實現ViewModel刷新view的解耦呢?
其實上面的代碼我們已經通過flow實現了一大半了,現在把int類型轉換成一個枚舉讓代碼更加嚴謹就能完全解耦了,此時就能引入MVI的最后一個概念state了
3:state是個啥
state是個和Intent一樣的枚舉,但是不同的是intent是個事件流,state是個狀態流
首先我們先定義一個和Intent差不多的封裝類state
sealed class EnglishState {
object BeforeLoading:EnglishState()
object Loading:EnglishState()
object FinishLoading:EnglishState()
}
然后我們把之前的MutableStateFlow封裝起來,不給view層修改權限,已保證我們業務邏輯不會寫在UI層,并且把1、2、3等狀態改為剛剛創建的EnglishState
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
val state: StateFlow<EnglishState>
get() = _state
init {
handleIntent();
}
private fun handleIntent() {
viewModelScope.launch {
englishIntent.consumeAsFlow().collect {
when(it){
is EngLishIntent.FinishLoading->{
_state.value=EnglishState.FinishLoading
}
is EngLishIntent.DoLoadingEnglish->{
//此處可以通過 it. 拿到DoLoadingEnglish的入參 后面會演示
_state.value=EnglishState.Loading
}
}
}
}
}
}
然后把Activity的打印UI更新部分通過state做不同的邏輯處理
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI學習")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被點擊"
doLaunch{
tvClass addText "send(EngLishIntent.FinishLoading)"
viewModel.englishIntent.send(EngLishIntent.FinishLoading)
}
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被點擊"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
lifecycleScope.launch {
viewModel.state.collect {
when(it){
is EnglishState.BeforeLoading->{
tvClass addText "初始化頁面"
}
is EnglishState.Loading ->{
tvClass addText "加載中..."
}
is EnglishState.FinishLoading ->{
tvClass addText "加載完畢..."
}
}
}
}
}
fun doLaunch(block: suspend CoroutineScope.() -> Unit){
GlobalScope.launch {
block.invoke(this)
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
分別點擊按鈕結果如下
到這里,一個基本的MVI就已經成型了,我們結合實際請求,稍稍做些許改動
4.原來MVI這么簡單
我們先將ViewModel賦予真正的請求能力,提供一個基類(可以通過各種方法來)
open class BaseViewModel : ViewModel() {
var getClient: () -> Urls = {
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) //設置超時時間
.retryOnConnectionFailure(true)
val logInterceptor = HttpLoggingInterceptor()
// if (BuildConfig.DEBUG) {
// //顯示日志
// logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
// } else {
// logInterceptor.setLevel(HttpLoggingInterceptor.Level.NONE)
// }
client.addInterceptor(GsonInterceptor())
Retrofit.Builder()
.client(client.build())
.baseUrl("https://route.showapi.com/")
.addConverterFactory(ViewModelGsonConverterFactory())
.build().create(Urls::class.java)
}
//向協程提供一個全局異常,用來處理異常UI
fun <T> errorContext(err: (errorMessage:Throwable) -> Unit):CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, e ->
err.invoke(e)
}
}
}
intent 修改修改,加一個請求類型
sealed class EngLishIntent {
//獲取英語句子數據
data class DoLoadingEnglish(val num:Int):EngLishIntent()
//獲取新聞數據
object DoLoadingNews:EngLishIntent()
}
State也改改,新增幾個數據狀態
sealed class EnglishState {
object BeforeLoading:EnglishState()
object Loading:EnglishState()
object FinishLoading:EnglishState()
data class EnglishData(val list:List<EnglishKey>):EnglishState()
data class NewsData(val list:List<NewsListKey>):EnglishState()
data class ErrorData(val error:String):EnglishState();
}
viewmodel改改,帶有真正的網絡請求
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
val state: StateFlow<EnglishState>
get() = _state
init {
handleIntent();
}
private fun handleIntent() {
viewModelScope.launch {
englishIntent.consumeAsFlow().collect {
//這兩種寫法太冗余了
// is EngLishIntent.DoLoadingEnglish -> loadingEnglish()
// is EngLishIntent.DoLoadingNews -> loadingEnglish()
commentLoading(it)
}
}
}
suspend fun intentToState(intent:EngLishIntent):EnglishState{
when (intent) {
//加載英語句子
is EngLishIntent.DoLoadingEnglish ->
return EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
//加載新聞句子
is EngLishIntent.DoLoadingNews ->
return EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
}
}
////加載英語句子
// private fun loadingEnglish() {
// viewModelScope.launch(context = (errorContext {
// _state.value = EnglishState.FinishLoading
// _state.value = EnglishState.ErrorData(it.message?:"請求異常")
// } + Dispatchers.Main)) {
// _state.value = EnglishState.Loading
// _state.value = EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
// _state.value = EnglishState.FinishLoading
// }
// }
//加載新聞
// private fun loadingNews() {
// viewModelScope.launch(context = (errorContext {
// _state.value = EnglishState.FinishLoading
// _state.value = EnglishState.ErrorData(it.message?:"請求異常")
// } + Dispatchers.Main)) {
// _state.value = EnglishState.Loading
// _state.value = EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
// _state.value = EnglishState.FinishLoading
// }
// }
private fun commentLoading(intent:EngLishIntent) {
viewModelScope.launch(context = (errorContext {
_state.value = EnglishState.FinishLoading
_state.value = EnglishState.ErrorData(it.message?:"請求異常")
} + Dispatchers.Main)) {
_state.value = EnglishState.Loading
_state.value = intentToState(intent)
_state.value = EnglishState.FinishLoading
}
}
}
最后把activity的按鈕改改,UI刷新邏輯改改變成這樣
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI學習")
btnLoadingNews.setOnClickListener {
tvClass addText "btnLoadingNews 被點擊"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingNews)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingNews)
}
}
btnLoadingEnglish.setOnClickListener {
tvClass addText "btnLoadingEnglish 被點擊"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
//這里注意改成有生命周期的lifecycleScope 否則網絡請求回來這里管道就銷毀了
lifecycleScope.launch {
viewModel.state.collect {
when(it){
is EnglishState.BeforeLoading->{
tvClass addText "初始化頁面"
}
is EnglishState.Loading ->{
tvClass addText "加載中..."
}
is EnglishState.FinishLoading ->{
tvClass addText "加載完畢..."
}
is EnglishState.EnglishData->{
for (key in it.list){
tvClass addText key.english addText key.chinese
}
}
is EnglishState.NewsData->{
for (key in it.list){
tvClass addText "標題:${key.title}" addText "摘要:${key.summary}" addText "省份:${key.provinceName} 時間:${key.updateTime}"
}
}
}
}
}
}
fun doLaunch(block: suspend CoroutineScope.() -> Unit){
GlobalScope.launch {
block.invoke(this)
}
}
infix fun TextView.addText(text: String) :TextView{
this.text = "${this.text?.toString()}$text\n";
return this
}
}
最后附上接口
interface Urls {
@GET("/1211-1")
suspend fun getEnglishWordsByLaunch(
@Query("count") count: Int?,
@Query("showapi_appid") id: String = "測試id",
@Query("showapi_sign") showapi_sign: String = "showapi_sign",
): ArrayList<EnglishKey>
@GET("/2217-4")
suspend fun getNewsListKeyByLaunch(
@Query("showapi_appid") id: String = "測試id",
@Query("showapi_sign") showapi_sign: String = "showapi_sign",
): ArrayList<NewsListKey>
點擊兩次按鈕后結果入下
一個簡單的MVI網絡請求架構到此結束
結尾
MVI其實主要思想是通過Intent將view和業務實現層分離,達到通過意圖傳遞邏輯方法。所以不一定非要基于MVVM,也適用于MVP,這次分享就到此結束了
最后感謝
https://blog.csdn.net/vitaviva/article/details/109406873
這篇文章提供的清晰簡單的思路,代碼思路均由這篇文章獲取