对实现移动应用界面设计的思考

1. 屏幕

1.1. 屏幕的职责

现在的移动设备都使用触摸屏,触摸屏承担了两项职责:展示界面和处理用户操作指令。界面上展示的东西又可以分为内容和样式。比如展示一行大标题,标题文字是内容,字体、字号、颜色、背景色等等是样式。处理用户操作指令也可以分成接收指令和执行指令两部分。

代码1  屏幕的职责

屏幕 = 展示界面 + 处理用户操作指令 = (内容 + 样式) + (接收指令 + 执行指令)

对于简单的应用,这几部分可以放在一起,比如放到活动Activity或组合式函数Composable中。如果界面较为复杂,就需要将职责分配到不同的对象,让每个对象足够简单、清晰、可测试。换句话说,要分离关注点,重新分解并组合上面的式子。

1.2. 重新分解屏幕职责

代码2  重新分解屏幕职责

屏幕 = 内容 + (样式 + 接收指令) + 执行指令

内容是静态的,可以封装成被动对象(见《架构蓝图:软件架构4+1视图模型》的“视图之间的联系”部分)作为参数传递给组合式函数。样式和接收指令部分放在组合式函数中。处理指令部分可以抽象成一个接口,和内容一起传递给组合式函数。

代码3  屏幕代码示例

@Composable
fun MyScreen(displayState: DisplayState, actionHandler: ActionHandler) {Text(text = displayState.message,fontSize = 20.dp,modifier = Modifier.clickable { actionHandler.onAction(UserClickAction())})
}

可以看到组合式函数MyScreen的职责有两个:

  1. 将内容与样式关联起来。
  2. 将用户操作映射为指令,传递给指令处理函数。

同时另外两项工作是MyScreen不应该考虑的:

  1. 生产内容。
  2. 执行用户操作指令。

这样设计的组合式函数非常简单,没有复杂逻辑(分支、循环),很容易编码和测试。

2. 内容

2.1. 展示内容

内容自身是静态的,没有行为。随着用户输入数据的变化,以及用户发出新的操作指令,内容会发生变化,或者说会产生新版本。这类场景最适合使用Flow处理。我们让ViewModel返回内容流对象,让屏幕收集流,展示内容。

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val screenModel = MyScreenModel(MyRepository())val actionHandler = MyActionHandler()setContent {var displayState by remember { mutableStateOf(DisplayState()) }LaunchedEffect(true) {screenModel.displayState.collect { displayState = it }}MyScreen(displayState, actionHandler)}
}

2.2. 生成内容

现在考虑如何生成内容。内容包括什么呢?内容的来源通常有3个部分:本地数据(包括数据库、媒体文件、偏好设置等)、远程数据(比如发送HTTP请求获得的数据)、用户输入的数据。前两部分都是后端数据,可以通过仓库Repository提供统一的接口。具体做法后面会介绍。现在只需要考虑:

代码4  分解内容

内容 = 用户输入的数据 + 后端数据

直观的想法是建立用户输入数据流和后端数据流,通过Flow.combine()方法合并成内容流。这里有一个问题,两部分内容不是平等或独立的,用户输入的数据可能影响后端数据流,二者更像是流水线上前后两个步骤的关系:用户输入流的变化会改变后端数据流。搭建流水线要做两件事:

  1. 建立用户输入流。用户输入的变化,通过用户输入流进行通知。
  2. 通过flatMapLatest将用户输入流和后端数据流连接起来,产生内容流。
class MyScreenModel(private val repository: MyRepository) : ViewModel(), ActionHandler {private val input = MutableSharedFlow<InputState>(replay = 1)val displayState = input.flatMapLatest { inputState ->repository.queryLocalDatabase(inputState.username, viewModelScope).map { displayState(it, inputState) }}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DisplayState())init {onAction(InitializeAction())}fun displayState(backendEntity: BackendEntity, inputState: InputState): DisplayState {return DisplayState(/* ... */)}override fun onAction(action: Any, context: Context?) {var newInputState: InputState? = nullwhen (action) {is SomeAction -> newInputState = InputState("...")is InitializeAction -> newInputState = InputState("初始化")}newInputState?.let {viewModelScope.launch {input.emit(it)}}}
}

这个方法来自于[翻译]安卓开发者该如何解决ViewModel的Flow Collector泄漏问题?

