android MVI到底是什么

前言

本篇文章的閱讀對(duì)象是為了感覺好像了解MVI但是又不知道這玩意到底是個(gè)啥的讀者
想理解MVI 需要提前理解幾個(gè)東西
1.為什么推薦使用MVI湘纵,android 的MVI是基于什么提出的
2.android 的MVI是基于什么實(shí)現(xiàn)的,為什么要用這些

以上三點(diǎn)我先用最簡(jiǎn)短的語言以自己的理解先做一個(gè)解答

1,為什么推薦使用MVI甩十,MVI是基于什么提出的

答:主要為了ViewModel層和View層的交互由雙向轉(zhuǎn)化為單向胞四,并且規(guī)范交互數(shù)據(jù)傳輸

android端由mvc到mvp再到mvvm最后到mvi盔腔,每一次的變化都讓代碼分層更加清晰壕曼,目前MVVM的缺點(diǎn)是ViewModel和view的交互還是屬于雙向交互,viewModel和Model的處理界限也比較模糊寸齐,所以提出MVI欲诺,MVI其實(shí)是基于MVVM, 在View和ViewModel中增加了Intent來作為中間傳輸渺鹦,通過響應(yīng)編程更新UI實(shí)現(xiàn)的扰法。這樣不僅規(guī)范View與ViewModel交互,且將交互順序由View—>ViewModel->View 的雙向交互變?yōu)閂iew->Intent->ViewModel->State->View的環(huán)形交互毅厚,通過Intent和State來解決ViewModel與Model的界限模糊問題塞颁。
也就是說ViewModel現(xiàn)在可以不關(guān)心如何被view觸發(fā),如何刷新UI吸耿,也不關(guān)心當(dāng)前有多少數(shù)據(jù)模型祠锣,只用來維護(hù)Intent和state管理(再直白些就是intent就是view調(diào)用viewModel的中間層,state就是viewModel回調(diào)view的中間層咽安,model通過intent和state去管理伴网,看起來會(huì)更加簡(jiǎn)潔)

2,android 的MVI是基于什么實(shí)現(xiàn)的

目前android主流的MVI是基于協(xié)程+flow+viewModel去實(shí)現(xiàn)的
kotlin協(xié)程就不說了妆棒,省去接口回調(diào)澡腾,控制代碼執(zhí)行順序,線程切換kotlin的協(xié)程功不可沒
flow:中文翻譯成流和Stream容易混淆糕珊,flow是響應(yīng)式流蛋铆,會(huì)有配備一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者(android可以理解成類似handler里的message,處理方式相似但是原理不同)
viewModel:jetpack家族放接,本來也可以自己寫,但是jetpack提供了可以管理生命周期的viewModel不比自己寫香么留特?

下面兩個(gè)文章看看更加有助理解mvi

kotlin 響應(yīng)式編程flow
https://juejin.cn/post/7034379406730592269
這篇文字幾乎和官方文檔寫的詳細(xì)程度差不多纠脾,但是解釋會(huì)更加友好

MVVM使用
http://www.reibang.com/p/f9d0688b241e
不喜歡看思路的可以通過這篇文章感受mvvm代碼的層次結(jié)構(gòu)

正片

這篇文章看完了能學(xué)會(huì)啥玛瘸?
1.flow在UI中簡(jiǎn)單用法
2.Intent是個(gè)啥
3.state是個(gè)啥
4.原來MVI這么簡(jiǎn)單

1:flow在UI中簡(jiǎn)單用法

為啥我看MVI要先看flow?
因?yàn)闆]有flow就沒有MVI的I的靈魂(如果你用rxjava或者自己創(chuàng)建監(jiān)聽者當(dāng)我沒說)
首先如果不知道flow怎么用的同學(xué)苟蹈,我得說說你了糊渊,kotlin好好學(xué)學(xué),mvvm都用kotlin寫了慧脱,mvi還想著java是不是太過分了!(只針對(duì)android)

首先掏出官方例子

//所有的collect方法都是suspend修飾的渺绒,所以扔了協(xié)程里
runBlocking {
//創(chuàng)建一個(gè)流
     flow {

//用循環(huán)定義一個(gè)生產(chǎn)者
        for (i in 1..10) {
//生產(chǎn)者發(fā)10個(gè)數(shù)
            emit(i)
       }  
    }.collect {//注冊(cè)這個(gè)流消費(fèi)者
//消費(fèi)者打印
           println(it)
   }
}

