为Android构建现代应用——设计原则

为Android构建现代应用——设计原则 - 掘金     

state”是声明性观点的核心

在通过Compose或SwiftUI等框架设计声明性视图时,我们必须明确的第一个范式是State(状态)。UI组件结合了它的图形表示(View)和它的State(状态)。UI组件中发生变化的任何属性或数据都可以表示为状态。例如,在TextField类型的UI组件中,用户输入的文本是一个可以更改的变量;因此,value是一个可以表示为状态(name)的变量,如下面的代码片所示。

   TextField(
          label = { Text("User name") },
          value = name,
          onValueChange = onNameChange
       )


声明性View的层次结构:

 

移动应用程序屏幕可以包含View层次结构,如上图所示。每个View依次可以包含多个State变量。例如,图中的所有View都有一个State。
包含或依赖于State的View称为Stateful View(有状态视图),没有State依赖的View称为Stateless View(无状态视图)。Google和Apple都建议尽可能设计无状态视图,因为使用这种类型有以下优点:
       1.你可以重用它们
       2.它们允许你将state管理委托给其他组件
       3.它们是功能性的,避免了副作用

根据这些建议,设计应该面向Stateless views(无状态视图),并将那些Stateful View(有状态视图)转换为Stateless views(无状态视图)。
那么,如何实现这的呢?

将"State hoisting"应用委托于states

状态提升是一种将Stateful View(有状态视图)转换为Stateless View(无状态视图)的技术。这是通过控制反转实现的,如下代码:

//这是一个Stateful View(有状态视图)
@Composable
fun text1(){
    var name by remember{ mutableStateOf("")}
    var phone by remember{mutableStateOf("")}
    ContactInformation1(
        name = name,
        nNameChange = {name=it},
        phone = phone,
        onPhoneChange ={phone=it} )

}

