Android 架構演進之路:從 MVC 到 MVI,擁抱單向數據流的革命

在移動應用開發的世界里,架構模式的演進從未停歇。從早期的 MVC 到后來的 MVP、MVVM,每一次變革都在嘗試解決前一代架構的痛點。而今天,我們將探討一種全新的架構模式 ——MVI(Model-View-Intent),它借鑒了前端 React 的單向數據流思想,并結合 Android 開發的最佳實踐,為我們帶來了一種更加清晰、可測試、可維護的代碼結構。

一、架構模式的演進歷程

1.1 MVC:原始的分層嘗試

MVC(Model-View-Controller)是最早被廣泛應用的架構模式之一,它將應用分為三個主要部分:

  • Model:負責數據和業務邏輯
  • View:負責 UI 展示
  • Controller:作為 View 和 Model 之間的橋梁,處理用戶輸入和狀態更新

在 Android 中,Activity 通常扮演 Controller 的角色,但隨著 UI 邏輯的復雜化,Activity 變得越來越臃腫,違反了單一職責原則。

1.2 MVP:引入 Presenter 層解耦

為了解決 MVC 的問題,MVP(Model-View-Presenter)應運而生:

  • Model:數據和業務邏輯
  • View:負責 UI 展示,通常是一個接口
  • Presenter:處理 UI 邏輯,與 Model 交互

MVP 通過接口將 View 和 Presenter 解耦,使得單元測試更加容易。但隨著項目規模增大,Presenter 層可能會變得非常龐大。

1.3 MVVM:數據綁定的力量

MVVM(Model-View-ViewModel)在 MVP 的基礎上引入了數據綁定:

  • Model:數據和業務邏輯
  • View:負責 UI 展示
  • ViewModel:暴露數據流,通過數據綁定與 View 交互

MVVM 通過 LiveData、Data Binding 等技術減少了 View 和 ViewModel 之間的耦合,但它仍然存在一些問題,例如雙向數據流可能導致的復雜性。

1.4 MVI:單向數據流的革命

MVI(Model-View-Intent)是架構模式的最新演進,它借鑒了 React 的單向數據流思想:

  • Model:不可變的狀態容器
  • View:訂閱狀態并渲染 UI
  • Intent:用戶操作轉換為 Intent,觸發狀態更新

MVI 的核心思想是單向數據流和狀態不可變性,所有的狀態變化都遵循一個可預測的流程,使得代碼更加易于理解和調試。

二、MVI 架構的核心概念

2.1 單向數據流

MVI 的核心是單向數據流,它遵循以下流程:

  1. 用戶操作產生 Intent
  2. Intent 被 ViewModel 處理
  3. ViewModel 根據 Intent 更新狀態
  4. View 訂閱狀態變化并更新 UI

這種單向流動使得數據的流向變得清晰可追溯,大大降低了代碼的復雜性。

2.2 不可變狀態

在 MVI 中,狀態是不可變的。每當有新的狀態產生時,不會修改原有狀態,而是創建一個新的狀態對象。這樣可以確保狀態的變化是可追蹤的,并且簡化了狀態管理。

2.3 狀態容器

ViewModel 作為狀態容器,負責管理和更新應用狀態。它接收 Intent 并根據這些 Intent 生成新的狀態,然后將新狀態發送給 View。

三、MVI 架構的實現

3.1 基礎架構實現

首先,我們需要定義一些基礎接口和類來構建 MVI 架構:

kotlin

