Android App封裝 ——架構(MVI + kotlin + Flow)
theme: juejin highlight: darcula
一、背景
最近看了好多MVI的文章,原理大多都是參照google釋出的 應用架構指南,但是實現方式有很多種,就想自己封裝一套自己喜歡用的MVI架構,以供以後開發App使用。
說幹就幹,準備對標“玩Android”,利用提供的資料介面,搭建一個自己習慣使用的一套App專案,專案地址:Github wanandroid。
二、MVI
先簡單說一下MVI,從MVC到MVP到MVVM再到現在的MVI,google是為了一直解決痛點所以不斷推出新的框架,具體的發展流程就不多做贅訴了,網上有好多,我們可以選擇性適合自己的。
應用架構指南中主要的就是兩個架構圖:
2.1 總體架構
Google推薦的是每個應用至少有兩層: - UI Layer 介面層: 在螢幕上顯示應用資料 - Data Layer 資料層: 提供所需要的應用資料(通過網路、檔案等) - Domain Layer(optional)領域層/網域層 (可選):主要用於封裝資料層的邏輯,方便與介面層的互動,可以根據User Case
圖中主要的點在於各層之間的依賴關係是單向的,所以方便了各層之間的單元測試
2.2 UI層架構
UI簡單來說就是拿到資料並展示,而資料是以state表示UI不同的狀態傳送給介面的,所以UI架構分為
- UI elements層:UI元素,由activity、fragment
以及包含的控制元件組成
- State holders層: state狀態的持有者,這裡一般是由viewModel
承擔
2.3 MVI UI層的特點
MVI在UI層相比與MVVM的核心區別是它的兩大特性: 1. 唯一可信資料來源 2. 資料單向流動。
從圖中可以看到,
1. 資料從Data Layer -> ViewModel -> UI,資料是單向流動的。ViewModel將資料封裝成UI State
傳輸到UI elements中,而UI elements是不會傳輸資料到ViewModel的。
2. UI elements上的一些點選或者使用者事件,都會封裝成events
事件,傳送給ViewModel
2.4 搭建MVI要注意的點
瞭解了MVI的原理和特點後,我們就要開始著手搭建了,其中需要解決的有以下幾點
1. 定義UI State
、events
2. 構建UI State
單向資料流UDF
3. 構建事件流events
4. UI State
的訂閱和傳送
三、搭建專案
3.1 定義UI State
、events
我們可以用interface先定義一個抽象的UI State
、events
,event
和intent
是一個意思,都可以用來表示一次事件。
```kotlin @Keep interface IUiState
@Keep
interface IUiIntent
然後根據具體邏輯定義頁面的UIState和UiIntent。
kotlin
data class MainState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState) : IUiState
sealed class BannerUiState {
object INIT : BannerUiState()
data class SUCCESS(val models: List
sealed class DetailUiState {
object INIT : DetailUiState()
data class SUCCESS(val articles: ArticleModel) : DetailUiState()
}
``
通過
MainState將頁面的不同狀態封裝起來,從而實現
唯一可信資料來源`
3.2 構建單向資料流UDF
在ViewModel中使用StateFlow
構建UI State流。
- _uiStateFlow
用來更新資料
- uiStateFlow
用來暴露給UI elements訂閱
```kotlin
abstract class BaseViewModel
private val _uiStateFlow = MutableStateFlow(initUiState())
val uiStateFlow: StateFlow<UiState> = _uiStateFlow
protected abstract fun initUiState(): UiState
protected fun sendUiState(copy: UiState.() -> UiState) {
_uiStateFlow.update { copy(_uiStateFlow.value) }
}
}
kotlin
class MainViewModel : BaseViewModel
override fun initUiState(): MainState {
return MainState(BannerUiState.INIT, DetailUiState.INIT)
}
} ```
3.3 構建事件流
在ViewModel中使用 Channel構建事件流
1. _uiIntentFlow
用來傳輸Intent
2. 在viewModelScope中開啟協程監聽uiIntentFlow
,在子ViewModel中只用重寫handlerIntent
方法就可以處理Intent事件了
3. 通過sendUiIntent就可以傳送Intent事件了
```
abstract class BaseViewModel
private val _uiIntentFlow: Channel<UiIntent> = Channel()
val uiIntentFlow: Flow<UiIntent> = _uiIntentFlow.receiveAsFlow()
fun sendUiIntent(uiIntent: UiIntent) {
viewModelScope.launch {
_uiIntentFlow.send(uiIntent)
}
}
init {
viewModelScope.launch {
uiIntentFlow.collect {
handleIntent(it)
}
}
}
protected abstract fun handleIntent(intent: IUiIntent)
class MainViewModel : BaseViewModel
override fun handleIntent(intent: IUiIntent) {
when (intent) {
MainIntent.GetBanner -> {
requestDataWithFlow()
}
is MainIntent.GetDetail -> {
requestDataWithFlow()
}
}
}
} ```
3.4 UI State
的訂閱和傳送
3.4.1 訂閱UI State
在Activity中訂閱UI state的變化
1. 在lifecycleScope
中開啟協程,collect
uiStateFlow
。
2. 使用map
來做區域性變數的更新
3. 使用distinctUntilChanged
來做資料防抖
```kotlin
class MainActivity : BaseMVIActivity() {
private fun registerEvent() {
lifecycleScope.launchWhenStarted {
mViewModel.uiStateFlow.map { it.bannerUiState }.distinctUntilChanged().collect { bannerUiState ->
when (bannerUiState) {
is BannerUiState.INIT -> {}
is BannerUiState.SUCCESS -> {
bannerAdapter.setList(bannerUiState.models)
}
}
}
}
lifecycleScope.launchWhenStarted {
mViewModel.uiStateFlow.map { it.detailUiState }.distinctUntilChanged().collect { detailUiState ->
when (detailUiState) {
is DetailUiState.INIT -> {}
is DetailUiState.SUCCESS -> {
articleAdapter.setList(detailUiState.articles.datas)
}
}
}
}
}
} ```
3.4.2 傳送Intent
直接呼叫sendUiIntent
就可以傳送Intent事件
kotlin
button.setOnClickListener {
mViewModel.sendUiIntent(MainIntent.GetBanner)
mViewModel.sendUiIntent(MainIntent.GetDetail(0))
}
3.4.3 更新Ui State
呼叫sendUiState
傳送Ui State更新
需要注意的是: 在UiState改變時,使用的是copy複製一份原來的UiState,然後修改變動的值。這是為了做到 “可信資料來源”,在定義MainState
的時候,設定的就是val
,是為了避免多執行緒併發讀寫,導致執行緒安全的問題。
```kotlin
class MainViewModel : BaseViewModel
override fun initUiState(): MainState {
return MainState(BannerUiState.INIT, DetailUiState.INIT)
}
override fun handleIntent(intent: IUiIntent) {
when (intent) {
MainIntent.GetBanner -> {
requestDataWithFlow(showLoading = true,
request = { mWanRepo.requestWanData() },
successCallback = { data -> sendUiState { copy(bannerUiState = BannerUiState.SUCCESS(data)) } },
failCallback = {})
}
is MainIntent.GetDetail -> {
requestDataWithFlow(showLoading = false,
request = { mWanRepo.requestRankData(intent.page) },
successCallback = { data -> sendUiState { copy(detailUiState = DetailUiState.SUCCESS(data)) } })
}
}
}
} ```
其中 requestDataWithFlow
是封裝的一個網路請求的方法
protected fun <T : Any> requestDataWithFlow(
showLoading: Boolean = true,
request: suspend () -> BaseData<T>,
successCallback: (T) -> Unit,
failCallback: suspend (String) -> Unit = { errMsg ->
//預設異常處理
},
) {
viewModelScope.launch {
val baseData: BaseData<T>
try {
baseData = request()
when (baseData.state) {
ReqState.Success -> {
sendLoadUiState(LoadUiState.ShowMainView)
baseData.data?.let { successCallback(it) }
}
ReqState.Error -> baseData.msg?.let { error(it) }
}
} catch (e: Exception) {
e.message?.let { failCallback(it) }
}
}
}
至此一個MVI的框架基本就搭建完畢了
3.5執行效果
四、 總結
不管是MVC、MVP、MVVM還是MVI,主要就是View和Model之間的互動關係不同 - MVI的核心是 資料的單向流動 - MVI使用kotlin flow可以很方便的實現 響應式程式設計 - MV整個View只依賴一個State重新整理,這個State就是 唯一可信資料來源
目前搭建了基礎框架,後續還會在此專案的基礎上繼續封裝jetpack等更加完善這個專案。
專案原始碼地址:Github wanandroid