3. 执行用户指令

前面我们已经把处理用户指令的过程分解为接收指令和执行指令。接收指令部分(将用户操作映射为指令)由组合式函数负责,现在只需要考虑执行指令。执行指令可能产生两个效果:改变用户输入数据,发送请求到后端。在这里,除了发送HTTP或RPC请求之外,对本地数据库或媒体文件的访问也当做发送请求到后端。第一个效果已经通过用户输入流处理了。所以只要考虑第二点。请求可以分为读请求和写请求。读写请求都可能更新内容流,因此相关职责要分配给持有内容流的ViewModel。读请求的处理相对简单,可以参考前面对用户输入流的处理。写请求可以分为新建对象、修改对象、删除对象3类。一些和业务流程有关的写请求,最后也可以归结到这3类之中。

首先考虑使用本地数据库作为后端。Room库为DAO提供了数据更新自动通知机制,因此新建、修改和删除这些操作产生的变化会通过数据流自动更新界面。不需要我们做额外的工作。

现在来看需要发送网络请求的情况。对于每个远程服务,可以在本地数据库中建立一个缓存表保存远程对象状态。每次远程请求成功后,将应答包含的对象信息写入缓存表。界面通过监听缓存表变动实现自动更新。

网络请求 -> 更新本地数据库 -> 更新数据流 -> 更新界面

这样在处理后端数据源时,不再需要区分本地数据库和远端服务,可以将本地数据库和远程服务封装成一个仓库Repository。业务代码只需依赖仓库,而不用关注背后实际的细节。

这里介绍一下新建远程对象的情况。新建对象时,本地已经拥有了新对象的全部业务属性。当然可能还要等待后端服务分配对象主键。此时后端服务可以返回新主键而非全部属性。仓库设置对象主键后插入本地数据库。这样可以减少网络请求成本。

4. 示例代码

package com.tommwq.roomdemoimport android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tommwq.roomdemo.ui.theme.RoomDemoTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterclass MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 也可以使用注入获得Repository和ScreenModel。val screenModel = MyScreenModel(MyRepository())setContent {RoomDemoTheme {Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {var displayState by remember { mutableStateOf(DisplayState()) }LaunchedEffect(true) {screenModel.displayState.collect { displayState = it }}MyScreen(displayState, screenModel)}}}}
}@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(displayState: DisplayState, actionHandler: ActionHandler) {Column {Text(text = displayState.message)TextField(value = displayState.username,onValueChange = {if (it != displayState.username) {actionHandler.onAction(ChangeUsernameAction(it))}})}
}/*** 输入状态。*/
data class InputState(val username: String, val changeTimes: Int = 0)/*** 显示状态。*/
data class DisplayState(val message: String = "", val username: String = "", val changeTimes: Int = 0)/*** 后端数据。*/
data class BackendEntity(val message: String)class MyRepository {fun queryLocalDatabase(username: String, scope: CoroutineScope): Flow<BackendEntity> {return flow {while (true) {val time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))emit(BackendEntity("$time Hello, $username"))delay(1000)}}.shareIn(scope, replay = 1, started = SharingStarted.WhileSubscribed())}fun invokeRemoteService(scope: CoroutineScope) {scope.async {// val data = invokeNetworkService()// saveToLocalDatabase(data)}}
}class InitializeAction
class InvokeRemoteServiceAction
data class ChangeUsernameAction(val username: String)/*** 用户操作处理器。*/
interface ActionHandler {/*** 处理用户操作,必要时更新状态。** @param action 用户操作* @param context 活动上下文*/fun onAction(action: Any, context: Context? = null)
}class MyScreenModel(private val repository: MyRepository) : ViewModel(), ActionHandler {private val input = MutableSharedFlow<InputState>(replay = 1)val displayState = input.flatMapLatest { inputState ->repository.queryLocalDatabase(inputState.username, viewModelScope).map { displayState(it, inputState) }}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DisplayState())init {onAction(InitializeAction())}/*** 根据后端数据和输入状态生成展示状态。* @param backendEntity 后端数据* @param inputState 输入状态*/fun displayState(backendEntity: BackendEntity, inputState: InputState): DisplayState {return DisplayState("时间 ${backendEntity.message} 修改次数 ${inputState.changeTimes}", inputState.username)}/*** 处理用户操作,必要时更新输入状态。** @param action 用户操作* @param context 活动上下文*/override fun onAction(action: Any, context: Context?) {var oldInputState = input.replayCache.lastOrNull()var newInputState: InputState? = nullwhen (action) {is ChangeUsernameAction -> newInputState = InputState(action.username, (oldInputState?.changeTimes ?: 0) + 1)is InitializeAction -> newInputState = InputState("用户")is InvokeRemoteServiceAction -> repository.invokeRemoteService(viewModelScope)}newInputState?.let {viewModelScope.launch {input.emit(it)}}}
}