// BaseIntent.kt
interface BaseIntent {
}// BaseState.kt
interface BaseState {data object Loading : BaseState  // 加載中狀態data object Complete: BaseState // 處理完成data class Error(val message: String = "", val updateId: Long = System.currentTimeMillis()): BaseState // 異常
}// BaseViewModel.kt
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launchabstract class BaseViewModel : ViewModel() {/*** 獲取上下文*/protected fun getContext(): Context = BaseApplication.instance.applicationContext// 接收來自視圖層的意圖(Intent)val intentsFlow = MutableSharedFlow<BaseIntent>()// 用于向視圖層發送狀態更新,類型指定為BaseStateval stateFlow = MutableStateFlow<BaseState?>(null)init {consumption()}/*** 分發消息*/private fun consumption() {viewModelScope.launch {intentsFlow.collect { intent ->handleIntents(intent)}}}abstract fun handleIntents(intent: BaseIntent)// 方法用于向視圖層發送新的BaseState類型的狀態protected fun updateState(newState: BaseState) {viewModelScope.launch {delay(100) // 防止消息太快,丟失stateFlow.value = newState}}
}// BaseActivity.kt
import android.util.Log
import android.view.LayoutInflater
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.launch
import kotlin.reflect.KClassabstract class BaseActivity<VM : BaseViewModel, VB : ViewBinding>(override val vbInflater: (LayoutInflater) -> VB
) : AppCompatActivity() {val tag = this::class.simpleNameprivate val _binding by lazy { vbInflater(layoutInflater) }val binding get() = _bindingopen val viewModel: VM by lazy {initializeViewModel()}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(binding.root)setupObservers()setupIntents()bindClickListener()}/*** 初始化ViewModel*/private fun initializeViewModel(): VM {val classifier = this::class.supertypes[0].arguments[0].type?.classifierLog.d(this::class.simpleName, "initializeViewModel()----->${classifier}")val viewModelClass = (classifier as? KClass<VM>)?.java?: throw IllegalArgumentException("Invalid ViewModel class")return ViewModelProvider(this)[viewModelClass]}/*** 分發狀態*/private fun setupObservers() {lifecycleScope.launch {viewModel.stateFlow.collect { state ->Log.d(tag, "setupObservers()---->$state")render(state)}}}// 抽象方法,由子類實現來根據不同的BaseState狀態渲染UIabstract fun render(state: BaseState?)// 輔助方法,用于發送BaseIntent類型的意圖到ViewModelprotected fun sendIntent(intent: BaseIntent) {lifecycleScope.launch {viewModel.intentsFlow.emit(intent)}}// 設置意圖,由子類實現open fun setupIntents() {}// 綁定點擊事件,由子類實現open fun bindClickListener() {}
}

3.2 登錄功能的 MVI 實現

接下來,我們以登錄功能為例,展示如何使用 MVI 架構實現一個完整的功能模塊。

首先是布局文件:

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_username"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toTopOf="@+id/et_password"app:layout_constraintVertical_chainStyle="packed"android:layout_marginStart="12dp"android:layout_marginEnd="12dp"android:layout_marginBottom="12dp"android:layout_width="match_parent"android:hint="手機號"android:layout_height="wrap_content"/><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_password"android:layout_width="match_parent"app:layout_constraintTop_toBottomOf="@+id/et_username"app:layout_constraintBottom_toTopOf="@+id/btn_login"android:hint="密碼"android:layout_margin="12dp"app:layout_constraintVertical_chainStyle="packed"android:layout_height="wrap_content"/><androidx.appcompat.widget.AppCompatButtonandroid:id="@+id/btn_login"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="24dp"android:layout_marginStart="12dp"android:layout_marginEnd="12dp"android:textColor="@color/white"android:text="Login"android:background="@drawable/selector_rounded_button"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintTop_toBottomOf="@+id/et_password"/><com.google.android.material.progressindicator.CircularProgressIndicatorandroid:id="@+id/progressBar"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:indeterminateTint="@color/main_color"app:indicatorColor="@color/main_color"app:trackColor="@color/main_color"android:layout_width="40dp"android:visibility="gone"android:layout_height="40dp"/></androidx.constraintlayout.widget.ConstraintLayout>

然后定義登錄相關的 Intent、State 和 ViewModel:

kotlin