//这是一个Stateless View(无状态视图)
@Composable
fun ContactInformation1(name:String,onNameChange:(String)->Unit,phone:String,onPhoneChange:(String)->Unit){
    Column(
        modifier = Modifier.fillMaxSize().padding(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally){
        TextField(
            label={Text(text = "User name")}, 
            value = name, 
            onValueChange = onNameChange)
        Spacer(Modifier.padding(5.dp))
        TextField(
            label={ Text(text = "Phone number")}, 
            value = phone,
            onValueChange = onPhoneChange)
        Spacer(modifier = Modifier.padding(5.dp))
        Button(onClick = { println("Order generated for $name and phone $phone")},){
            Text(text = "Pay order")
        }
    }
}


在这个代码中,name和phone的状态控件被委托给text1(),因此ContactInformation1()不关心他的数据状态,可以被其他view重用。
text1()变为Stateful(有状态),ContactInformation1变为Stateless(无状态)。

//有状态视图
@Preview
@Composable
fun OrderSoreen(){
    //states name and phone
    var name by remember {mutableStateOf("")}
    var phone by remember { mutableStateOf("") }
    ContaotInformation(name=name, onNameChange = {name=it},phone=phone, onPhoneChange = {phone=it}){}
}
//无状态视图
@Composable
fun ContaotInformation(
    name:String,
    onNameChange:(String)->Unit,
    phone:String,
    onPhoneChange:(String)->Unit,
    payOrder:()->Unit) {
    
}

在上面代码中,控制的反转是通过高阶函数实现的,允许状态和操作的定义作为参数传递给ContaotInformation视图。

定义“真实数据源”,谁负责提供状态呢?

首先,让我们理清“真实数据源”这个词是什么?
真实数据源指得是提供视图需要呈现在屏幕上得数据得可靠来源,并且用户将与这些数据进行交互。
在我们得分析中,数据与状态密切相关。视图使用状态来接收完成其工作所需的信息(数据)。
在上图中,我们看到了如何在各自的视图中找到状态。这意味这上述图中的每个视图都是真实的数据源。甚至我们之前讨论过的UI TextField组件的变量名也可以是一个状态,因此,它也是一个事实数据源。

在一个视图层次中有这么多的真实源是否合理?

答案是不合理的。

建议将真实的数据源限制在单个组件(或者尽可能少),这样你就可以对流有更大哦的控制,并避免状态不一致。
拥有一个单一的,明确定义的真实数据源也有助于正确实现单向数据流设计模式,这是由声明性视图(如Compose或SwiftUI)推广的模式。

如何在我们的设计中减少真实数据源的数量呢?

这可以通过上面解释的状态提升技术减少有状态视图的数量,并将状态集中在一个视图中。一般来说,委托是层次基本最高的视图,即父视图。
如下图:只有一个真实数据源,那就是父视图。
一方面,子视图只负责传播与用户交互接收到的事件。另一方面,他们接收到渲染视图的状态(重组),以反映UI的变化。

图片1.png

 

除了将所有状态处理责任委托给一个视图,还有其他选择吗?

答案是肯定的。 

更好的选择是将这个责任委托给一个状态持有者或者承担这个角色的ViewModel。我们在下一节中看到更多的细节。

ViewModel作为真实数据源 为了防止视图被责任压得喘不过气来,另一个组件被召唤来处理状态管理。这个适当的元素就是我们熟知的ViewModel。 如下图所示,将状态从View移动到ViewModel可以创建责任分离,使得展示逻辑和其对状态的影响可以集中化。

将状态处理委托给一个ViewModel图:

 

图片2.jpg

 

尽管在实现中这个组件(ViewModel)是可选的,但我强烈建议使用它,因为它提供了许多优点,如有效管理数据和视图之间的生命周期。关于这个架构组件的更多信息,我建议查阅关于ViewModels的官方Google文档。视图和ViewModel之间的通信只包括两种类型的消息,事件和状态:

1.事件是由任何视图或子视图通知给ViewModel的动作,作为用户与UI组件交互的结果。

2.状态代表ViewModel交付给视图进行各自图形解释的信息(数据)。

3.ViewModel的主要功能是接收来自视图发送的事件,解释它们,应用业务逻辑,并将它们转化为状态,以便回传给视图。

4.视图的任务是接收由ViewModel发送的状态,并通过重组将它们转化为图形UI表示。

5.现在,对于每个组件的责任以及它们之间的消息有了更清晰的认识,让我们现在分析一下信息流的情况。


理解数据流,“单向数据流模式”

如果我们简化图上中的图,结果会使得下面的图:

单向数据流:

图片3.jpg

 

这是视图和ViewModel之间的循环消息。信息流只遵循一个方向,因此得名单向数据流模式。

可以将事件注入循环的外部因素是用户交互,如列表中的滚动,按钮上的点击,以及与其他应用层的交互,如来自仓库的响应或用户的响应,后台计时器,或者可能是推送通知的到达。

这个循环不能被中断,因为任何诱发的中断或延迟都会导致用户体验差。用户会感觉到应用程序慢,被阻塞,质量差。 因此,设计时应尽可能考虑以下规则:
-   定义视图的可组合项必须是幂等的和功能性的。
-   在视图端,不能有任何拖慢循环的任务。任何需要大量处理的任务都必须委托给ViewModel,它将通过反应式编程和Flow Coroutines异步执行这些任务。

现在你对数据流和View和ViewModel之间交换的消息有了更好的理解,那么一个合乎逻辑的问题是:

View和ViewModel之间的通信渠道是如何实现的?

我们接下来看看。

让我们连接View和ViewModel组件 如图所示,需要实现的两种类型的通信渠道已经清晰地标识出来。 

第一个通道是事件通道,方向是View –> ViewModel。 

对于这个实现,只需要ViewModel公开可以被View调用的公共操作,如下面的代码片段所示。

 //UI's Events

       fun onNameChange(): (String) -> Unit = {

        name = it

        }

        fun onPhoneChange(): (String) -> Unit = {

        phone = it

        }


第二个通道是状态通道,方向是 ViewModel –> View。

UI如何知道状态已经改变呢?

观察状态。要追踪状态,首先,ViewModel必须通过mutableStateOf组件将它们暴露给UI,如下所示:
 

   // UI's states

        var name by mutableStateOf("")

        private set

        var phone by mutableStateOf("")

        private set

mutableStateOf不仅允许将状态暴露给视图,而且还允许视图订阅以接收该状态的任何更改的通知。

让我们看看ViewModel和View(Composable)的完整实现

viewModel:

class OrderViewModel1: ViewModel() {
    //UI's states
    var name by mutableStateOf("")
    private set
    var phone by mutableStateOf("")
    private set

    //UI's Events
    fun onNameChange():(String) -> Unit ={
        name =it
    }

    fun onPhoneChange():(String)->Unit ={
        phone =it
    }

    fun payOrder():()->Unit ={
        println("Order generated for $name and phone $phone")
    }
}

View(Composables):

@Preview
@Composable
fun OrderScreen(viewModel:OrderViewModel1 = viewModel()){
    ContactInformation2(
        name = viewModel.name,
        onNameChange = viewModel.onNameChange(),
        phone = viewModel.phone,
        onPhoneChange = viewModel.onPhoneChange(),
        payOrder = viewModel.payOrder()
    )
}

@Composable
fun ContactInformation2(
    name:String,
    onNameChange:(String)->Unit,
    phone:String,
    onPhoneChange:(String)->Unit,
    payOrder:()->Unit) {
    Column(modifier = Modifier
        .fillMaxSize()
        .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally){
        TextField(label = { Text(text = "User name")}, value = name, onValueChange =onNameChange )
        Spacer(modifier = Modifier.padding(5.dp))
        TextField(label = { Text(text = "phone number")}, value =phone , onValueChange =onPhoneChange )
        Spacer(modifier = Modifier.padding(5.dp))
        Button(onClick = payOrder){
            Text(text = "Pay order")
        }
    }
}

到目前为止,我们已经看到,像名字和电话这样的状态是一个字符串变量的表示;也就是说,状态代表一个原始变量。然而,我们可以将状态表示扩展到组件和屏幕。

在下一节中,我们将查看表示状态的其他选项。

被表示为状态的结构

在Compose和一般的声明式视图中,状态可以表示不同类型的UI结构:

由状态表示的结构:

image.png

 


-   属性UI的状态:它们是以状态表示的原始变量。在图中,如名字、电话或地址等文本输入字段就是这种类型。
-   组件UI的状态:代表与组合相关联的UI元素的状态。例如,在OrderScreen上,一个叫做ContactInformationForm的组件可以组合所需的数据,如联系信息。这个组件可能有NameValueChanged、PhoneValueChanged和SuccessValidated的状态。
-   屏幕UI的状态:它代表与一个可以被视为绝对和独立状态的屏幕相关联的状态;例如,一个叫做OrderScreen的屏幕可能有以下状态:加载中、成功加载或加载失败。

现在,让我们看看在Android和Kotlin中存在哪些实现选项来定义这些状态。

属性UI的状态

它们是从原始类型变量(如String、Boolean、List或Int等)声明的状态。

如果它在ViewModel中声明(ViewModel作为真实数据源),其定义可能是这样的:

var name by mutableStateOf("")
private set

var phone by mutableStateOf("")
private set

var address by mutableStateOf("")
private set

var payEnable by mutableStateOf(false)
private set

如果它在View中声明(View作为真实数据源),它在Composable中的定义可能是这样的:

var name by remember { mutableStateOf("") }

  var phone by remember { mutableStateOf("") }

  var address by remember { mutableStateOf("") }

 var payEnable by remember { mutableStateOf(false) }

remember是一个Composable,它允许你在重新组合时临时保持变量的状态。因为它是一个Composable,所以这个属性只能在声明式视图中定义,也就是在Composable函数中。

请始终记住,要通过"by"关键字使用委托,你需要导入:

import androidx.compose.runtime.getValue 
import androidx.compose.runtime.setValue

在前面的例子中,我们只讨论了通过使用mutableStateOf组件来表示属性或变量的状态。

然而,也可能数据流可以被表示为状态并被Composables观察。这些额外的选项与Flow、LiveData或RxJava有关。在“实现‘特性’”中,我们将看到使用StateFlow的几个例子。

组件UI的状态

当你有一组相互关联的UI元素时,他们的状态可以被组织到一个单一的结构或者UI组件和一个单一的状态中。

例如,在前面的图中,元素User name、Phone number、Address,甚至Pay Order按钮可以被组织到一个单一的UI组件中,并且其状态在一个叫做FormUiState的单一状态中表示。

// 定义 FormUiState 数据类
data class FormUiState(
    val nameValueChanged: String = "",
    val phoneValueChanged: String = "",
    val addressValueChanged: String = ""
)

// 定义 FormUiState 的扩展属性
val FormUiState.successValidated: Boolean
    get() = nameValueChanged.length > 1 && phoneValueChanged.length > 3


在这种情况下,将多个状态建模到一个合并的状态类中效果非常好,因为这些变量是相关的,甚至定义了其他变量的值。例如,这就发生在 `successValidated` 变量上,它依赖于 `nameValueChanged` 和 `phoneValueChanged` 变量。

合并状态对实现带来了好处,集中了控制,整理了代码。这将是我们在实现中最常用的技术。

**屏幕UI的状态**

如果需要建模的状态可以是独立的,并且是同一家族的一部分,你可以使用以下定义:

sealed class OrderScreenUiState {
    data class Success(val order:Order):OrderScreenUiState()
    data class Failed(val message:String):OrderScreenUiState()
    object Loading:OrderScreenUiState()
}


这种实现方式适合处理绝对和排他的状态;你有一个状态或另一个状态,但不会同时有两种状态。

通常,像OnboardignScreen或ResultScreen这样的简单屏幕可以用这些状态进行建模。

当屏幕更复杂并且包含许多独立操作且有多种关系的UI元素时,建议优先选择使用属性UI状态和组件UI状态技术来定义状态

建模和分组事件

回到OrderScreen的例子,我们现在将看一下如何建模Events,并如何类似于States地将它们分组。

考虑一个下图所示的屏幕:

多次事件:

image.png

 

ViewModel向视图暴露四个操作(事件),每个操作被一个视图UI元素使用。

分析这四个事件,它们与输入用户联系信息的表单相关,所以将它们分组到一个事件类型中是有意义的,如下图所示:

 组合事件:

image.png

 

表示不同类型事件的实现可能是这样的:

sealed class ContactFormEvent {
    data class onNameChange(val name:String):FormUiEvent()
    data class onPhoneChange(val phone:String):FormUiEvent()
    data class onAddressChange(val address:String):FormUiEvent()
    object PayOrder:FormUiEvent()
}


最后,你不必在简化状态或事件时过于严格。需要分析每种用法的优点和缺点,并做出相应的决策。

对于那些相关的UI组件,将它们分组是很有意义的;一些其他的横切元素将更健康地保持它们的独立性。

总结

在这第一章中,我们回顾了在现代Android应用开发中使用的主要概念。

像状态和事件,状态提升,真实数据源,和单向数据流这样的概念在实现Jetpack Compose,ViewModels,和其他可用于Android的架构组件之前是必须理解的。这就是为什么我们在这第一章就开始讲这些概念的原因。

在接下来的章节中,我们进入移动应用中的架构和设计的定义,为此我们将使用本章介绍的概念作为参考。

稍后,我们将使用电子商务作为概念来实现一个名为“Order Now”的移动应用。这个应用将具有电子商务的主要部分,如购物车,产品列表,和结账过程。

这项工作将引导读者接触到接近真实和生产应用的设计和开发经验。

但首先,我们将应用这一章学到的概念来实现一个简单的表单。

这将是下一章所描述的主题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RuoYi-VUE : make sure to provide the “name“ option

前言 略 错误 错误原因 theme-picker 组件未被注册。 解决 src/App.vue代码恢复成若依的代码即可。&#xff08;PS&#xff1a;不知道代码被谁修改了&#xff09; 缺少这一段&#xff1a; <script> import ThemePicker from "/components/ThemePicker";…

hive基础

目录 DDL&#xff08;data definition language&#xff09; 创建数据库 创建表 hive中数据类型 create table as select建表 create table like语法 修改表名 修改列 更新列 替换列 清空表 关系运算符 聚合函数 字符串函数 substring:截取字符串 replace :替换…

C进阶:内存操作函数

内存操作函数 memcpy 头文件&#xff1a;string.h 基本用途&#xff1a;进行不相关&#xff08;不重叠的内存&#xff09;拷贝。 函数原型&#xff1a;void* memcpy(void* destination,//指向目标数据的指针 const void* source,//指向被拷贝数据的指针 size_t num);//拷贝的数…

分布式光伏电站监控及集中运维管理-安科瑞黄安南

前言&#xff1a;今年以来&#xff0c;在政策利好推动下光伏、风力发电、电化学储能及抽水蓄能等新能源行业发展迅速&#xff0c;装机容量均大幅度增长&#xff0c;新能源发电已经成为新型电力系统重要的组成部分&#xff0c;同时这也导致新型电力系统比传统的电力系统更为复杂…

【数据挖掘】时间序列的傅里叶变换:用numpy解释的快速卷积

一、说明 本篇告诉大家一个高级数学模型&#xff0c;即傅里叶模型的使用&#xff1b; 当今&#xff0c;傅里叶变换及其所有变体构成了我们现代世界的基础&#xff0c;为压缩、通信、图像处理等技术提供了动力。我们从根源上理解&#xff0c;从根本上应用&#xff0c;这是值得付…

HTML5——基础知识及使用

HTML 文件基本结构 <html><head><title>第一个页面</title></head><body>hello world</body> </html> html 标签是整个 html 文件的根标签(最顶层标签).head 标签中写页面的属性.body 标签中写的是页面上显示的内容.title 标…

实现外部缓存-Redis

目录 实现 RedisTemplate RedisTemplate的序列化 RedisSerializer 创建Redis缓存配置类 测试使用 创建配置类 创建注解测试实体 创建配置文件 创建单元测试类进行测试 实现 RedisTemplate XXXTemplate 是 Spring 的一大设计特色&#xff0c;其中&#xff0c;RedisTe…

Mybatis操作数据库执行流程的先后顺序是怎样的?

MyBatis是一个支持普通SQL查询、存储及高级映射的持久层框架&#xff0c;它几乎消除了JDBC的冗余代码。使Java开发人员可以使用面向对象的编程思想来操作数据库。对于MyBatis的工作原理和操作流程的理解&#xff0c;我们先来看下面的工作流程图。 MaBatis的工作流程 在上图中…

element的el-upload实现多个图片上传以及预览与删除

<el-form-itemlabel"实验室照片:"prop"labUrlList"v-if"ruleForm.labHave"><el-upload:action"urlUpload":headers"loadHeader"list-type"picture-card":file-list"ruleForm.labUrlList"class…

【el-tree查询并高亮】vue使用el-tree组件,搜索展开并选中对应节点,高亮搜索的关键字,过滤后高亮关键字,两种方法

第一种&#xff08;直接展开并高亮关键字&#xff09; 效果图这样的&#xff0c;会把所有的有这些关键字的节点都展开 代码&#xff1a; 这里的逻辑就是通过递归循环把所有和关键字匹配的节点筛选出来 然后通过setCheckedKeys方法把他展开选中 然后通过filterReal把关键字高亮…

数据库redis作业

数据库redis作业 redis9种数据类型的基本操作 redis持久化&#xff1a;分别启用rdb和aof&#xff0c;并查看是否有对应文件生成 作业1&#xff1a;redis9种数据类型的基本操作 1、key操作 key * #查询所有的key keys *exists 参数 #参数&#xff1a;key #判断该key是否存…

【ADS】ADS复制原理图或版图到另一个工程

直接Ctrl CCtrl V无法粘贴 可以先导入要复制的工程 加入工程&#xff0c;复制完后在勾掉工程

单独在文件中打开allure生成的index.html报告时却显示为loading

【问题描述】&#xff1a;单独在文件中打开allure生成的index.html报告时显示为loading&#xff0c;如下图&#xff1a; 【问题定位】&#xff1a;其实在allure-report下index.html文件是不能直接打开的&#xff0c;出现页面都是loading的情况&#xff0c;这是因为直接allure报…

Rust 数据类型 之 类C枚举 c-like enum

目录 枚举类型 enum 定义和声明 例1&#xff1a;Color 枚举 例2&#xff1a;Direction 枚举 例3&#xff1a;Weekday 枚举 类C枚举 C-like 打印输出 强制转成整数 例1&#xff1a;Weekday 枚举 例2&#xff1a;HttpStatus 枚举 例3&#xff1a;Color 枚举 模式匹配…

使用 Docker 快速上手官方版 LLaMA2 开源大模型

本篇文章&#xff0c;我们聊聊如何使用 Docker 容器快速上手 Meta AI 出品的 LLaMA2 开源大模型。 写在前面 昨天特别忙&#xff0c;早晨申请完 LLaMA2 模型下载权限后&#xff0c;直到晚上才顾上折腾了一个 Docker 容器运行方案&#xff0c;都没来得及写文章来聊聊这个容器怎…

wxchart 小程序 线条图不显示y轴的网格线 (分割线)

如下图&#xff1a;项目需求不显示包括x轴的6条灰色分割线。 分析&#xff1a; 看了一下源码已经写死了是5条分割线&#xff0c;加一条x轴刻度线。没给公开配置方法。 解决方案&#xff1a; 既然没有配置项目&#xff0c;可以转变思路&#xff0c;把这些线条配置成白色&…

Spring整合Mybatis原理

首先介绍一下Mybatis的工作原理 先简略的放两张图&#xff0c;后面的知识结合这两张图比较好理解 Mybatis的基本工作原理 在 Mybatis 中&#xff0c;我们可以使用⼀个接口去定义要执行sql&#xff0c;简化代码如下&#xff1a; 定义⼀个接口&#xff0c;Select 表示要执行查询…

【UniApp开发小程序】”我的“界面实现+“信息修改“界面实现+登出账号实现+图片上传组件【基于若依管理系统开发】

文章目录 界面实现界面效果我的修改信息 “我的”界面实现api页面退出账号让自我介绍只显示一行&#xff0c;结尾多余的字使用...代替跳转到信息修改页面 信息修改界面实现api页面动态给对象设置属性名和值修改密码图片上传组件 部分后端代码Controller 界面实现 界面效果 我…

Windows11的VTK安装:VS201x+Qt5/Qt6 +VTK7.1/VTK9.2.6

需要提前安装好VS2017和VS2019和Qt VS开发控件以及Qt VS-addin。 注意Qt6.2.4只能跟VTK9.2.6联合编译&#xff08;目前VTK9和Qt6的相互支持版本&#xff09;。 首先下载VTK&#xff0c;需要下载源码和data&#xff1a; Download | VTKhttps://vtk.org/download/ 然后这两个文…

1 请使用js、css、html技术实现以下页面,表格内容根据查询条件动态变化。

1.1 创建css文件&#xff0c;用于编辑style 注意&#xff1a; 1.背景颜色用ppt的取色器来获取&#xff1a; 先点击ppt的形状轮廓&#xff0c;然后点击取色器&#xff0c;吸颜色&#xff0c;然后再点击形状轮廓的其他轮廓颜色&#xff0c;即可获取到对应颜色。 2.表格间的灰色线…