Jetpack?Compose?的新型架構(gòu)?MVI使用詳解
為什么是MVI而不是MVVM
MVVM作為流行的架構(gòu)模式,應(yīng)用在 Compose上,并沒有大的問題或者設(shè)計(jì)缺陷。但是在使用期間,發(fā)現(xiàn)了并不適合我的地方,或者說是使用起來不順手的地方:
- 數(shù)據(jù)觀察者過多:如果界面有多個(gè)狀態(tài),就要多個(gè) LiveData 或者 Flow,維護(hù)麻煩。
- 更新 UI 狀態(tài)的來源過多:數(shù)據(jù)觀察者多,并行或同時(shí)更新 UI,造成不必要的重繪。
- 大量訂閱觀察者函數(shù),也沒有約束:存儲和更新沒有分離,容易混亂,代碼臃腫。
單向數(shù)據(jù)流

單向數(shù)據(jù)流 (UDF) 是一種設(shè)計(jì)模式,在該模式下狀態(tài)向下流動(dòng),事件向上流動(dòng)。通過采用單向數(shù)據(jù)流,您可以將在界面中顯示狀態(tài)的可組合項(xiàng)與應(yīng)用中存儲和更改狀態(tài)的部分分離開來。
使用單向數(shù)據(jù)流的應(yīng)用的界面更新循環(huán)如下所示:
- 事件:界面的某一部分生成一個(gè)事件,并將其向上傳遞,例如將按鈕點(diǎn)擊傳遞給 ViewModel 進(jìn)行處理;或者從應(yīng)用的其他層傳遞事件,如指示用戶會(huì)話已過期。
- 更新狀態(tài):事件處理腳本可能會(huì)更改狀態(tài)。
- 顯示狀態(tài):狀態(tài)容器向下傳遞狀態(tài),界面顯示此狀態(tài)。
以上是官方對單向數(shù)據(jù)流的介紹。下面介紹適合單項(xiàng)數(shù)據(jù)流的架構(gòu) MVI。
MVI
MVI 包含三部分,Model — View — Intent
- Model 表示 UI 的狀態(tài),例如加載和數(shù)據(jù)。
- View 根據(jù)狀態(tài)展示對應(yīng) UI。
- Intent 代表用戶與 UI 交互時(shí)的意圖。例如點(diǎn)擊一個(gè)按鈕提交數(shù)據(jù)。
可以看出 MVI 完美的符合官方推薦架構(gòu) ,我們引用 React Redux 的概念分而治之:
- State 需要展示的狀態(tài),對應(yīng) UI 需要的數(shù)據(jù)。
- Event 來自用戶和系統(tǒng)的是事件,也可以說是命令。
- Effect 單次狀態(tài),即不是持久狀態(tài),類似于 EventBus ,例如加載錯(cuò)誤提示出錯(cuò)、或者跳轉(zhuǎn)到登錄頁,它們只執(zhí)行一次,通常在 Compose 的副作用中使用。
實(shí)現(xiàn)
首先我們需要約束類型的接口:
interface UiState interface UiEvent interface UiEffect
然后創(chuàng)建抽象的 ViewModel :
abstract class BaseViewModel< S : UiState, E : UiEvent,F : UiEffect> : ViewModel() {}
對于狀態(tài)的處理,我們使用StateFlow,StateFlow就像LiveData但具有初始值,所以需要一個(gè)初始狀態(tài)。這也是一種SharedFlow.我們總是希望在 UI 變得可見時(shí)接收最后一個(gè)視圖狀態(tài)。為什么不使用MutableState,因?yàn)?code>Flow 的api和操作符十分強(qiáng)大。
private val initialState: S by lazy { initialState() }
protected abstract fun initialState(): S
private val _uiState: MutableStateFlow<S> by lazy { MutableStateFlow(initialState) }
val uiState: StateFlow<S> by lazy { _uiState }
對于意圖,即事件,我要接收和處理:
private val _uiEvent: MutableSharedFlow<E> = MutableSharedFlow()
init {
subscribeEvents()
}
/**
* 收集事件
*/
private fun subscribeEvents() {
viewModelScope.launch {
_uiEvent.collect {
// reduce event
}
}
}
fun sendEvent(event: E) {
viewModelScope.launch {
_uiEvent.emit(event)
}
}
然后 Reducer 處理事件,更新狀態(tài):
/**
* 處理事件,更新狀態(tài)
* @param state S
* @param event E
*/
private fun reduceEvent(state: S, event: E) {
viewModelScope.launch {
handleEvent(event, state)?.let { newState -> sendState { newState } }
}
}
protected abstract suspend fun handleEvent(event: E, state: S): S?
單一的副作用:
private val _uiEffect: MutableSharedFlow<F> = MutableSharedFlow()
val uiEffect: Flow<F> = _uiEffect
protected fun sendEffect(effect: F) {
viewModelScope.launch { _uiEffect.emit(effect) }
}
使用
接下來實(shí)現(xiàn)一個(gè) Todo 應(yīng)用,打開應(yīng)用獲取歷史任務(wù),點(diǎn)擊加號增加一條新的任務(wù),完成任務(wù)后后 Toast 提示。
首先分析都有哪些狀態(tài):
- 是否在加載歷史任務(wù)
- 是否添加新任務(wù)
- 任務(wù)列表
創(chuàng)建約束類:
internal data class TodoState(
val isShowAddDialog: Boolean=false,
val isLoading: Boolean = false,
val goodsList: List<Todo> = listOf(),
) : UiState
然后分析有哪些意圖:
- 加載任務(wù)(進(jìn)入自動(dòng)加載,所以省略)
- 顯示任務(wù)
- 加載框的顯示隱藏
- 添加新任務(wù)
- 完成任務(wù)
internal sealed interface TodoEvent : UiEvent {
data class ShowData(val items: List<Todo>) : TodoEvent
data class OnChangeDialogState(val show: Boolean) : TodoEvent
data class AddNewItem(val text: String) : TodoEvent
data class OnItemCheckedChanged(val index: Int, val isChecked: Boolean) : TodoEvent
}
而單一事件就一種,完成任務(wù)時(shí)候的提示:
internal sealed interface TodoEffect : UiEffect {
// 已完成
data class Completed(val text: String) : TodoEffect
}
界面
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
internal fun TodoScreen(
viewModel: TodoViewModel = viewModel(),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
viewModel.collectSideEffect { effect ->
Log.e("", "TodoScreen: collectSideEffect")
when (effect) {
is TodoEffect.Completed -> Toast.makeText(context,
"${effect.text}已完成",
Toast.LENGTH_SHORT)
.show()
}
}
// LaunchedEffect(Unit) {
// viewModel.uiEffect.collect { effect ->
// Log.e("", "TodoScreen: LaunchedEffect")
//
// when (effect) {
// is TodoEffect.Completed -> Toast.makeText(context,
// "${effect.text}已完成",
// Toast.LENGTH_SHORT)
// .show()
// }
// }
// }
when {
state.isLoading -> ContentWithProgress()
state.goodsList.isNotEmpty() -> TodoListContent(
state.goodsList,
state.isShowAddDialog,
onItemCheckedChanged = { index, isChecked ->
viewModel.sendEvent(TodoEvent.OnItemCheckedChanged(index, isChecked))
},
onAddButtonClick = { viewModel.sendEvent(TodoEvent.OnChangeDialogState(true)) },
onDialogDismissClick = { viewModel.sendEvent(TodoEvent.OnChangeDialogState(false)) },
onDialogOkClick = { text -> viewModel.sendEvent(TodoEvent.AddNewItem(text)) },
)
}
}
@Composable
private fun TodoListContent(
todos: List<Todo>,
isShowAddDialog: Boolean,
onItemCheckedChanged: (Int, Boolean) -> Unit,
onAddButtonClick: () -> Unit,
onDialogDismissClick: () -> Unit,
onDialogOkClick: (String) -> Unit,
) {
Box {
LazyColumn(content = {
itemsIndexed(todos) { index, item ->
TodoListItem(item = item, onItemCheckedChanged, index)
if (index == todos.size - 1)
AddButton(onAddButtonClick)
}
})
if (isShowAddDialog) {
AddNewItemDialog(onDialogDismissClick, onDialogOkClick)
}
}
}
@Composable
private fun AddButton(
onAddButtonClick: () -> Unit,
) {
Box(modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier
.size(40.dp)
.align(Alignment.Center)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onAddButtonClick
))
}
}
@Composable
private fun AddNewItemDialog(
onDialogDismissClick: () -> Unit,
onDialogOkClick: (String) -> Unit,
) {
var text by remember { mutableStateOf("") }
AlertDialog(onDismissRequest = { },
text = {
TextField(
value = text,
onValueChange = { newText ->
text = newText
},
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Blue,
disabledIndicatorColor = Color.Blue,
unfocusedIndicatorColor = Color.Blue,
backgroundColor = Color.LightGray,
)
)
},
confirmButton = {
Button(
onClick = { onDialogOkClick(text) },
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue)
) {
Text(text = "Ok", style = TextStyle(color = Color.White, fontSize = 12.sp))
}
}, dismissButton = {
Button(
onClick = onDialogDismissClick,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue)
) {
Text(text = "Cancel", style = TextStyle(color = Color.White, fontSize = 12.sp))
}
}
)
}
@Composable
private fun TodoListItem(
item: Todo,
onItemCheckedChanged: (Int, Boolean) -> Unit,
index: Int,
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
colors = CheckboxDefaults.colors(Color.Blue),
checked = item.isChecked,
onCheckedChange = {
onItemCheckedChanged(index, !item.isChecked)
}
)
Text(
text = item.text,
modifier = Modifier.padding(start = 16.dp),
textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None,
style = TextStyle(
color = Color.Black,
fontSize = 14.sp
)
)
}
}
@Composable
private fun ContentWithProgress() {
Surface(color = Color.LightGray) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
ViewModel
internal class TodoViewModel :
BaseViewModel<TodoState, TodoEvent, TodoEffect>() {
private val repository: TodoRepository = TodoRepository()
init {
getTodo()
}
private fun getTodo() {
viewModelScope.launch {
val goodsList = repository.getTodoList()
sendEvent(TodoEvent.ShowData(goodsList))
}
}
override fun initialState(): TodoState = TodoState(isLoading = true)
override suspend fun handleEvent(event: TodoEvent, state: TodoState): TodoState? {
return when (event) {
is TodoEvent.AddNewItem -> {
val newList = state.goodsList.toMutableList()
newList.add(
index = state.goodsList.size,
element = Todo(false, event.text),
)
state.copy(
goodsList = newList,
isShowAddDialog = false
)
}
is TodoEvent.OnChangeDialogState -> state.copy(
isShowAddDialog = event.show
)
is TodoEvent.OnItemCheckedChanged -> {
val newList = state.goodsList.toMutableList()
newList[event.index] = newList[event.index].copy(isChecked = event.isChecked)
if (event.isChecked) {
sendEffect(TodoEffect.Completed(newList[event.index].text))
}
state.copy(goodsList = newList)
}
is TodoEvent.ShowData -> state.copy(isLoading = false, goodsList = event.items)
}
}
}
優(yōu)化
本來單次事件在LaunchedEffect里加載,但是會(huì)出現(xiàn)在 UI 在停止?fàn)顟B(tài)下依然收集新事件,并且每次寫LaunchedEffect比較麻煩,所以寫了一個(gè)擴(kuò)展:
@Composable
fun <S : UiState, E : UiEvent, F : UiEffect> BaseViewModel<S, E, F>.collectSideEffect(
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
sideEffect: (suspend (sideEffect: F) -> Unit),
) {
val sideEffectFlow = this.uiEffect
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(sideEffectFlow, lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(lifecycleState) {
sideEffectFlow.collect { sideEffect(it) }
}
}
}