5. 参考资料

  1. [翻译]安卓开发者该如何解决ViewModel的Flow Collector泄漏问题?[翻译]安卓开发者该如何解决ViewModel的Flow Collector泄漏问题? - 掘金
  2. Android开发中“真正”的仓库模式 Android开发中“真正”的仓库模式 - 掘金
  3. Room监听本地数据变化原理 Room监听本地数据变化原理 - 掘金
  4. 架构蓝图:软件架构4+1视图模型 https://blog.csdn.net/tq1086/article/details/132437666

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/700581.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 实现 ATR 指标计算(真实波幅):股票技术分析的利器系列(10)

Python 实现 ATR 指标计算&#xff08;真实波幅&#xff09;&#xff1a;股票技术分析的利器系列&#xff08;10&#xff09; 介绍算法解释 代码rolling函数介绍核心代码 完整代码 介绍 ATR&#xff08;真实波幅&#xff09;是一种技术指标&#xff0c;用于衡量市场波动性的程…

北航历届复试总结

政治题 科技兴国 英文翻译&#xff08;读一遍英文再翻译&#xff09; 随机抽一段计算机相关的英文论文&#xff0c;读一遍后翻译。建议全读完后再翻译&#xff0c;这样遇到不会的单词可以靠上下文猜一猜意思。之后如果你英语不强&#xff08;像我这种菜鸡&#xff09;&#xf…

【AHK】 MacOS复制粘贴习惯/MacOS转win键位使用习惯修改建议

自从转Mos后&#xff0c;转回win习惯又很不适应&#xff0c;怎么尽可能地降低两者的操作差异呢&#xff1f;通过AHK将大部分的Alt键位操作适当修改即可&#xff0c;特别是Home和End这类键&#xff0c;感觉是Mac的使用习惯更舒适 !c:: send,^c return!v:: send,^v return!x:: se…

content-type

content-type是什么 Content-Type 是 HTTP 协议中的一个头部字段&#xff08;Header Field&#xff09;&#xff0c;用于指示发送的实体数据的媒体类型&#xff08;Media Type&#xff09;。该字段用于告知服务器或客户端接收到的数据的内容类型&#xff0c;以便正确地处理和解…

如何获取 mysql 外键索引的元数据信息?

背景 对数据进行统一的管理处理&#xff0c;后续方便使用。 基本信息 见上一篇的处理。 select * from information_schema.KEY_COLUMN_USAGE where REFERENCED_TABLE_SCHEMA test \G;*************************** 1. row ***************************CONSTRAINT_CATALOG:…

Linux--ACL权限管理

一.ACL权限管理简介 ACL&#xff08;Access Control List&#xff0c;访问控制列表&#xff09;是一种文件权限管理机制&#xff0c;它提供了比传统的UGO&#xff08;用户、组、其他&#xff09;权限更灵活的权限设置方式。以下是ACL的一些主要功能&#xff1a; 针对特定用户或…

【Linux】docker构建环境编译运行linux内核

文章目录 1. 使用docker构建linux内核编译运行环境1.1. 为普通用户安装docker并验证是否安装成功1.1.1. 安装docker稳定版1.1.2. 启动docker1.1.3. 将当前用户加入docker用户组1.1.4. 验证docker是否安装成功 1.2. docker基本使用1.2.1. 列出所有镜像1.2.2. 查看当前所有容器的…

Vue3学习——路由

