Android MVI架構(gòu)

MVI(Model-View-Intent)是 Google 應(yīng)用架構(gòu)指南中推薦的架構(gòu)模式梳庆,它主要解決了傳統(tǒng)架構(gòu)模式中可能存在的狀態(tài)管理復(fù)雜患民,耦合度高寸认,測(cè)試?yán)щy等問(wèn)題,這篇文章旨在從零開(kāi)始搭建一個(gè) MVI 架構(gòu)伍茄,使我們的業(yè)務(wù)代碼更加簡(jiǎn)潔優(yōu)雅,提高后續(xù)的開(kāi)發(fā)效率施逾。

簡(jiǎn)介

MVI 架構(gòu)由三個(gè)主要部分組成:Model敷矫,View 和 Intent例获,每部分都有各自明確的職責(zé)。

模型(Model):應(yīng)用程序的數(shù)據(jù)層曹仗,負(fù)責(zé)管理數(shù)據(jù)的狀態(tài)和提供數(shù)據(jù)操作的方法榨汤。
視圖(View):用戶界面的表示,負(fù)責(zé)顯示數(shù)據(jù)并響應(yīng)用戶的操作怎茫。
意圖(Intent):用戶的操作或事件收壕,該事件將傳遞給模型來(lái)執(zhí)行相應(yīng)的操作。

在 MVVM 架構(gòu)中轨蛤,ViewModel 從數(shù)據(jù)層獲取數(shù)據(jù)蜜宪,通過(guò) ViewModel 層的數(shù)據(jù)變化驅(qū)動(dòng) UI 更新,而在 MVI 中祥山,不同的是圃验,MVI 是做 UI 狀態(tài)的集中管理,簡(jiǎn)言之就是將所有的狀態(tài)寫(xiě)在一個(gè)類(lèi)中缝呕,可以是密封類(lèi)或普通類(lèi)澳窑,并以單向數(shù)據(jù)流的形式,將 UI 狀態(tài)輸出到 UI 層供常,UI 層根據(jù)狀態(tài)做相應(yīng)的處理摊聋。舉個(gè)例子:Activity 向 ViewModel 發(fā)送 Intent 事件,ViewModel 集中處理用戶操作话侧,也就是用戶意圖事件的統(tǒng)一管理栗精。

MVI 架構(gòu)的兩個(gè)主要特點(diǎn)就是 UI 狀態(tài)的集中管理和單向數(shù)據(jù)流

特點(diǎn)

優(yōu)點(diǎn)

單向數(shù)據(jù)流:通過(guò)單向數(shù)據(jù)流確保狀態(tài)的一致性和可預(yù)測(cè)性,所有的狀態(tài)變化都通過(guò) Intent 觸發(fā)瞻鹏,并由 Model 處理悲立,最終反映在 View 上,這種方式使得狀態(tài)變化更加清晰和易于追蹤新博。
簡(jiǎn)化狀態(tài)管理:UI 的所有變化都來(lái)自于狀態(tài)薪夕,我們只需關(guān)注狀態(tài)的變化即可實(shí)現(xiàn) UI 更新,這種方式使得架構(gòu)更加簡(jiǎn)單赫悄,易于調(diào)試和維護(hù)原献。
線程安全:State 實(shí)例是不可變的,這有助于確保線程安全埂淮。每次狀態(tài)更新時(shí)都會(huì)創(chuàng)建新的 State 對(duì)象姑隅,避免了多線程環(huán)境下的數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。
解耦和復(fù)用:通過(guò)將 UI 邏輯與業(yè)務(wù)邏輯分離倔撞,實(shí)現(xiàn)了較高的解耦度讲仰,這使得 UI 組件可以被輕松替換或復(fù)用,提高了代碼的復(fù)用性和可維護(hù)性痪蝇。

缺點(diǎn)

狀態(tài)膨脹:當(dāng)處理復(fù)雜頁(yè)面時(shí)鄙陡,狀態(tài)可能會(huì)變得非常龐大和復(fù)雜冕房,這會(huì)導(dǎo)致?tīng)顟B(tài)管理變得困難。
內(nèi)存開(kāi)銷(xiāo):由于每次狀態(tài)更新都需要?jiǎng)?chuàng)建新的 State 對(duì)象趁矾,因此在高頻率的狀態(tài)更新場(chǎng)景下可能會(huì)帶來(lái)一定的內(nèi)存開(kāi)銷(xiāo)耙册。

