为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的变化。
除了将所有状态处理责任委托给一个视图,还有其他选择吗?
答案是肯定的。
更好的选择是将这个责任委托给一个状态持有者或者承担这个角色的ViewModel。我们在下一节中看到更多的细节。
ViewModel作为真实数据源 为了防止视图被责任压得喘不过气来,另一个组件被召唤来处理状态管理。这个适当的元素就是我们熟知的ViewModel。 如下图所示,将状态从View移动到ViewModel可以创建责任分离,使得展示逻辑和其对状态的影响可以集中化。
将状态处理委托给一个ViewModel图:
尽管在实现中这个组件(ViewModel)是可选的,但我强烈建议使用它,因为它提供了许多优点,如有效管理数据和视图之间的生命周期。关于这个架构组件的更多信息,我建议查阅关于ViewModels的官方Google文档。视图和ViewModel之间的通信只包括两种类型的消息,事件和状态:
1.事件是由任何视图或子视图通知给ViewModel的动作,作为用户与UI组件交互的结果。
2.状态代表ViewModel交付给视图进行各自图形解释的信息(数据)。
3.ViewModel的主要功能是接收来自视图发送的事件,解释它们,应用业务逻辑,并将它们转化为状态,以便回传给视图。
4.视图的任务是接收由ViewModel发送的状态,并通过重组将它们转化为图形UI表示。
5.现在,对于每个组件的责任以及它们之间的消息有了更清晰的认识,让我们现在分析一下信息流的情况。
理解数据流,“单向数据流模式”
如果我们简化图上中的图,结果会使得下面的图:
单向数据流:
这是视图和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结构:
由状态表示的结构:
- 属性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地将它们分组。
考虑一个下图所示的屏幕:
多次事件:
ViewModel向视图暴露四个操作(事件),每个操作被一个视图UI元素使用。
分析这四个事件,它们与输入用户联系信息的表单相关,所以将它们分组到一个事件类型中是有意义的,如下图所示:
组合事件:
表示不同类型事件的实现可能是这样的:
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”的移动应用。这个应用将具有电子商务的主要部分,如购物车,产品列表,和结账过程。
这项工作将引导读者接触到接近真实和生产应用的设计和开发经验。
但首先,我们将应用这一章学到的概念来实现一个简单的表单。
这将是下一章所描述的主题。