欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Jetpack?Compose?的新型架構(gòu)?MVI使用詳解

 更新時間:2022年09月20日 08:38:52   作者:艾維碼  
這篇文章主要介紹了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 包含三部分,ModelViewIntent

  • 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)的處理,我們使用StateFlowStateFlow就像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 Studio如何修改字體的大小

    Android Studio如何修改字體的大小

    這篇文章主要介紹了Android Studio如何修改字體的大小的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • Android自定義網(wǎng)絡(luò)連接工具類HttpUtil

    Android自定義網(wǎng)絡(luò)連接工具類HttpUtil

    這篇文章主要介紹了Android自定義網(wǎng)絡(luò)連接工具類HttpUtil,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-11-11
  • android開發(fā)基礎(chǔ)教程—打電話發(fā)短信

    android開發(fā)基礎(chǔ)教程—打電話發(fā)短信

    打電話發(fā)短信的功能已經(jīng)離不開我們的生活了,記下來介紹打電話發(fā)短信的具體實現(xiàn)代碼,感興趣的朋友可以了解下
    2013-01-01
  • Android  TextView中部分文字高亮顯示

    Android TextView中部分文字高亮顯示

    這篇文章主要介紹了Android TextView中部分文字高亮顯示的相關(guān)資料,需要的朋友可以參考下
    2017-07-07
  • 關(guān)于Android WebView的loadData方法的注意事項分析

    關(guān)于Android WebView的loadData方法的注意事項分析

    本篇文章是對Android中WebView的loadData方法的注意事項進行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-06-06
  • Mac Android Studio安裝圖文教程

    Mac Android Studio安裝圖文教程

    本文主要介紹了Mac Android Studio安裝教程,文中通過圖文代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • Android context源碼詳解及深入分析

    Android context源碼詳解及深入分析

    這篇文章主要介紹了Android context源碼詳解及深入分析的相關(guān)資料,這里對Android Context 如何使用進行了詳細(xì)介紹,需要的朋友可以參考下
    2017-01-01
  • Android使用文件進行IPC

    Android使用文件進行IPC

    這篇文章主要為大家詳細(xì)介紹了Android使用文件進行IPC,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • android實現(xiàn)背景平鋪的三種方法

    android實現(xiàn)背景平鋪的三種方法

    這篇文章主要介紹了Android的圖片平鋪效果的實現(xiàn)方法,主要有使用系統(tǒng)API、使用XML配置、自定義繪制三種方法,需要的朋友可以參考下
    2014-02-02
  • Android協(xié)程的7個重要知識點匯總

    Android協(xié)程的7個重要知識點匯總

    在現(xiàn)代Android應(yīng)用開發(fā)中,協(xié)程(Coroutine)已經(jīng)成為一種不可或缺的技術(shù),它不僅簡化了異步編程,還提供了許多強大的工具和功能,可以在高階場景中發(fā)揮出色的表現(xiàn),本文將深入探討Coroutine重要知識點,幫助開發(fā)者更好地利用Coroutine來構(gòu)建高效的Android應(yīng)用
    2023-09-09

最新評論