// LoginIntent.kt
sealed class LoginIntent: BaseIntent {data class EntryPhone(val phone: String): LoginIntent() // 手機號的意圖data class EntryPassword(val password: String): LoginIntent() // 密碼的意圖data object Login: LoginIntent() // 點擊登錄
}// LoginViewState.kt
sealed class LoginViewState: BaseState {data class LoginStatus(val highlight: Boolean) : LoginViewState()// 高亮狀態data object Loading : LoginViewState()                   // 加載中狀態data class Success(val user: UserBean?) : LoginViewState() // 成功登錄狀態data class Error(val message: String) : LoginViewState() // 登錄錯誤狀態
}// LoginViewModel.kt
@HiltViewModel
class LoginViewModel @Inject constructor (private val repository: LoginRepository,
) : BaseViewModel() {private var phone: String = ""private var password: String = ""// 設置登錄按鈕是否可用private fun setLoginButtonEnabled(enabled: Boolean) {updateState(LoginViewState.LoginStatus(enabled))}/*** 調用登錄*/private fun login() {updateState(LoginViewState.Loading)viewModelScope.launch {repository.login(phone, password).onSuccess {updateState(LoginViewState.Success(it))}.onFailure {updateState(LoginViewState.Error(it.message.toString()))}}}/*** 處理意圖*/override fun handleIntents(intent: BaseIntent) {when(intent) {is LoginIntent.EntryPhone -> {phone = intent.phonesetLoginButtonEnabled(phone.length >= 6 && password.length >= 6)}is LoginIntent.EntryPassword -> {password = intent.passwordsetLoginButtonEnabled(password.length >= 6 && phone.length >= 6)}is LoginIntent.Login -> {login()}}}
}

最后是 Activity 的實現:

kotlin

// LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : BaseActivity<LoginViewModel, ActivityLoginBinding>(ActivityLoginBinding::inflate
) {/*** 監聽狀態*/override fun render(state: BaseState?) {when(state) {is LoginViewState.LoginStatus -> {setLoginButtonEnabled(state.highlight)}is LoginViewState.Loading -> {showLoading()}is LoginViewState.Success -> {if (state.user != null) {showSuccess(state.user)}}is LoginViewState.Error -> {showError(state.message)}}}/*** View視圖與Intent意圖進行綁定*/override fun setupIntents() {binding.etUsername.addTextChangedListener { text ->sendIntent(LoginIntent.EntryPhone(text.toString()))}binding.etPassword.addTextChangedListener { text ->sendIntent(LoginIntent.EntryPassword(text.toString()))}binding.etUsername.setText("1234567890")binding.etPassword.setText("54321#")}/*** 設置點擊事件*/override fun bindClickListener() {super.bindClickListener()binding.btnLogin.setOnClickListener {sendIntent(LoginIntent.Login)}}/*** 登錄成功*/private fun showSuccess(user: UserBean) {binding.progressBar.visibility = View.GONEToast.makeText(this, "Login successful: ${user.userName}", Toast.LENGTH_SHORT).show()}/*** 顯示錯誤信息*/override fun showError(errorMessage: String?) {super.showError(errorMessage)binding.progressBar.visibility = View.GONEToast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()}// 登錄按鈕是否可用private fun setLoginButtonEnabled(enabled: Boolean) {binding.btnLogin.isEnabled = enabled}
}

四、MVI 架構的優勢與挑戰

4.1 優勢

  1. 單向數據流:數據流動方向明確,易于理解和調試
  2. 可測試性:ViewModel 不依賴 View,單元測試更加簡單
  3. 狀態可追溯:所有狀態變化都可以追蹤,便于定位問題
  4. 解耦徹底:View 和 Model 完全解耦,職責更加清晰
  5. 一致性:所有狀態更新都遵循相同的模式,代碼風格統一

4.2 挑戰

  1. 學習曲線:相較于 MVVM,MVI 的概念更加抽象,需要一定的時間來理解
  2. 初期開發效率:需要編寫更多的代碼(Intent 和 State),初期開發效率可能較低
  3. 狀態管理復雜度:在復雜場景下,狀態管理可能變得復雜,需要合理設計狀態結構

五、總結

MVI 架構通過引入單向數據流和不可變狀態,為 Android 開發帶來了一種更加清晰、可測試、可維護的代碼結構。雖然初期學習和開發成本較高,但在大型項目中,MVI 的優勢將得到充分體現。

如果你正在尋找一種能夠徹底解決 View 和 Model 耦合問題的架構模式,那么 MVI 絕對值得一試。通過遵循 MVI 的設計原則,你可以構建出更加健壯、易于維護的 Android 應用。

希望本文能夠幫助你理解 MVI 架構的核心概念和實現方式,讓你在 Android 開發的道路上更進一步。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/82490.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/82490.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/82490.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【YOLOv8-pose部署至RK3588】模型訓練→轉換RKNN→開發板部署