這個(gè)流很簡(jiǎn)單就是創(chuàng)建一個(gè)流,然后消費(fèi)打印菱鸥,用這段代碼中兩個(gè)方法比較重要宗兼,emit和collect,源碼就不分析了就是emit是生產(chǎn)者發(fā)送數(shù)據(jù)氮采,collect是消費(fèi)者接受數(shù)據(jù)
然后我們把這個(gè)例子稍微復(fù)雜化一點(diǎn)放到例子里
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學(xué)習(xí)")
        runBlocking {
            viewMode.flow.collect {
//將數(shù)字打印到textview上
                tvClass addText "$it"
            }
        }

    }
//做了個(gè)直接打印到textview的快捷方法殷绍,可以忽略
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

來看執(zhí)行結(jié)果

執(zhí)行結(jié)果

現(xiàn)在通過flow將文字展示到了UI上,但是有個(gè)問題鹊漠,我們的業(yè)務(wù)場(chǎng)景一般是觸發(fā)某個(gè)事件以后才會(huì)刷新UI主到,而且刷新UI我們只有一個(gè)或幾個(gè)結(jié)果,不是一連串的數(shù)字躯概,所以我們?cè)谶@個(gè)基礎(chǔ)上再次升級(jí)
首先flow這個(gè)方法已經(jīng)不是那么好用了登钥,我們引入一個(gè)新的概念StateFlow(我可以點(diǎn))
StateFlow由兩個(gè)API構(gòu)成MutableStateFlow和StateFlow,主要用來通過狀態(tài)類的變化來發(fā)送狀態(tài)變化流娶靡。原理大體就是通過get牧牢,set去監(jiān)聽狀態(tài)state變化,然后發(fā)送流固蛾,這里就不展開了结执,可以看各個(gè)不同版本的源碼

然后將viewModel中的flow改為StateFlow并加入兩個(gè)刷新UI的方法

class EnglishVM : BaseViewModel() {
//MutableStateFlow需要默認(rèn)傳入一個(gè)狀態(tài),我們隨便傳個(gè)1代表默認(rèn)狀態(tài)
   val state = MutableStateFlow<Int>(1)
//將狀態(tài)改為2代表正在加載
    fun doLoading(){
        state.value = 2
    }
//將狀態(tài)改為3代表加載完畢
    fun finishLoading(){
        state.value = 3
    }
}

然后給activity增加兩個(gè)按鈕,添加點(diǎn)擊事件艾凯,分別調(diào)用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學(xué)習(xí)")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被點(diǎn)擊"

            viewMode.finishLoading()
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被點(diǎn)擊"
            viewMode.doLoading()
        }

         GlobalScope.launch  {
            viewMode.state.collect {
                tvClass addText "$it"
            }
        }

    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

運(yùn)行并分別點(diǎn)擊LOADING和FINISH


運(yùn)行結(jié)果

好的一個(gè)簡(jiǎn)單的通過flow更新UI的效果已經(jīng)完畢了献幔,下面開始實(shí)現(xiàn)MVI

2:Intent是個(gè)啥

我可以很負(fù)責(zé)的告訴你,Intent就是個(gè)枚舉趾诗,而且是個(gè)特殊的枚舉蜡感,在kotlin中可以通過sealed關(guān)鍵字來生成封閉類,這個(gè)關(guān)鍵字生成的封閉類在when語句中可以不用謝else恃泪,而且由于是封閉類郑兴,所以可以通過數(shù)據(jù)對(duì)象來實(shí)現(xiàn)各種騷操作
比如下面的代碼

//寫個(gè)英語的意圖
sealed class EngLishIntent {
//用數(shù)據(jù)類表示加載英語方法
    data class doLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對(duì)象表示完成加載方法
    object finishLoading:EngLishIntent()
}

但是怎么用這個(gè)Intent呢?又涉及到一個(gè)kotlin的概念Channel(我可以點(diǎn))
channel本來是用來做協(xié)程之間通訊的贝乎,而我們的view層的觸發(fā)操作和viewModel層獲取數(shù)據(jù)這個(gè)流程恰巧應(yīng)該是需要完全分離的情连,并且channel具備flow的特性,所以用channel來做view和viewModel的通訊非常適合
我們通過再把上面的例子览效,通過Intent來處理下