多级路由 routes:[{path:/about,component:About,},{path:/home,component:Home,children:[ //通过children配置子级路由{path:news, //此处一定不要写&#xff1a;/newscomponent:News},{path:message,//此处一定不要写&#xff1a;/messagecomponent:Message}]} ]命名路由 …

lower_bound详解

lower_bound是C标准模板库(STL)中的一个算法&#xff0c;用于在有序区间中查找第一个大于或等于给定值的元素的位置。这个函数非常有用&#xff0c;特别是当我们需要在有序数据集中进行二分查找时。下面是对lower_bound函数的详细讲解&#xff0c;包括其用法、原理、实现细节以…

pyqt如何实现拖拽打开文件(通过windows的快捷方式打开文件)

桌面端的开发中如何通过windows的快捷方式打开文件&#xff0c;那么如何将需要打开的数据传递给qt程序呢&#xff1f; 研究了一下发现很简单 通过sys.argv可以轻松的实现传参 sys.argv import sys print(sys.argv)这个方法可以获取系统传递给程序的参数&#xff0c;默认是个列…

企业型多域名SSL证书

多域名SSL证书是目前市场上用的比较多的一种&#xff0c;主要解决多个不同规则的域名申请&#xff0c;但不适合主域名&#xff08;根域名&#xff09;相同的域名&#xff0c;因为这种域名直接申请通配符。 企业型其实就是OV类型或者EV类型&#xff0c;由于在CA/B产品名称规范中…

pi(2)

上一次我们说到了这个程序 #include <iostream> #include <cmath> #include <limits> int continuedFractionTerm(int n) { if (n 0) return 1; if (n % 2 0) { return 2 * n 1; } else { return 2 * n; } } std::pair<int, int> be…

jetson nano——安装archiconda

目录 1.archiconda3我在这提供了下载链接&#xff0c;点解下面链接即可1.看好文件所在位置&#xff0c;如果装错了&#xff0c;那么环境变量的路径自己进行相应的修改。2.添加环境变量 2.可能部分伙伴输入一些激活&#xff0c;啥的命令激活不了&#xff0c;那么输入下面这些代码…

react18加antd新手上路使用

第一次使用react和antd组件库&#xff0c;记录过程中实用的几个组件和使用方法&#xff1b; 项目中依赖版本 "react": "^18.2.0", "antd": "^5.3.0",Input关闭历史填充 <Input placeholder"请输入ID/名称" allowClear a…

深入浅出JVM(八)之类加载器

前文已经描述Java源文件经过前端编译器后变成字节码文件&#xff0c;字节码文件通过类加载器的类加载机制在Java虚拟机中生成Class对象 前文深入浅出JVM&#xff08;六&#xff09;之前端编译过程与语法糖原理重点描述过编译的过程 前文深入浅出JVM&#xff08;三&#xff09…

算法训练营day35(补),动态规划3

func max(a, b int) int { if a > b { return a } return b } //343. 整数拆分 //拆分的数尽量相等才能保证最大 func integerBreak(n int) int { dp : make([]int, n1) // 初始值&#xff0c;0,1没有意义&#xff0c;为零&#xff0c;2可以拆为1*11 dp[2] 1 for i :…

视频基础学习二——图像深度与格式(RGB与YUV)

文章目录 前言一、图像深度1.什么是图像深度2.图像深度的意义3.常见的图像深度8位16位24位32位 二、图像格式1.RGB格式2.RGB样式2.YUVYUV的来由YUV样式RGB和YUV之间的转换YUV的常见类型 总结 前言 本文的目的是为了梳理音视频基础相关的知识&#xff0c;有很多做流媒体、音视频…

高级语言期末2010级A卷

1.编写函数&#xff0c;按照如下公式计算圆周率π的值&#xff08;精确到1e-5&#xff09; #include <stdio.h>double pai() {double last0;double flag1;int n1;while(flag-last>1e-5) {lastflag;flag*1.0*(2*n)*(2*n)/((2*n-1)*(2*n1));n;}return 2*last; }int main…

基于SpringBoot的停车场管理系统

基于SpringBootVue的停车场管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatis工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 前台首页 停车位 个人中心 管理员界面 摘要 摘要&#xff1a;随着城市化进程的…

C#,计算几何,计算机图形学(Computer Graphics)洪水填充算法(Flood Fill Algorithm)与源代码

1 泛洪填充算法(Flood Fill Algorithm) 泛洪填充算法(Flood Fill Algorithm) &#xff0c;又称洪水填充算法&#xff0c;是在很多图形绘制软件中常用的填充算法&#xff0c;最熟悉不过就是 windows 自带画图软件的油漆桶功能。 2 源程序 using System; using System.Collecti…