已在GitHub開源與本博客同步的YOLOv8_RK3588_object_pose 項目&#xff0c;地址&#xff1a;https://github.com/A7bert777/YOLOv8_RK3588_object_pose 詳細使用教程&#xff0c;可參考README.md或參考本博客第六章 模型部署 文章目錄 一、項目回顧二、文件梳理三、YOLOv8-pose…

集成30+辦公功能的實用工具

軟件介紹 本文介紹的軟件是千峰辦公助手。 軟件功能概述與開發目的 千峰辦公助手集成了自動任務、系統工具、文件工具、PDF工具、OCR圖文識別、文字處理、電子表格七個模塊&#xff0c;擁有30余項實用功能。作者開發該軟件的目的是解決常見辦公痛點&#xff0c;把機械操作交…

IDEA啟動報錯:Cannot invoke “org.flowable.common.engine.impl.persistence.ent

1.問題 項目啟動報錯信息 java.lang.NullPointerException: Cannot invoke "org.flowable.common.engine.impl.persistence.ent 2.問題解析 出現這個問題是在項目中集成了Flowable或Activiti工作流&#xff0c;開啟自動創建工作流創建的表&#xff0c;因為不同環境的數據…

網絡安全--PHP第三天

今天學習文件上傳的相關知識 上傳的前端頁面如下 upload.html <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"&g…

【愚公系列】《生產線數字化設計與仿真》004-顏色分類站仿真(基礎概念)

??【技術大咖愚公搬代碼:全棧專家的成長之路,你關注的寶藏博主在這里!】?? ??開發者圈持續輸出高質量干貨的"愚公精神"踐行者——全網百萬開發者都在追更的頂級技術博主! ?? 江湖人稱"愚公搬代碼",用七年如一日的精神深耕技術領域,以"…

基于 uni-app + <movable-view>拖拽實現的標簽排序-適用于微信小程序、H5等多端

在實際業務中&#xff0c;我們經常遇到「標簽排序」或「菜單調整」的場景。微信小程序原生的 movable-view 為我們提供了一個簡單、高效的拖拽能力&#xff0c;結合 Vue3 uni-app 的組合&#xff0c;我們可以實現一個體驗良好的標簽管理界面。 核心組件&#xff1a;<movab…

一些較好的學習方法

1、網上有一些非常經典的電路&#xff0c;而且有很多視頻博主做了詳細的講解。 2、有一部分拆解的UP主&#xff0c;拆解后會還原該器件的原理圖&#xff0c;并一步步做講解。 3、有兩本書&#xff0c;數電、模電&#xff0c;這兩本書中的內容很多都值得學習。 5、某寶上賣的…

《1.1_4計算機網絡的分類|精講篇|附X-mind思維導圖》

網絡相關知識 按使用范圍分類 公用網 由電信部門或其他提供通信服務的經營部門組建、管理和控制&#xff0c;向全社會提供服務的網絡。 專用網 由某個單位或部門組建、僅供本單位或部門內部使用的網絡。 按傳輸介質分類 有線網絡 如&#xff1a;雙絞線、同軸電纜、光纖…

Git 和 GitHub 學習指南本地 Git 配置、基礎命令、GitHub 上傳流程、企業開發中 Git 的使用流程、以及如何將代碼部署到生產服務器

Windows 上 Git 安裝與配置 下載安裝&#xff1a;訪問 Git 官方網站下載適用于 Windows 的安裝程序。運行安裝包時會出現許可協議、安裝目錄、組件選擇等界面&#xff08;如下圖&#xff09;。在“Select Components”頁面建議勾選 Git Bash Here 等選項&#xff0c;以便在資源…

航空航天領域對滾珠絲桿的精度要求有多高?

航空航天領域對滾珠絲桿的精度要求非常高&#xff0c;尤其是飛行器、火箭和衛星等載具的導航和定位系統都需要高精度的滾珠絲桿&#xff0c;以確保高精度的位置控制和穩定的導航性能。那么&#xff0c;航空航天領域對滾珠絲桿的精度要求有多高&#xff1f; 1、定位精度&#xf…

