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

news2024/11/24 0:58:14

为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”的移动应用。这个应用将具有电子商务的主要部分,如购物车,产品列表,和结账过程。

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

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

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

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

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

相关文章

Kotlin~Observer观察者模式

概念 定义一对多的依赖关系,让多个观察者同时监听一个主题对象。 角色介绍 Subject:主题,也称被观察者,它是具有状态的对象维护着一个观察者列表。提供添加、删除和通知观察者的方法。ConcreteSubject:具体主题&…

mfc140.dll丢失的解决方法(最新解决方法)

一:mfc140.dll的作用: mfc140.dll的主要作用是提供了(简称MFC)的函数和资源,它是用于构建Windows应用程序的一组C类库。MFC是微软开发环境中提供的一个工具集,它封装了Windows操作系统的底层API&#xff0…

2.多线程-初阶(中)

文章目录 4. 多线程带来的的风险-线程安全 (重点)4.1 观察线程不安全4.2 线程安全的概念4.3 线程不安全的原因4.3.1原子性4.3.2可见性4.3.3代码顺序性 4.4 解决之前的线程不安全问题 5. synchronized[ˈsɪŋkrənaɪzd] 关键字-监视器锁monitor lock5.1 synchronized 的特性5.…

【限流】4 种常见的限流实现方案

在微服务应用中,考虑到技术栈的组合,团队人员的开发水平,以及易维护性等因素,一个比较通用的做法是,利用 AOP 技术 自定义注解实现 对特定的方法或接口进行限流。 下面基于这个思路来分别介绍下几种常用的限流方案的…

OceanBase 压测时为什么冻结阈值在变化?

本文从源码角度分析了 OceanBase 压测中冻结阈值动态变化的原因,并给出运维建议。 作者:张乾 外星人2号,兼任五位喵星人的铲屎官。 本文来源:原创投稿 爱可生开源社区出品,原创内容未经授权不得随意使用,转…

Redis两种持久化机制RDB和AOF详解(面试常问,工作常用)

redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失。幸好Redis还为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Append Only File)。 在这里假设你已经了解了redis的基础…

微信小程序上,实现图片右上角数字显示

微信小程序上,实现图片右上角数字显示 直接上代码: 样式代码index.wxss如下: .circle_rednum {position: absolute;color: white;font-size: 13px;background-color: #EC2F43;width: 23px;height: 23px;line-height: 23px;left: 80%;top: …

RuntimeError: DataLoader worker (pid 2105929) is killed by signal: Killed.

PyTorch DataLoader num_workers Test - 加快速度 可以利用PyTorch DataLoader类的多进程功能来加快神经网络训练过程。 加快训练进程 为了加快训练过程,我们将利用DataLoader类的num_workers可选属性。 num_workers属性告诉DataLoader实例要使用多少个子进程进…

pytorch工具——pytorch中的autograd

目录 关于torch.tensor关于tensor的操作关于梯度gradients 关于torch.tensor 关于tensor的操作 x1torch.ones(3,3) xtorch.ones(2,2,requires_gradTrue) print(x1,\n,x)yx2 print(y) print(x.grad_fn) print(y.grad_fn)zy*y*3 outz.mean() print(z,out)注意 atorch.randn(2,…

SQL调优教程

SQL调优教程 基础方法论 任何计算机应用系统性能问题最终都可以归结为 1.cpu消耗 2.内存使用 3.对磁盘,网络或其他I/O设备的输入/输出(I/O)操作 遇到性能问题时,要判断的第一点就是“在这三种资源中,是否有哪一种资源达到了有问题的程度”&…

通过四点分析CRM系统的发展趋势

CRM系统企业管理客户的有力武器,为企业发展、降本增效奠定了基础,国内CRM经过十几年的发展产品已经十分成熟,很多企业会问CRM系统未来发展趋势是什么?今天小编就分享几个CRM未来的趋势。 1.AI人工智能 去年席卷全球的ChatGPT让大…

【数据结构】链表是否有环相关问题

文章目录 快指针走3、4、5步甚至更多可以吗为什么快慢指针一定在入口点相遇![在这里插入图片描述](https://img-blog.csdnimg.cn/ba346dbc9fee425dbb895ae2962e99ce.png) 快指针走3、4、5步甚至更多可以吗 部分情况下可以。 如果这样,相对(追及&#xf…

在VSCode中实现Rust编程调试指南

在 VS Code 中调试 Rust:终极指南 在本教程中,您将学习如何使用 VS Code 调试 Rust。可用于使用 VS Code 调试 Rust 的操作。设置 VS Code 来调试 Rust Rust因其易用性、安全性和高性能而继续保持其作为最受欢迎的编程语言的地位。随着 Rust 的流行&…

paddle尝试PP-Vehicle-属性识别

PP-Vehicle-属性识别 windows11环境,conda虚拟环境中运行。 首先安装环境 conda create -n paddle python3.7conda activate paddlepip install paddlepaddle-gpupip install pyyamlpip install opencv-pythonpip install scipypip install matplotlibpip install…

关于Integer类的一个有趣的面试问题

相信很多人觉得答案是false,false,因为Integer是一个类,把int类型的数据传给Integer类型的数据会创建一个对象,而a,b,c,d作为引用指向的是不同的地址,所以判断相同得到的结果应该是false 但这个想法就正中下怀了&#…

链表 --- C语言实现

本篇文章来详细介绍一下数据结构中的链表。 目录 1.链表的概念及结构 2.链表的分类 3.单链表的实现 4.链表的面试题 5.双向链表的实现 6.顺序表和链表的区别 1.链表的概念及结构 概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素…

【C++】多态(举例+详解,超级详细)

本篇文章会对C中的多态进行详解。希望本篇文章会对你有所帮助。 文章目录 一、多态的定义及实现 1、1 多态的概念 1、2 多态的构成条件 1、2、1 虚函数 1、2、2 虚函数的重写 1、2、3 析构函数构成重写特例原因 1、3 多态的实例练习 1、3、1 例1 1、3、2 例2 1、3、3 例3 1、4…

【C语言】指针进阶(2)

接上期文章指针进阶(1)指针进阶(1) 目录 1.函数指针 2.函数指针数组 3.指向函数指针数组的指针 4.回调函数 4.1qsort的用法 void*类型的指针介绍 使用qsort对数组进行排序 使用qsort对结构体进行排序: 4.2使用…

【C++】-初步认识和学习继承

💖作者:小树苗渴望变成参天大树🎈 🎉作者宣言:认真写好每一篇博客💤 🎊作者gitee:gitee✨ 💞作者专栏:C语言,数据结构初阶,Linux,C 动态规划算法🎄 如 果 你 …

【CAS6.6源码解析】在IDEA中调试可插拔的supprot模块

CAS源码的casWebApplication启动后,默认只加载最小的支撑系统的模块,很多模块(大部分在support包下)是需要手动去引入的(对新人来说有坑),这里介绍一下如何手动引入这些模块。 文章目录 调试步骤…