意圖代碼如下

sealed class EngLishIntent {
//用數(shù)據(jù)類表示加載英語方法
    data class DoLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對(duì)象表示完成加載方法
    object FinishLoading:EngLishIntent()
}

viewModel將Intent引入

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
     val state = MutableStateFlow<Int>(1)
//初始化的時(shí)候?qū)hannel的消費(fèi)者綁定
    init {
        handleIntent();
    }
//注冊(cè)消費(fèi)者
    private fun handleIntent() {
        viewModelScope.launch {
//將Channel轉(zhuǎn)化為flow却舀,并且注冊(cè)消費(fèi)者
            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的入?yún)?后面會(huì)演示
                        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學(xué)習(xí)")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被點(diǎn)擊"
//協(xié)程方法統(tǒng)一提取,方便日后修改
            doLaunch{
                tvClass addText "send(EngLishIntent.FinishLoading)"
//拿到viewMode的englishIntent去傳遞意圖
                viewMode.englishIntent.send(EngLishIntent.FinishLoading)
            }
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被點(diǎn)擊"
            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";
    }
}

然后看下點(diǎn)擊兩個(gè)按鈕后的運(yùn)行結(jié)果


運(yùn)行結(jié)果

結(jié)果和上次的結(jié)果沒什么太大的區(qū)別挽拔,而且感覺代碼還變復(fù)雜了辆脸,為什么要這么做?
注意看下面兩個(gè)圖


原始方法

Intent

之前是直接使用viewModel提供的方法的螃诅,現(xiàn)在變成了傳輸intent里的枚舉啡氢,徹底將View和ViewModel解耦了,現(xiàn)在唯一耦合的就是viewModel持有的Intent了术裸,實(shí)現(xiàn)了業(yè)務(wù)解耦倘是,很棒棒

既然知道了通過intent能實(shí)現(xiàn)view發(fā)起事件對(duì)viewModel的解耦,那能不能實(shí)現(xiàn)ViewModel刷新view的解耦呢穗椅?
其實(shí)上面的代碼我們已經(jīng)通過flow實(shí)現(xiàn)了一大半了辨绊,現(xiàn)在把int類型轉(zhuǎn)換成一個(gè)枚舉讓代碼更加嚴(yán)謹(jǐn)就能完全解耦了,此時(shí)就能引入MVI的最后一個(gè)概念state了

3:state是個(gè)啥

state是個(gè)和Intent一樣的枚舉匹表,但是不同的是intent是個(gè)事件流门坷,state是個(gè)狀態(tài)流
首先我們先定義一個(gè)和Intent差不多的封裝類state

sealed class EnglishState {
    object BeforeLoading:EnglishState()
    object Loading:EnglishState()
    object FinishLoading:EnglishState()
}

然后我們把之前的MutableStateFlow封裝起來,不給view層修改權(quán)限袍镀,已保證我們業(yè)務(wù)邏輯不會(huì)寫在UI層默蚌,并且把1、2苇羡、3等狀態(tài)改為剛剛創(chuàng)建的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的入?yún)?后面會(huì)演示
                        _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學(xué)習(xí)")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被點(diǎn)擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.FinishLoading)"
                viewModel.englishIntent.send(EngLishIntent.FinishLoading)
            }
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被點(diǎn)擊"
            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";
    }
}

分別點(diǎn)擊按鈕結(jié)果如下


image.png

到這里绸吸,一個(gè)基本的MVI就已經(jīng)成型了,我們結(jié)合實(shí)際請(qǐng)求设江,稍稍做些許改動(dòng)

4.原來MVI這么簡(jiǎn)單

我們先將ViewModel賦予真正的請(qǐng)求能力锦茁,提供一個(gè)基類(可以通過各種方法來)

open class BaseViewModel : ViewModel() {
    var getClient: () -> Urls = {
        val client = OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS) //設(shè)置超時(shí)時(shí)間
            .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)
    }
//向協(xié)程提供一個(gè)全局異常,用來處理異常UI
    fun <T> errorContext(err: (errorMessage:Throwable) -> Unit):CoroutineExceptionHandler {
       return CoroutineExceptionHandler { _, e ->
           err.invoke(e)
       }
    }
}