技術篇-2.5.Matlab應用場景及開發工具安裝

Matlab 在數學建模和數值分析等領域具有無可替代的地位。它幾乎涵蓋所有常見數學算法的內置函數庫&#xff0c;使得從數據預處理、方程求解到優化算法的實現&#xff0c;無需編寫大量底層代碼即可快速完成&#xff1b;同時&#xff0c;Matlab 強大的可視化能力&#xff0c;可以…

Vtk概覽1

vtk環境搭建 見&#xff08;VTK開發環境配置(Visual Studio C)-詳細圖文教程-CSDN博客&#xff09; 在學習vtk圖形圖像進階的第二章時&#xff0c;通過vs2022建的控制臺程序&#xff0c;編寫運行示例2.1 發現 不顯示圖像。 #include <iostream> #include<vtkRenderW…

【數據集】基于ubESTARFM法的100m 地溫LST數據集(澳大利亞)

目錄 數據概述一、輸入數據與處理二、融合算法1. ESTARFM(Enhanced STARFM)2. ubESTARFM(Unbiased ESTARFM)代碼實現數據下載參考根據論文《Generating daily 100 m resolution land surface temperature estimates continentally using an unbiased spatiotemporal fusion…

Lucide:一款精美的開源矢量圖標庫,前端圖標新選擇

名人說:博觀而約取,厚積而薄發。——蘇軾《稼說送張琥》 創作者:Code_流蘇(CSDN)(一個喜歡古詩詞和編程的Coder??) 目錄 一、前言:為何選擇 Lucide?二、Lucide 是什么?1. 基本介紹2. Lucide vs Feather三、如何在項目中使用 Lucide?1. 安裝圖標包(以 React 為例)2…

BeanUtil和BeanUtils有什么區別

BeanUtil 和 BeanUtils 是兩個常見的工具類&#xff0c;通常用于 Java 開發中處理對象之間的屬性復制或轉換。它們的功能可能看起來相似&#xff0c;但實際上它們來自不同的庫&#xff0c;并且在實現細節和使用方式上存在一些差異。 以下是它們的主要區別&#xff1a; 1. 來源…

【CF】Day66——Edu 168.D + CF 853 (Div. 2).C (樹 + 二分 + 貪心 | 組合數學)

D. Maximize the Root 題目&#xff1a; 思路&#xff1a; 樹上二分&#xff0c;中下題 我們可以發現如果 x 可以&#xff0c;那么 x - 1 肯定也可以&#xff0c;所以可以直接二分答案 具體的&#xff0c;我們每次二分能增加的值 mid &#xff0c;如果 a[i] < mid&#xf…

生成對抗網絡(GANs)中的損失函數公式 判別器最優解D^*(x)的推導

https://www.bilibili.com/video/BV1YyHSekEE2 這張圖片展示的是生成對抗網絡&#xff08;GANs&#xff09;中的損失函數公式&#xff0c;特別是針對判別器&#xff08;Discriminator&#xff09;和生成器&#xff08;Generator&#xff09;的優化目標。讓我們用Markdown格式逐…

分布式爬蟲架構設計

隨著互聯網數據的爆炸式增長&#xff0c;單機爬蟲已經難以滿足大規模數據采集的需求。分布式爬蟲應運而生&#xff0c;它通過多節點協作&#xff0c;實現了數據采集的高效性和容錯性。本文將深入探討分布式爬蟲的架構設計&#xff0c;包括常見的架構模式、關鍵技術組件、完整項…

[java]eclipse中windowbuilder插件在線安裝

目錄 一、打開eclipse 二、打開插件市場 三、輸入windowbuilder&#xff0c;點擊install 四、進入安裝界面 五、勾選我同意... 重啟即可 一、打開eclipse 二、打開插件市場 三、輸入windowbuilder&#xff0c;點擊install 四、進入安裝界面 五、勾選我同意... 重啟即可

sass,less是什么?為什么要使用他們?

理解 他們都是css的預處理器,允許開發者通過更高級的語法編寫css代碼(支持變量,嵌套),然后通過編譯成css文件 使用原因 結構清晰,便于擴展提高開發效率,便于后期開發維護