一.“state”是声明性观念的中心

在经过Compose或SwiftUI等框架规划声明性视图时,咱们有必要明确的第一个范式是State(状况)。UI组件结合了它的图形表明(View)和它的State(状况)。UI组件中发生改动的任何属性或数据都能够表明为状况。例如,在TextField类型的UI组件中,用户输入的文本是一个能够更改的变量;因而,value是一个能够表明为状况(name)的变量,如下面的代码片所示。

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

声明性View的层次结构:

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

移动使用程序屏幕能够包括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的改动。

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

除了将所有状况处理职责托付给一个视图,还有其他挑选吗?

答案是肯定的。

更好的挑选是将这个职责托付给一个状况持有者或许承担这个角色的ViewModel。咱们鄙人一节中看到更多的细节。

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

将状况处理托付给一个ViewModel图:

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

尽管在完成中这个组件(ViewModel)是可选的,但我强烈主张运用它,由于它供给了许多长处,如有效办理数据和视图之间的生命周期。关于这个架构组件的更多信息,我主张查阅关于ViewModels的官方Google文档。视图和ViewModel之间的通讯只包括两种类型的音讯,事情和状况:

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

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

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

4.视图的使命是接纳由ViewModel发送的状况,并经过重组将它们转化为图形UI表明。

5.现在,关于每个组件的职责以及它们之间的音讯有了更清晰的认识,让咱们现在剖析一下信息流的状况。

了解数据流,“单向数据流形式”

假如咱们简化图上中的图,成果会使得下面的图:

单向数据流:

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

这是视图和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结构:

由状况表明的结构:

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

  • 属性UI的状况:它们是以状况表明的原始变量。在图1.5中,如名字、电话或地址等文本输入字段便是这种类型。
  • 组件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 变量上,它依靠于 nameValueChangedphoneValueChanged 变量。

合并状况对完成带来了长处,集中了操控,整理了代码。这将是咱们在完成中最常用的技术。

屏幕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地将它们分组。

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

多次事情:

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

ViewModel向视图露出四个操作(事情),每个操作被一个视图UI元素运用。

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

组合事情:

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

表明不同类型事情的完成或许是这样的:

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

这项作业将引导读者接触到接近实在和生产使用的规划和开发经历。

但首要,咱们将使用这一章学到的概念来完成一个简单的表单。

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