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 -> {}
}
}
}
}
}