Jetpack?Compose?的新型架構(gòu)?MVI使用詳解
為什么是MVI而不是MVVM
MVVM作為流行的架構(gòu)模式,應(yīng)用在 Compose上,并沒有大的問題或者設(shè)計缺陷。但是在使用期間,發(fā)現(xiàn)了并不適合我的地方,或者說是使用起來不順手的地方:
- 數(shù)據(jù)觀察者過多:如果界面有多個狀態(tài),就要多個 LiveData 或者 Flow,維護麻煩。
- 更新 UI 狀態(tài)的來源過多:數(shù)據(jù)觀察者多,并行或同時更新 UI,造成不必要的重繪。
- 大量訂閱觀察者函數(shù),也沒有約束:存儲和更新沒有分離,容易混亂,代碼臃腫。
單向數(shù)據(jù)流
單向數(shù)據(jù)流 (UDF) 是一種設(shè)計模式,在該模式下狀態(tài)向下流動,事件向上流動。通過采用單向數(shù)據(jù)流,您可以將在界面中顯示狀態(tài)的可組合項與應(yīng)用中存儲和更改狀態(tài)的部分分離開來。
使用單向數(shù)據(jù)流的應(yīng)用的界面更新循環(huán)如下所示:
- 事件:界面的某一部分生成一個事件,并將其向上傳遞,例如將按鈕點擊傳遞給 ViewModel 進行處理;或者從應(yīng)用的其他層傳遞事件,如指示用戶會話已過期。
- 更新狀態(tài):事件處理腳本可能會更改狀態(tài)。
- 顯示狀態(tài):狀態(tài)容器向下傳遞狀態(tài),界面顯示此狀態(tài)。
以上是官方對單向數(shù)據(jù)流的介紹。下面介紹適合單項數(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ù)據(jù)。
可以看出 MVI 完美的符合官方推薦架構(gòu) ,我們引用 React Redux 的概念分而治之:
- State 需要展示的狀態(tài),對應(yīng) UI 需要的數(shù)據(jù)。
- Event 來自用戶和系統(tǒng)的是事件,也可以說是命令。
- Effect 單次狀態(tài),即不是持久狀態(tài),類似于 EventBus ,例如加載錯誤提示出錯、或者跳轉(zhuǎn)到登錄頁,它們只執(zhí)行一次,通常在 Compose 的副作用中使用。
實現(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
但具有初始值,所以需要一個初始狀態(tài)。這也是一種SharedFlow.
我們總是希望在 UI 變得可見時接收最后一個視圖狀態(tài)。為什么不使用MutableState
,因為Flow
的api和操作符十分強大。
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) } }
使用
接下來實現(xiàn)一個 Todo 應(yīng)用,打開應(yīng)用獲取歷史任務(wù),點擊加號增加一條新的任務(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ù)(進入自動加載,所以省略)
- 顯示任務(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ù)時候的提示:
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
里加載,但是會出現(xiàn)在 UI 在停止?fàn)顟B(tài)下依然收集新事件,并且每次寫LaunchedEffect
比較麻煩,所以寫了一個擴展:
@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,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11android開發(fā)基礎(chǔ)教程—打電話發(fā)短信
打電話發(fā)短信的功能已經(jīng)離不開我們的生活了,記下來介紹打電話發(fā)短信的具體實現(xiàn)代碼,感興趣的朋友可以了解下2013-01-01關(guān)于Android WebView的loadData方法的注意事項分析
本篇文章是對Android中WebView的loadData方法的注意事項進行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06