以上就是Jetpack Compose 的新型架構(gòu) MVI使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Jetpack Compose 架構(gòu)MVI的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義網(wǎng)絡(luò)連接工具類HttpUtil
這篇文章主要介紹了Android自定義網(wǎng)絡(luò)連接工具類HttpUtil,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
android開發(fā)基礎(chǔ)教程—打電話發(fā)短信
打電話發(fā)短信的功能已經(jīng)離不開我們的生活了,記下來介紹打電話發(fā)短信的具體實(shí)現(xiàn)代碼,感興趣的朋友可以了解下2013-01-01
關(guān)于Android WebView的loadData方法的注意事項(xiàng)分析
本篇文章是對Android中WebView的loadData方法的注意事項(xiàng)進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
Android協(xié)程的7個(gè)重要知識點(diǎn)匯總
在現(xiàn)代Android應(yīng)用開發(fā)中,協(xié)程(Coroutine)已經(jīng)成為一種不可或缺的技術(shù),它不僅簡化了異步編程,還提供了許多強(qiáng)大的工具和功能,可以在高階場景中發(fā)揮出色的表現(xiàn),本文將深入探討Coroutine重要知識點(diǎn),幫助開發(fā)者更好地利用Coroutine來構(gòu)建高效的Android應(yīng)用2023-09-09

