我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Jetpack Compose 的新型架构 MVI使用详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Jetpack Compose 的新型架构 MVI使用详解

为什么是MVI而不是MVVM

MVVM作为流行的架构模式,应用在 Compose上,并没有大的问题或者设计缺陷。但是在使用期间,发现了并不适合我的地方,或者说是使用起来不顺手的地方:

  • 数据观察者过多:如果界面有多个状态,就要多个 LiveData 或者 Flow,维护麻烦。
  • 更新 UI 状态的来源过多:数据观察者多,并行或同时更新 UI,造成不必要的重绘。
  • 大量订阅观察者函数,也没有约束:存储和更新没有分离,容易混乱,代码臃肿。

单向数据流

单向数据流 (UDF) 是一种设计模式,在该模式下状态向下流动,事件向上流动。通过采用单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分分离开来。

使用单向数据流的应用的界面更新循环如下所示:

  • 事件:界面的某一部分生成一个事件,并将其向上传递,例如将按钮点击传递给 ViewModel 进行处理;或者从应用的其他层传递事件,如指示用户会话已过期。
  • 更新状态:事件处理脚本可能会更改状态。
  • 显示状态:状态容器向下传递状态,界面显示此状态。

以上是官方对单向数据流的介绍。下面介绍适合单项数据流的架构 MVI。

MVI

MVI 包含三部分,ModelViewIntent

  • Model 表示 UI 的状态,例如加载和数据。
  • View 根据状态展示对应 UI。
  • Intent 代表用户与 UI 交互时的意图。例如点击一个按钮提交数据。

可以看出 MVI 完美的符合官方推荐架构 ,我们引用 React Redux 的概念分而治之:

  • State 需要展示的状态,对应 UI 需要的数据。
  • Event 来自用户和系统的是事件,也可以说是命令。
  • Effect 单次状态,即不是持久状态,类似于 EventBus ,例如加载错误提示出错、或者跳转到登录页,它们只执行一次,通常在 Compose 的副作用中使用。

实现

首先我们需要约束类型的接口:

interface UiState
interface UiEvent
interface UiEffect

然后创建抽象的 ViewModel :

abstract class BaseViewModel< S : UiState, E : UiEvent,F : UiEffect>  : ViewModel() {}

对于状态的处理,我们使用StateFlowStateFlow就像LiveData但具有初始值,所以需要一个初始状态。这也是一种SharedFlow.我们总是希望在 UI 变得可见时接收最后一个视图状态。为什么不使用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 处理事件,更新状态:

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

使用

接下来实现一个 Todo 应用,打开应用获取历史任务,点击加号增加一条新的任务,完成任务后后 Toast 提示。

首先分析都有哪些状态:

  • 是否在加载历史任务
  • 是否添加新任务
  • 任务列表

创建约束类:

internal data class TodoState(
    val isShowAddDialog: Boolean=false,
    val isLoading: Boolean = false,
    val goodsList: List<Todo> = listOf(),
) : UiState

然后分析有哪些意图:

  • 加载任务(进入自动加载,所以省略)
  • 显示任务
  • 加载框的显示隐藏
  • 添加新任务
  • 完成任务
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
}

而单一事件就一种,完成任务时候的提示:

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

优化

本来单次事件在LaunchedEffect里加载,但是会出现在 UI 在停止状态下依然收集新事件,并且每次写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 的新型架构 MVI使用详解的详细内容,更多关于Jetpack Compose 架构MVI的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Jetpack Compose 的新型架构 MVI使用详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

详解Python手写数字识别模型的构建与使用

这篇文章主要为大家详细介绍了Python中手写数字识别模型的构建与使用,文中的示例代码简洁易懂,对我们学习Python有一定的帮助,需要的可以参考一下
2022-12-22

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录