適用場(chǎng)景

MVI 特別適合于需要強(qiáng)大響應(yīng)性和狀態(tài)管理的應(yīng)用,如實(shí)時(shí)聊天毫捣,表單驗(yàn)證和復(fù)雜交互的應(yīng)用程序详拙。由于 MVI 可能會(huì)引入更多的類(lèi)和接口,導(dǎo)致代碼結(jié)構(gòu)相對(duì)復(fù)雜培漏,所以在小型簡(jiǎn)單的頁(yè)面中可能會(huì)顯得有些繁瑣溪厘。

基類(lèi)搭建

先貼上需要用到的依賴:

implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.android)
implementation(libs.androidx.activity.activity.ktx)
implementation (libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.squareup.okhttp)
implementation(libs.squareup.logging.interceptor)
implementation(libs.squareup.retrofit)
implementation(libs.squareup.converter.gson)

BaseActivity

abstract class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initView()
        initData()
    }

    abstract fun initView()
    abstract fun initData()
}

BaseVBActivity

abstract class BaseVBActivity<VB : ViewBinding>(val block: (LayoutInflater) -> VB) :
    BaseActivity() {
    private var _binding: VB? = null
    protected val binding: VB
        get() = requireNotNull(_binding) { "The binding has been destroyed" }

    override fun initView() {
        _binding = block(layoutInflater)
        setContentView(binding.root)
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

}

BaseFragment

abstract class BaseFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
        initData()
    }

    abstract fun initView()
    abstract fun initData()
}

BaseVBFragment

abstract class BaseVBFragment<VB : ViewBinding>(val block: (LayoutInflater) -> VB) :
    BaseFragment() {
    private var _binding: VB? = null
    protected val binding: VB
        get() = requireNotNull(_binding) { "The binding has been destroyed" }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = block(layoutInflater)
        return binding.root
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

定義用戶意圖和 UI 狀態(tài)

interface IUIState

interface IUiIntent

BaseViewModel

abstract class BaseViewModel<UiState : IUIState, UiIntent : IUiIntent> : ViewModel() {

    private val _uiStateFlow = MutableStateFlow(initUIState())
    val uiStateFlow: StateFlow<UiState> = _uiStateFlow

    private val intentChannel: Channel<UiIntent> = Channel()

    protected abstract fun initUIState(): UiState
    protected abstract fun handleIntent(intent: UiIntent)

    init {
        viewModelScope.launch {
            intentChannel.consumeAsFlow().collect {
                handleIntent(it)
            }
        }
    }

    fun sendUiIntent(uiIntent: UiIntent) {
        viewModelScope.launch {
            intentChannel.send(uiIntent)
        }
    }

    protected fun sendUiState(copy: UiState.() -> UiState) {
        _uiStateFlow.update { copy(_uiStateFlow.value) }
    }

}

業(yè)務(wù)代碼
這里舉個(gè)簡(jiǎn)單的例子:網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù),然后將這個(gè)請(qǐng)求結(jié)果顯示在一個(gè) TextView 上牌柄。

定義一個(gè)工具類(lèi)畸悬,用于創(chuàng)建 Retrofit 。

class RetrofitUtil {

    companion object {

        private const val TIME_OUT = 20L

        private fun createRetrofit(): Retrofit {

            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY

            val okHttpClient = OkHttpClient().newBuilder().apply {
                addInterceptor(interceptor)
                retryOnConnectionFailure(true)
                connectTimeout(TIME_OUT, TimeUnit.SECONDS)
                writeTimeout(TIME_OUT, TimeUnit.SECONDS)
                readTimeout(TIME_OUT, TimeUnit.SECONDS)
            }.build()

            return Retrofit.Builder().apply {
                addConverterFactory(GsonConverterFactory.create())
                baseUrl(BASE_URL)
                client(okHttpClient)
            }.build()

        }

        fun <T> getAPI(clazz: Class<T>): T {
            return createRetrofit().create(clazz)
        }

    }
}

定義一個(gè)網(wǎng)絡(luò)請(qǐng)求的幫助類(lèi)珊佣,用于存放和調(diào)用各種網(wǎng)絡(luò)請(qǐng)求方法蹋宦。