intent 修改修改叉存,加一個(gè)請(qǐng)求類型

sealed class EngLishIntent {
    //獲取英語句子數(shù)據(jù)
    data class DoLoadingEnglish(val num:Int):EngLishIntent()
    //獲取新聞數(shù)據(jù)
    object DoLoadingNews:EngLishIntent()
}

State也改改码俩,新增幾個(gè)數(shù)據(jù)狀態(tài)

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改改,帶有真正的網(wǎng)絡(luò)請(qǐng)求

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?:"請(qǐng)求異常")
//        } + 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?:"請(qǐng)求異常")
//        } + 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?:"請(qǐng)求異常")
        } + Dispatchers.Main)) {
            _state.value = EnglishState.Loading
            _state.value = intentToState(intent)
            _state.value = EnglishState.FinishLoading
        }
    }
}

最后把a(bǔ)ctivity的按鈕改改歼捏,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學(xué)習(xí)")
        btnLoadingNews.setOnClickListener {
            tvClass addText "btnLoadingNews 被點(diǎn)擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingNews)"
                viewModel.englishIntent.send(EngLishIntent.DoLoadingNews)
            }
        }
        btnLoadingEnglish.setOnClickListener {
            tvClass addText "btnLoadingEnglish 被點(diǎn)擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"

                viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }
//這里注意改成有生命周期的lifecycleScope 否則網(wǎng)絡(luò)請(qǐng)求回來這里管道就銷毀了
        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 "標(biāo)題:${key.title}" addText "摘要:${key.summary}" addText "省份:${key.provinceName} 時(shí)間:${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 = "測(cè)試id",
        @Query("showapi_sign") showapi_sign: String = "showapi_sign",
    ): ArrayList<EnglishKey>

    @GET("/2217-4")
    suspend fun getNewsListKeyByLaunch(
        @Query("showapi_appid") id: String = "測(cè)試id",
        @Query("showapi_sign") showapi_sign: String = "showapi_sign",
    ): ArrayList<NewsListKey>

點(diǎn)擊兩次按鈕后結(jié)果入下


image.png

一個(gè)簡(jiǎn)單的MVI網(wǎng)絡(luò)請(qǐng)求架構(gòu)到此結(jié)束

結(jié)尾

MVI其實(shí)主要思想是通過Intent將view和業(yè)務(wù)實(shí)現(xiàn)層分離稿存,達(dá)到通過意圖傳遞邏輯方法。所以不一定非要基于MVVM瞳秽,也適用于MVP瓣履,這次分享就到此結(jié)束了
最后感謝
https://blog.csdn.net/vitaviva/article/details/109406873
這篇文章提供的清晰簡(jiǎn)單的思路,代碼思路均由這篇文章獲取

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末练俐,一起剝皮案震驚了整個(gè)濱河市袖迎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖瓢棒,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浴韭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡脯宿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門泉粉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來连霉,“玉大人,你說我怎么就攤上這事嗡靡《搴常” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵讨彼,是天一觀的道長(zhǎng)歉井。 經(jīng)常有香客問我,道長(zhǎng)哈误,這世上最難降的妖魔是什么哩至? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蜜自,結(jié)果婚禮上菩貌,老公的妹妹穿的比我還像新娘。我一直安慰自己重荠,他們只是感情好箭阶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著戈鲁,像睡著了一般仇参。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婆殿,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天诈乒,我揣著相機(jī)與錄音,去河邊找鬼鸣皂。 笑死抓谴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寞缝。 我是一名探鬼主播癌压,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼荆陆!你這毒婦竟也來了滩届?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帜消,沒想到半個(gè)月后棠枉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泡挺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年辈讶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娄猫。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贱除,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出媳溺,到底是詐尸還是另有隱情月幌,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布悬蔽,位于F島的核電站扯躺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蝎困。R本人自食惡果不足惜录语,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望难衰。 院中可真熱鬧钦无,春花似錦、人聲如沸盖袭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳄虱。三九已至弟塞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拙已,已是汗流浹背决记。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留倍踪,地道東北人系宫。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像建车,于是被迫代替她去往敵國(guó)和親扩借。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容