class RequestHelper private constructor() {

    private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

    companion object {
        val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            RequestHelper()
        }
    }

    suspend fun getListData(params: HashMap<String, String>) = httpApi.getHttpData(params)
}

這里就定義了一個(gè)意圖,用來(lái)獲取網(wǎng)絡(luò)數(shù)據(jù)咒锻。

sealed class MainIntent : IUiIntent {
    data class GetListData(var page: Int) : MainIntent()
}

定義 UI 狀態(tài)

sealed class MainUiState : IUIState {
    data object Init : MainUiState()
    data class Success(val data: DataResponse?): MainUiState()
    data class Fail(val msg: String?): MainUiState()
}

繼承 BaseViewModel冷冗,實(shí)現(xiàn)我們具體的業(yè)務(wù)功能。

class MainViewModel : BaseViewModel<MainUiState, MainIntent>() {
    override fun initUIState() = MainUiState.Init

    override fun handleIntent(intent: MainIntent) {
        when (intent) {
            is MainIntent.GetListData -> {
                getListData(intent.page)
            }
        }
    }

    private fun getListData(page: Int) = netRequest {
        request {
            val hashMap = hashMapOf<String, String>()
            hashMap["token"] = TOKEN
            hashMap["pageSize"] = PAGE_SIZE
            hashMap["page"] = page.toString()
            RequestHelper.instance.getListData(hashMap)
        }
        success {
            sendUiState { MainUiState.Success(it) }
        }
        error {
            sendUiState { MainUiState.Fail(it) }
        }
    }
}

至于這個(gè) netRequest 網(wǎng)絡(luò)請(qǐng)求的封裝惑艇,可以看我的另一篇文章:如何讓 Android 網(wǎng)絡(luò)請(qǐng)求像詩(shī)一樣優(yōu)雅蒿辙,這里不再贅述。

在 Fragment 中滨巴,發(fā)送意圖并根據(jù) UI 狀態(tài)做相應(yīng)的處理思灌。

class MainFragment : BaseVBFragment<FragmentMainBinding>({
    FragmentMainBinding.inflate(it)
}) {
    private val mViewModel by viewModels<MainViewModel>()

    override fun initView() {
        binding.textView.text = "Initialization"
    }

    override fun initData() {
        binding.textView.setOnClickListener {
            mViewModel.sendUiIntent(MainIntent.GetListData(1))
        }
        lifecycleScope.launch {
            mViewModel.uiStateFlow.collect {
                when (it) {
                    is MainUiState.Success -> {
                        binding.textView.text = it.data?.data.toString()
                    }

                    is MainUiState.Fail -> {
                        binding.textView.text = it.msg
                    }

                    else -> {}
                }
            }
        }
    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市恭取,隨后出現(xiàn)的幾起案子泰偿,更是在濱河造成了極大的恐慌,老刑警劉巖蜈垮,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耗跛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡攒发,警方通過(guò)查閱死者的電腦和手機(jī)调塌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惠猿,“玉大人羔砾,你說(shuō)我怎么就攤上這事。” “怎么了蜒茄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)餐屎。 經(jīng)常有香客問(wèn)我檀葛,道長(zhǎng),這世上最難降的妖魔是什么腹缩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任屿聋,我火速辦了婚禮,結(jié)果婚禮上藏鹊,老公的妹妹穿的比我還像新娘润讥。我一直安慰自己,他們只是感情好盘寡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布楚殿。 她就那樣靜靜地躺著,像睡著了一般竿痰。 火紅的嫁衣襯著肌膚如雪脆粥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天影涉,我揣著相機(jī)與錄音变隔,去河邊找鬼。 笑死蟹倾,一個(gè)胖子當(dāng)著我的面吹牛匣缘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鲜棠,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼肌厨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了岔留?” 一聲冷哼從身側(cè)響起夏哭,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎献联,沒(méi)想到半個(gè)月后竖配,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡里逆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年进胯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片原押。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胁镐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盯漂,我是刑警寧澤颇玷,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站就缆,受9級(jí)特大地震影響帖渠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜竭宰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一空郊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧切揭,春花似錦狞甚、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至嗤谚,卻和暖如春棺蛛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背巩步。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工旁赊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人椅野。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓终畅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親竟闪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子离福,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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