HarmonyOS 项目目录介绍

ArkTS 基本语法详解

HarmonyOS 4.0 以后可以使用 ArkTS 或者 HTML/CSS/JS 技术来开发 HarmonyOS 应用,而 ArkTS 是 HarmonyOS 优选的主力应用开发语言。

ArkTS 围绕应用开发在 TypeScript(简称 TS)生态基础上做了进一步扩展,继承了 TS 的所有特性,是 TS 的超集。因此,在学习 ArkTS 语言之前,建议首先具备 TS 语言开发能力。

当前,ArkTS 在 TS 的基础上主要扩展了如下能力:

  • 基本语法:ArkTS 定义了声明式 UI 描述、自定义组件和动态扩展 UI 元素的能力,再配合 ArkUI 开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了 UI 开发的主体。
  • 状态管理:ArkTS 提供了多维度的状态管理机制。在 UI 开发框架中,与 UI 相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和 UI 的联动。
  • 渲染控制:ArkTS 提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的 UI 内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

未来,ArkTS 会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、系统类型增强、分布式开发范式等更多特性。

ArkTS 基本组成

  • 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中 @Entry、@Component 和 @State 都是装饰器,@Component 表示自定义组件,@Entry 表示该自定义组件为入口组件,@State 表示组件中的状态变量,状态变量变化会触发 UI 刷新。
  • 自定义组件:可复用的 UI 单元,可组合其它组件,如上述被 @Component 装饰的 struct Index。
  • UI 描述:以声明式的方式来描述 UI 的结构,例如 build() 方法中的代码块 。
  • 系统组件:ArkUI 框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的 Column、Text、Divider、Button。
  • 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在 Button 后面的 onClick()。
  • 属性方法:组件可以通过链式调用配置多项属性,如 fontSize()、width()、height()、backgroundColor() 等。

系统组件、属性方法、事件方法具体使用可参考基于 ArkTS 的声明式开发范式

ArkTS 布局结构

布局的结构通常是分层级的,代表了用户界面中的整体架构。一个常见的页面结构如下所示:

为实现上述效果,需要在页面中声明对应的元素。其中,Page 表示页面的根节点,Column/Row 等元素为系统组件。针对不同的页面结构,ArkUI 提供了不同的布局组件来帮助我们实现对应布局的效果,例如 Row 用于实现线性布局等。

ArkTS 数据类型

TypeScript 支持一些基础的数据类型,如布尔型、数组、字符串等。

数字

TypeScript 里的所有数字都是浮点数,这些浮点数的类型是 number。除了支持十进制,还支持二进制、八进制、十六进制。

1
2
3
4
5
// 数值类型
let num1: number = 18 // 十进制
let num2: number = 0b10111 // ob 二进制
let num3: number = 0o14 // 0o 八进制
let num4: number = 0x1f // 0x 十六进制

字符串

TypeScript 里使用 string 表示文本数据类型, 可以使用双引号( “ )或单引号( ‘ )表示字符串或者反引号(`)。

反引号中可以配合 ${} 解析变量。

1
2
3
4
// 字符串类型
let str1: string = 'HarmonyOS Next不支持Android 应用了'
let str2: string = "ArkTS"
let str3: string = `马总今年${num1}岁了`

布尔值

TypeScript 中可以使用 boolean 来表示这个变量是布尔值,可以赋值为 true 或者 false。

1
2
3
// 布尔类型 true、false
let stateOn: boolean = true
let stateOff: boolean = false

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。如果一个数据可能有多重类型,或者当下还没想好用哪个类型 …

1
2
3
4
let flag: string | number | boolean
flag = '1'
flag = 1
flag = true

数组

TypeScrip 有两种方式可以定义数组。

1
2
3
4
5
// 第一种是使用数组泛型,Array<元素类型>。
let course1: Array<string> = ['Flutter', "HarmonyOS", `Golang`]

// 第二种是在元素类型后面接上 [],表示由此类型元素组成的一个数组。
let course2: string[] = ['Flutter', "HarmonyOS", `Golang`]

枚举

enum 类型是对 JavaScript 标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。

1
2
enum Color {Red, Green, Blue}
let c: Color = Color.Green

元组

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string 和 number 类型的元组。

1
2
3
let x: [string, number]
x = ['hello', 10] // OK
x = [10, 'hello'] // Error

Any(不推荐使用)

any 类型,表示的是变量可以是任意类型,相当于关闭了 ts 的类型检测功能,不建议使用。如果是 any 类型,则允许被赋值为任意类型。

1
2
3
4
5
6
7
8
let a: any = 666
a = "Semlinker" // 通过,any 类型可以被赋任意值
a = false // 通过,any 类型可以被赋任意值
a = 66 // 通过,any 类型可以被赋任意值
a = undefined // 通过,any 类型可以被赋任意值
a = null // 通过,any 类型可以被赋任意值
a = [] // 通过,any 类型可以被赋任意值
a = {} // 通过,any 类型可以被赋任意值

Unknown

unknown 与 any 一样,也相当于关闭了 ts 的类型检测功能。有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。那么我们可以使用 unknown 类型来标记这些变量。(同 any )。

1
2
3
let notSure: unknown = 4
notSure = 'maybe a string instead'
notSure = false

Void

当一个函数没有返回值时,你通常会见到其返回值类型是 void。

1
2
3
function test(): void {
console.log('This is function is void')
}

Null 和 Undefined

TypeScript 里,null 和 undefined 两者各自有自己的类型分别叫做 null 和 undefined。

1
2
let u: undefined = undefined
let n: null = null

ArkTS 渲染控制

if/else:条件渲染

  • 支持 if、else 和 else if 语句。
  • if、else if 后跟随的条件语句可以使用状态变量。
  • 允许在容器组件内使用,通过条件渲染语句构建不同的子组件。
  • 当 if、else if 后跟随的状态判断中使用的状态变量值变化时,条件渲染语句会进行更新条件可以包括 Typescript 表达式。
1
2
3
4
5
if (条件表达式) {
组件内容1
} else {
组件内容2
}

ForEach:循环渲染

ForEach 接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在 ForEach 父容器组件中的子组件。例如,ListItem 组件要求 ForEach 的父容器组件必须为 List 组件。

1
2
3
4
5
6
7
8
9
10
/**
* @param arr 要遍历的数组
* @param itemGenerator 页面的生成函数
* @param keyGenerator 键生成函数,提供唯一标识,如果后续数组中数据发生变化,会判断元素的唯一标识是否发生变化,有变更再去做渲染,这样减少了不必要的渲染,提高了页面的渲染效率(可选参数,如果不填会会有默认的键生成函数,生成规则是用角标拼上数组元素的数据)
*/
ForEach(
arr:Array<any>,
itemGenerator:(item: any, index: number) => void,
keyGenerator ? : (item: any, index: number) => string
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let arr: number[] = [1, 2, 4, 8, 16]

@Entry
@Component
struct ArrayPage {

arr2: string[] = ['HarmonyOS', 'Flutter', 'Android']

build() {
List({ space: 10 }) {
// 访问全局变量 arr 不要加 this
ForEach(arr, (item: number, key: number) => {
ListItem() {
Text(`${item}--${key}`)
.fontSize(20)
.width('100%')
.height(50)
.textAlign(TextAlign.Center)
.backgroundColor("#eee")
.borderRadius(10)
}
}, (item: number) => item.toString())

// 访问局部变量 arr2 要加 this
ForEach(this.arr2, (item: string) => {
ListItem() {
Text(item)
.fontSize(20)
.width('100%')
.height(50)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Gray)
.borderRadius(10)
}
}, (item: string) => item)
}.width('100%')
.height('100%')
.padding(10)
}
}

实际开发中尽量不要使用全局变量,组件中使用全局变量不需要加 this。

LazyForEach:数据懒加载

LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当 LazyForEach 在滚动容器中使用,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

基础组件

Image

应用中显示图片需要使用 Image 组件实现,Image 支持多种图片格式,包括 png、jpg、bmp、svg 和 gif,具体用法请参考 Image 组件。

Image 通过调用接口来创建,接口调用形式如下:

1
Image(src: PixelMap | ResourceStr | DrawableDescriptor)

该接口通过图片数据源获取图片,支持本地图片和网络图片的渲染展示。其中,src 是图片的数据源,加载方式请参考加载图片资源

存档图类型数据源

存档图类型的数据源可以分为本地资源、网络资源、Resource 资源、媒体库资源和 base64。

  • 本地资源

    创建文件夹,将本地图片放入 ets 文件夹下的任意位置。

    Image 组件引入本地图片路径,即可显示图片(根目录为 ets 文件夹)。

    1
    2
    Image('images/view.jpg')
    .width(200)
  • 网络资源

    引入网络图片需申请权限 ohos.permission.INTERNET,具体申请方式请参考声明权限。此时,Image 组件的 src 参数为网络图片的链接。

    Image 组件首次加载网络图片时,需要请求网络资源,非首次加载时,默认从缓存中直接读取图片,更多图片缓存设置请参考 setImageCacheCountsetImageRawDataCacheSizesetImageFileCacheSize

    1、在 src/main/module.json5 中申请网络权限。

    1
    2
    3
    4
    5
    "requestPermissions": [
    {
       "name": "ohos.permission.INTERNET"
    }
    ]

    2、加载网络图片。

    1
    Image('https://gitee.com/zch0304/images/raw/master/note/test_icon1.jpg')
  • Resource 资源

    使用资源格式可以跨包/跨模块引入图片,resources 文件夹下的图片都可以通过 $r 资源接口读取到并转换到 Resource 格式。

    1
    2
    // src/main/resources/base/media/icon.png
    Image($r('app.media.icon'))

    还可以将图片放在 rawfile 文件夹下。

    1
    2
    // src/main/resources/rawfile/example1.png
    Image($rawfile('example1.png'))
  • 媒体库 file://data/storage

    支持 file://路径前缀的字符串,用于访问通过媒体库提供的图片路径。

    1、调用接口获取图库的照片 url。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    import picker from '@ohos.file.picker';
    import { BusinessError } from '@ohos.base';

    @Entry
    @Component
    struct Index {
    @State imgDatas: string[] = [];
    // 获取照片 url 集
    getAllImg() {
    try {
    let PhotoSelectOptions:picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 5;
    let photoPicker:picker.PhotoViewPicker = new picker.PhotoViewPicker();
    photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult:picker.PhotoSelectResult) => {
    this.imgDatas = PhotoSelectResult.photoUris;
    console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
    }).catch((err:Error) => {
    let message = (err as BusinessError).message;
    let code = (err as BusinessError).code;
    console.error(`PhotoViewPicker.select failed with. Code: ${code}, message: ${message}`);
    });
    } catch (err) {
    let message = (err as BusinessError).message;
    let code = (err as BusinessError).code;
    console.error(`PhotoViewPicker failed with. Code: ${code}, message: ${message}`); }
    }

    // aboutToAppear 中调用上述函数,获取图库的所有图片 url,存在 imgDatas 中
    async aboutToAppear() {
    this.getAllImg();
    }
    // 使用 imgDatas 的 url 加载图片。
    build() {
    Column() {
    Grid() {
    ForEach(this.imgDatas, (item:string) => {
    GridItem() {
    Image(item)
    .width(200)
    }
    }, (item:string):string => JSON.stringify(item))
    }
    }.width('100%').height('100%')
    }
    }

    2、从媒体库获取的 url 格式通常如下。

    1
    2
    Image('file://media/Photos/5')
    .width(200)
  • base64

    路径格式为 data:image/[png|jpeg|bmp|webp];base64,[base64 data],其中 [base64 data] 为 Base64 字符串数据。

    Base64 格式字符串可用于存储图片的像素数据,在网页上使用较为广泛。

多媒体像素图

PixelMap 是图片解码后的像素图,具体用法请参考图片开发指导。以下示例将加载的网络图片返回的数据解码成 PixelMap 格式,再显示在 Image 组件上。

1、创建 PixelMap 状态变量。

1
@State image: PixelMap | undefined = undefined;

2、引用多媒体。

请求网络图片,解码编码 PixelMap。

a. 引用网络权限与媒体库权限。

1
2
3
4
import http from '@ohos.net.http';
import ResponseCode from '@ohos.net.http';
import image from '@ohos.multimedia.image';
import { BusinessError } from '@ohos.base';

b. 填写网络图片地址。

1
2
3
4
5
6
7
8
9
10
let OutData: http.HttpResponse
http.createHttp().request("https://www.example.com/xxx.png",
(error: BusinessError, data: http.HttpResponse) => {
if (error) {
console.error(`http reqeust failed with. Code: ${error.code}, message: ${error.message}`);
} else {
OutData = data
}
}
)

c. 将网络地址成功返回的数据,编码转码成 pixelMap 的图片格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let code: http.ResponseCode | number = OutData.responseCode
if (ResponseCode.ResponseCode.OK === code) {
let imageData: ArrayBuffer = OutData.result as ArrayBuffer;
let imageSource: image.ImageSource = image.createImageSource(imageData);

class tmp {
height: number = 100
width: number = 100
}

let si: tmp = new tmp()
let options: Record<string, number | boolean | tmp> = {
'alphaType': 0, // 透明度
'editable': false, // 是否可编辑
'pixelFormat': 3, // 像素格式
'scaleMode': 1, // 缩略值
'size': { height: 100, width: 100 }
} // 创建图片大小

class imagetmp {
image: PixelMap | undefined = undefined
set(val: PixelMap) {
this.image = val
}
}

imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
let im = new imagetmp()
im.set(pixelMap)
})
}

d. 显示图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class htp{
httpRequest: Function | undefined = undefined
set(){
if(this.httpRequest){
this.httpRequest()
}
}
}
Button("获取网络图片")
.onClick(() => {
let sethtp = new htp()
sethtp.set()
})
Image(this.image).height(100).width(100)

显示矢量图

Image 组件可显示矢量图(svg 格式的图片),支持的 svg 标签为:svg、rect、circle、ellipse、path、line、polyline、polygon 和 animate。

svg 格式的图片可以使用 fillColor 属性改变图片的绘制颜色。

1
2
3
Image($r('app.media.cloud'))
.width(50)
.fillColor(Color.Blue)

设置图片缩放类型

通过 objectFit 属性使图片缩放到高度和宽度确定的框内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Entry
@Component
struct MyComponent {
scroller: Scroller = new Scroller()

build() {
Scroll(this.scroller) {
Column() {
Row() {
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。
.objectFit(ImageFit.Contain)
.margin(15)
.overlay('Contain', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_img_2'))
.width(200)
.height(150)
.border({ width: 1 })
.objectFit(ImageFit.Cover)
.margin(15)
// 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
.overlay('Cover', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 自适应显示。
.objectFit(ImageFit.Auto)
.margin(15)
.overlay('Auto', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}

Row() {
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
.objectFit(ImageFit.Fill)
.margin(15)
// 不保持宽高比进行放大缩小,使得图片充满显示边界。
.overlay('Fill', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比显示,图片缩小或者保持不变。
.objectFit(ImageFit.ScaleDown)
.margin(15)
.overlay('ScaleDown', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持原有尺寸显示。
.objectFit(ImageFit.None)
.margin(15)
.overlay('None', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}
}
}
}

图片插值

当原图分辨率较低并且放大显示时,图片会模糊出现锯齿。这时可以使用 interpolation 属性对图片进行插值,使图片显示得更清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Entry
@Component
struct Index {
build() {
Column() {
Row() {
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.None)
.borderWidth(1)
.overlay("Interpolation.None", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.Low)
.borderWidth(1)
.overlay("Interpolation.Low", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
}.width('100%')
.justifyContent(FlexAlign.Center)

Row() {
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.Medium)
.borderWidth(1)
.overlay("Interpolation.Medium", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
Image($r('app.media.grass'))
.width('40%')
.interpolation(ImageInterpolation.High)
.borderWidth(1)
.overlay("Interpolation.High", { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
.margin(10)
}.width('100%')
.justifyContent(FlexAlign.Center)
}
.height('100%')
}
}

设置图片重复样式

通过 objectRepeat 属性设置图片的重复样式方式,重复样式请参考 ImageRepeat 枚举说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct MyComponent {
build() {
Column({ space: 10 }) {
Row({ space: 5 }) {
Image($r('app.media.ic_public_favor_filled_1'))
.width(110)
.height(115)
.border({ width: 1 })
.objectRepeat(ImageRepeat.XY)
.objectFit(ImageFit.ScaleDown)
// 在水平轴和竖直轴上同时重复绘制图片
.overlay('ImageRepeat.XY', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_public_favor_filled_1'))
.width(110)
.height(115)
.border({ width: 1 })
.objectRepeat(ImageRepeat.Y)
.objectFit(ImageFit.ScaleDown)
// 只在竖直轴上重复绘制图片
.overlay('ImageRepeat.Y', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_public_favor_filled_1'))
.width(110)
.height(115)
.border({ width: 1 })
.objectRepeat(ImageRepeat.X)
.objectFit(ImageFit.ScaleDown)
// 只在水平轴上重复绘制图片
.overlay('ImageRepeat.X', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}.height(150).width('100%').padding(8)
}
}

设置图片渲染模式

通过 renderMode 属性设置图片的渲染模式为原色或黑白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entry
@Component
struct MyComponent {
build() {
Column({ space: 10 }) {
Row({ space: 50 }) {
Image($r('app.media.example'))
// 设置图片的渲染模式为原色
.renderMode(ImageRenderMode.Original)
.width(100)
.height(100)
.border({ width: 1 })
// overlay 是通用属性,用于在组件上显示说明文字
.overlay('Original', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.example'))
// 设置图片的渲染模式为黑白
.renderMode(ImageRenderMode.Template)
.width(100)
.height(100)
.border({ width: 1 })
.overlay('Template', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}.height(150).width('100%').padding({ top: 20,right: 10 })
}
}

设置图片解码尺寸

通过 sourceSize 属性设置图片解码尺寸,降低图片的分辨率。

原图尺寸为 1280x960,该示例将图片解码为 40x40 和 90x90。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entry
@Component
struct Index {
build() {
Column() {
Row({ space: 50 }) {
Image($r('app.media.example'))
.sourceSize({
width: 40,
height: 40
})
.objectFit(ImageFit.ScaleDown)
.aspectRatio(1)
.width('25%')
.border({ width: 1 })
.overlay('width:40 height:40', { align: Alignment.Bottom, offset: { x: 0, y: 40 } })
Image($r('app.media.example'))
.sourceSize({
width: 90,
height: 90
})
.objectFit(ImageFit.ScaleDown)
.width('25%')
.aspectRatio(1)
.border({ width: 1 })
.overlay('width:90 height:90', { align: Alignment.Bottom, offset: { x: 0, y: 40 } })
}.height(150).width('100%').padding(20)
}
}
}

为图片添加滤镜效果

通过 colorFilter 修改图片的像素颜色,为图片添加滤镜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entry
@Component
struct Index {
build() {
Column() {
Row() {
Image($r('app.media.example'))
.width('40%')
.margin(10)
Image($r('app.media.example'))
.width('40%')
.colorFilter(
[1, 1, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0])
.margin(10)
}.width('100%')
.justifyContent(FlexAlign.Center)
}
}
}

同步加载图片

一般情况下,图片加载流程会异步进行,以避免阻塞主线程,影响 UI 交互。但是特定情况下,图片刷新时会出现闪烁,这时可以使用 syncLoad 属性,使图片同步加载,从而避免出现闪烁。不建议图片加载较长时间时使用,会导致页面无法响应。

1
2
Image($r('app.media.icon'))
.syncLoad(true)

事件调用

通过在 Image 组件上绑定 onComplete 事件,图片加载成功后可以获取图片的必要信息。如果图片加载失败,也可以通过绑定 onError 回调来获得结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct MyComponent {
@State widthValue: number = 0
@State heightValue: number = 0
@State componentWidth: number = 0
@State componentHeight: number = 0

build() {
Column() {
Row() {
Image($r('app.media.ic_img_2'))
.width(200)
.height(150)
.margin(15)
.onComplete(msg => {
if(msg){
this.widthValue = msg.width
this.heightValue = msg.height
this.componentWidth = msg.componentWidth
this.componentHeight = msg.componentHeight
}
})
// 图片获取失败,打印结果
.onError(() => {
console.info('load image fail')
})
.overlay('\nwidth: ' + String(this.widthValue) + ', height: ' + String(this.heightValue) + '\ncomponentWidth: ' + String(this.componentWidth) + '\ncomponentHeight: ' + String(this.componentHeight), {
align: Alignment.Bottom,
offset: { x: 0, y: 60 }
})
}
}
}
}

布局组件

ArkTS 通用属性

ArkTS 通用属性用于设置组件的宽高、边距。

名称 参数说明 描述
width Length 设置组件自身的宽度,缺省时使用元素自身内容需要 的宽度。若子组件的宽大于父组件的宽,则会画出父 组件的范围。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
height Length 设置组件自身的高度,缺省时使用元素自身内容需要 的高度。若子组件的高大于父组件的高,则会画出父 组件的范围。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
size { width ? : Length, height ? : Length } 设置高宽尺寸。
padding Padding | Length 设置内边距属性。参数为 Length 类型时,四个方向内边距同时生效。默认值:0。padding 设置百分比时,上下左右内边距均以父容器的 width 作为基础值。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
margin Margin | Length 设置外边距属性。参数为 Length 类型时,四个方向外边距同时生效。默认值:0。margin 设置百分比时,上下左右外边距均以父容器的 width 作为基础值。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
constraintSize { minWidth ? : Length, maxWidth ? : Length, minHeight ? : Length, maxHeight ? : Length } 设置约束尺寸,组件布局时,进行尺寸范围限制。 constraintSize 的优先级高于 Width 和 Height。若设置的 minWidth 大于 maxWidth,则 minWidth 生效, minHeight 与 maxHeight 同理。默认值:{ minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity }。

Length 长度类型,用于描述尺寸单位。

类型 说明
string 需要显式指定像素单位,如 ‘10px’,也可设置百分比字符串,如 ‘100%’。
number 默认单位 vp。
Resource 资源引用类型,引入系统资源或者应用资源中的尺寸。

vp 为虚拟像素单位:vp 使用虚拟像素,使元素在不同密度的设备上具有一致的视觉体量。

fp 字体像素单位:字体像素(font pixel)大小默认情况下与 vp 相同,即默认情况下 1 fp = 1 vp。如果用户在设置中选择了更大的字体,字体的实际显示大小就会在 vp 的基础上乘以用户设置的缩放系数,即 1 fp = 1 vp * 缩放系数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Row() {
// 宽度 200,高度 200,外边距 20(蓝色区域),内边距 10(白色区域)
Row() {
Row().size({
width: '100%', height: '100%'
}).backgroundColor(Color.Yellow)
}
.width(200)
.height(200)
.padding(10)
.margin(20)
.backgroundColor(Color.White)
}.backgroundColor(Color.Blue)

Text(`This is a text.This is a text.This is a text.This is a text.This is a text`)
.width('90%')
.backgroundColor(Color.Orange)
.constraintSize({ maxWidth: 200 })

// 父容器尺寸确定时,设置了 layoutWeight 的子元素在主轴布局尺寸按照权重进行分配,忽略本身尺寸设置。
Row() {
// 权重 1,占主轴剩余空间 1/3
Text('layoutWeight(1)')
.size({ height: 110 })
.backgroundColor(Color.Orange)
.textAlign(TextAlign.Center)
.layoutWeight(1)

// 权重 2,占主轴剩余空间 2/3
Text('layoutWeight(2)')
.size({
width: '30%', height: 110
})
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.layoutWeight(2)

// 未设置 layoutWeight 属性,组件按照自身尺寸渲染
Text('no layoutWeight')
.size({
width: '33%', height: 110
})
.backgroundColor(Color.Brown)
.textAlign(TextAlign.Center)
}.size({ width: '100%', height: 140 })
.backgroundColor(0xCCCCCC)
}
.width('100%')
.height('100%')
.margin({ top: 5 })
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
}
}

Column 详解

Column 是沿垂直方向布局的容器,可以包含多个子组件,多个子组件会在垂直方向上按照顺序排列。

Column 接口

1
Column(value?: {space?: string | number})

参数:

参数名 参数类型 必填 参数描述
space string | number 纵向布局元素垂直方向间距。从 API version 9 开始,space 为负数或者 justifyContent 设置为 FlexAlign.SpaceBetween、 FlexAlign.SpaceAround、FlexAlign.SpaceEvenly 时不生效。默认值:0,单位 vp。说明:可选值为大于等于 0 的数字,或者可以转换为数字的字符串。

Column 属性

名称 参数类型 描述
alignItems HorizontalAlign 设置子组件在水平方向上的对齐格式。默认值:HorizontalAlign.Center 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
justifyContent8+ FlexAlign 设置子组件在垂直方向上的对齐格式。默认值:FlexAlign.Start 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Row() {
($r('app.media.app_icon')).width(100).height(100)
}
.width(140)
.height(140)
.backgroundColor(Color.Red)
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.margin({ top: 5 })
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
}
}

Row 详解

Row 为沿水平方向布局容器,可以包含多个子组件,多个子组件会在水平方向上按照顺序排列。

Row 接口

1
Row(value?: {space?: string | number})

参数:

参数 名 参数类 型 必 填 参数描述
space string | number 横向布局元素间距。从 API version 9 开始,space 为负数或者 justifyContent 设置为 FlexAlign.SpaceBetween、FlexAlign.SpaceAround、FlexAlign.SpaceEvenly 时不生效。默认值:0,单位 vp。说明:可选值为大于等于 0 的数字,或者可以转换为 数字的字符串。

Row 属性

名称 参数类型 描述
alignItems VerticalAlign 设置子组件在垂直方向上的对齐格式。默认值:VerticalAlign.Center 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
justifyContent8+ FlexAlign 设置子组件在水平方向上的对齐格式。默认值:FlexAlign.Start 从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。

自定义组件

自定义组件传值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Container()
Container({ color: Color.Orange, icon: $r('app.media.user') })
Container({ color: Color.Brown, icon: $r('app.media.settings') })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
}
}

@Component
struct Container {
color: Color = Color.Red
icon: Resource = $r('app.media.app_icon')

build() {
Row() {
Image(this.icon).width(100).height(100)
}
.width(140)
.height(140)
.backgroundColor(this.color)
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}

Row、Column 结合 layoutWeight 实现弹性布局

ArkTS 中使用 Row、Column 结合 layoutWeight 属性可以实现弹性布局。

水平弹性布局

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entry
@Component
struct Index {
build() {
Row() {
Row().height(100).backgroundColor(Color.Red).layoutWeight(1)
Row().height(100).backgroundColor(Color.Green).layoutWeight(1)
Row() {
Text('no layoutWeight').textAlign(TextAlign.Center).width('100%')
}.height(100).backgroundColor(Color.Orange).size({ height: 100, width: 200 })
}.size({ width: '100%', height: 100 })
}
}

垂直弹性布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon1.jpg").objectFit(ImageFit.Fill)
}.size({ width: '100%', height: 160 })

Row({ space: 10 }) {
Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon2.jpg").objectFit(ImageFit.Fill)
}.layoutWeight(2)

Row() {
Column({ space: 10 }) {
Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon3.jpg").objectFit(ImageFit.Fill)
}.layoutWeight(1).width('100%')

Row() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon4.jpg").objectFit(ImageFit.Fill)
}.layoutWeight(1).width('100%')
}
}.layoutWeight(1)
}.size({ width: '100%', height: 140 })
}
.margin(5)
}
}

Stack 定位组件

Stack 组件可以实现容器堆叠,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。

Stack 接口

1
Stack(value?: { alignContent?: Alignment })

参数:

参数名 参数类型 必 填 参数描述
alignContent Alignment 设置子组件在容器内的对齐方式。默认值:Alignment.Center。

Stack 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
Stack({ alignContent: Alignment.BottomStart }) {
Row() {
Text("HarmonyOS").fontSize(40).textAlign(TextAlign.Center)
}.width(300).height(300).backgroundColor(Color.Orange)

Row().width(100).height(100).backgroundColor(Color.Blue)
}
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}

Stack 子组件中 zIndex 控制层级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entry
@Component
struct Index {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text("Stack子元素1").fontSize(20)
}.width(100).height(100).backgroundColor(Color.Orange).zIndex(2)

Column() {
Text("Stack子元素2").fontSize(20)
}.width(150).height(150).backgroundColor(Color.Yellow).zIndex(1)

Column() {
Text("Stack子元素3").fontSize(20)
}.width(200).height(200).backgroundColor(Color.Pink)
}.width(350).height(350).backgroundColor(Color.Gray)
}
}

Stack 结合 List 实现动态列表

要实现的功能:
1、实现浮动按钮。
2、点击按钮让列表的数字加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct Index {
@State list: number[] = [1, 2, 3]

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
List({ space: 10 }) {
ForEach(this.list, (item: number) => {
ListItem() {
Text(`${item}`)
.fontSize(20)
.width('100%')
.height(50)
.backgroundColor('#eee')
.textAlign(TextAlign.Center)
}
})
}.width('100%').height('100%').padding(10)

Button({ type: ButtonType.Circle, stateEffect: true }) {
Text('+').fontSize(40).fontColor(Color.White)
}.width(55).height(55).margin({ right: 20, bottom: 20 }).onClick(() => {
this.doAdd()
})
}
}.width('100%').height('100%').alignItems(HorizontalAlign.Center)
}

doAdd() {
this.list.push(this.list[this.list.length-1] + 1)
}
}

Stack 结合 List 实现浮动导航

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Entry
@Component
struct Index {
@State list: number[] = [1, 2, 3]

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
Stack({ alignContent: Alignment.TopEnd }) {
List({ space: 10 }) {
ForEach(this.list, (item: number) => {
ListItem() {
Text(`${item}`)
.fontSize(20)
.width('100%')
.height(50)
.backgroundColor('#eee')
.textAlign(TextAlign.Center)
}
})
}.width('100%').height('100%').padding({ left: 10, top: 70, right: 10, bottom: 10 })

Row() {
Text("导航").fontSize(16).textAlign(TextAlign.Center).width('100%')
}.width('100%').height(60).backgroundColor(Color.Orange)
}

Button({ type: ButtonType.Circle, stateEffect: true }) {
Text('+').fontSize(40).fontColor(Color.White)
}.width(55).height(55).margin({ right: 20, bottom: 20 }).onClick(() => {
this.doAdd()
})
}
}.width('100%').height('100%').alignItems(HorizontalAlign.Center)
}

doAdd() {
this.list.push(this.list[this.list.length-1] + 1)
}
}

弹性布局(Flex)

Flex 弹性布局可以更加方便的对容器中的子元素进行排列、对齐和分配剩余空间等。单行的 Flex 跟 Row 组件的表现几乎一致,单列的 Flex 则跟 Column 组件表现几乎一致。但 Row 与 Column 都是单行单列的,Flex 则可以突破了这个限制,当主轴上空间不足时,则向纵轴上去扩展显示。

Flex 接口

1
Flex(value?: { direction?: FlexDirection, wrap?: FlexWrap, justifyContent?: FlexAlign, alignItems?: ItemAlign, alignContent?: FlexAlign })

标准 Flex 布局容器。从 API version 9 开始,该接口支持在 ArkTS 片中使用。

Flex 参数

参数名 参数类型 必 填 默认值 参数描述
direction FlexDirection FlexDirection.Row 子组件在 Flex 容器上排列的方向,即主轴的方向。
wrap FlexWrap FlexWrap.NoWrap Flex 容器是单行/列还是多行/列排列。说明:在多行布局时,通过交叉轴方向,确认新行堆叠方向。
justifyContent FlexAlign FlexAlign.Start 子组件在 Flex 容器主轴上的对齐格式。
alignItems ItemAlign ItemAlign.Start 子组件在 Flex 容器交叉轴上的对齐格式。
alignContent FlexAlign FlexAlign.Start 交叉轴中有额外的空间时,多行内容的对齐方式。仅在 wrap 为 Wrap 或 WrapReverse 下生效。

Flex 轴的基本概念

  • 主轴:Flex 组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始点,结束位置称为主轴结束点。
  • 交叉轴:垂直于主轴方向的轴线。交叉轴开始的位置称为交叉轴起始点,结束位置称为交叉轴结束点。

direction 参数控制布局方向

Flex 布局中通过 direction 可以改变布局方向,在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数 direction,可以决定主轴的方向,从而控制子组件的排列方向。

  • FlexDirection.Row:主轴为水平方向,子组件从起始端沿着水平方向开始排布。
  • FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着 FlexDirection. Row 相反的方向开始排布。
  • FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布。
  • FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着 FlexDirection. Column 相反的方向开始排布。
1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(Color.Red)
Text('2').width('33%').height(50).backgroundColor(Color.Green)
Text('3').width('33%').height(50).backgroundColor(Color.Orange)
}.width('90%').height(70).padding(10).backgroundColor('#ccc')
}
}

wrap 参数控制布局换行

弹性布局分为单行布局和多行布局。默认情况下,Flex 容器中的子元素都排在一条线(又称“轴线”) 上。wrap 属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex 是单行布局还是多行布局。在多行布局时,通过交叉轴方向,确认新行堆叠方向。

FlexWrap. NoWrap(默认值):不换行。如果子组件的宽度总和大于父元素的宽度,则子组件会被压缩宽度。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ wrap: FlexWrap.NoWrap }) {
Text('1').width('50%').height(50).backgroundColor(Color.Red)
Text('2').width('50%').height(50).backgroundColor(Color.Green)
Text('3').width('50%').height(50).backgroundColor(Color.Orange)
}.width('90%').padding(10).backgroundColor('#ccc')
}
}

FlexWrap. Wrap:换行,每一行子组件按照主轴方向排列。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(Color.Red)
Text('2').width('50%').height(50).backgroundColor(Color.Green)
Text('3').width('50%').height(50).backgroundColor(Color.Orange)
}.width('90%').padding(10).backgroundColor('#ccc')
}
}

FlexWrap. WrapReverse:换行,每一行子组件按照主轴反方向排列。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ wrap: FlexWrap.WrapReverse }) {
Text('1').width('50%').height(50).backgroundColor(Color.Red)
Text('2').width('50%').height(50).backgroundColor(Color.Green)
Text('3').width('50%').height(50).backgroundColor(Color.Orange)
}.width('90%').padding(10).backgroundColor('#ccc')
}
}

justifyContent 配置主轴对齐方式

FlexAlign.Start(默认值):子组件在主轴方向起始端对齐,第一个子组件与父元素边沿对齐,其他元素与前一个元素对齐。

FlexAlign.Center:子组件在主轴方向居中对齐。

FlexAlign.End:子组件在主轴方向终点端对齐,最后一个子组件与父元素边沿对齐,其它元素与后一个元素对齐。

FlexAlign.SpaceBetween:Flex 主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件和最后一个子组件与父元素边沿对齐。

FlexAlign.SpaceAround:Flex 主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件到主轴起始端的距离和最后一个子组件到主轴终点端的距离是相邻元素之间距离的一半。

FlexAlign.SpaceEvenly:Flex 主轴方向元素等间距布局,相邻子组件之间的间距、第一个子组件与主轴起始端的间距、最后一个子组件到主轴终点端的间距均相等。

alignContent 配置内容对齐

可以通过 alignContent 参数设置子组件各行在交叉轴剩余空间内的对齐方式,只在多行的 flex 布局中生效(仅在 wrap 为 Wrap 或 WrapReverse 下生效),可选值有:

FlexAlign.Start:子组件各行与交叉轴起点对齐,默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entry
@Component
struct Index {
build() {
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(Color.Red)
Text('2').width('60%').height(20).backgroundColor(Color.Green)
Text('3').width('40%').height(20).backgroundColor(Color.Orange)
Text('4').width('30%').height(20).backgroundColor(Color.Pink)
Text('5').width('20%').height(20).backgroundColor(Color.Yellow)
}.height(100).backgroundColor('#ccc')
}
}

FlexAlign.Center:子组件各行在交叉轴方向居中对齐。

FlexAlign.End:子组件各行与交叉轴终点对齐。

FlexAlign.SpaceBetween:子组件各行与交叉轴两端对齐,各行间垂直间距平均分布。

FlexAlign.SpaceAround:子组件各行间距相等,是元素首尾行与交叉轴两端距离的两倍。

FlexAlign.SpaceEvenly:子组件各行间距,子组件首尾行与交叉轴两端距离都相等。

alignItems 参数控制交叉轴对齐方式

容器和子元素都可以设置交叉轴对齐方式,且子元素设置的对齐方式优先级较高。可以通过 Flex 组件的 alignItems 参数设置子组件在交叉轴的对齐方式。

ItemAlign.Auto:使用 Flex 容器中默认配置。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(20).backgroundColor(Color.Red)
Text('2').width('33%').height(40).backgroundColor(Color.Green)
Text('3').width('33%').height(60).backgroundColor(Color.Orange)
}.height(80).backgroundColor('#ccc')
}
}

ItemAlign.Start:交叉轴方向首部对齐,默认值。

ItemAlign.Center:交叉轴方向居中对齐。

ItemAlign.End:交叉轴方向底部对齐。

ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。

ItemAlign. Baseline:交叉轴方向文本基线对齐。

Flex 自适应拉伸布局属性

Row、Column 结合 layoutWeight 可以实现自适应拉伸弹性布局。

1、flexGrow 属性

设置父容器的剩余空间分配给此属性所在组件的比例。用于“瓜分”父组件的剩余空间。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex() {
Text('1').flexGrow(1).height(60).backgroundColor(Color.Red)
Text('2').flexGrow(2).height(60).backgroundColor(Color.Green)
Text('no flexGrow').width(100).height(60).backgroundColor(Color.Orange)
}.height(80).backgroundColor('#ccc')
}
}

父容器宽度 400vp,三个子组件原始宽度为 100vp,总和 300vp,剩余空间 100vp 根据 flexGrow 值的占比分配给子组件,未设置 flexGrow 的子组件不参与“瓜分”。 第一个元素以及第二个元素以 2:3 分配剩下的 100vp。第一个元素为 100vp+100vp2/5=140vp,第 二个元素为 100vp+100vp3/5=160vp。

2、flexShrink 属性

当父容器空间不足时,子组件的压缩比例。

1
2
3
4
5
6
7
8
9
10
11
@Entry
@Component
struct Index {
build() {
Flex() {
Text('flexShrink(3)').flexShrink(3).width(200).height(60).backgroundColor(Color.Red)
Text('no flexShrink').width(200).height(60).backgroundColor(Color.Green)
Text('flexShrink(1)').flexShrink(1).width(200).height(60).backgroundColor(Color.Orange)
}.width(400).height(80).backgroundColor('#ccc')
}
}

Flex 应用案例

弹性布局在开发场景中用例特别多,比如页面头部导航栏的均匀分布、页面框架的搭建、多行多列数据的排列等等。

1、热搜功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entry
@Component
struct Index {
@State hotSearch: string[] = ['乒乓球', 'NBA直播', '世界杯', '广东城际铁路', 'Flutter教程', 'HarmonyOS', 'ArkTS']

build() {
Column() {
Text('热门搜索')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
.padding(10)
.fontColor('#666')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.hotSearch, (item: string) => {
Text(`${item}`)
.fontSize(18)
.backgroundColor('#eee')
.padding({ left: 16, top: 10, right: 16, bottom: 10 })
.margin(10)
.borderRadius(12)
}, (item: string) => item)
}
}.width('100%').height('100%')
}
}

2、帮助列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
interface HelpListInterface {
title: string,
icon: Resource
}

@Entry
@Component
struct Index {
@State helpList: HelpListInterface[] = [
{
title: "我的订阅",
icon: $r("app.media.subscribe")
},
{
title: "常见问题",
icon: $r("app.media.problem")
},
{
title: "在线客服",
icon: $r("app.media.customer_service")
},
{
title: "意见反馈",
icon: $r("app.media.opinion")
},
{
title: "关怀模式",
icon: $r("app.media.give")
},
{
title: "会员中心",
icon: $r("app.media.user")
}
]

build() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.helpList, (item: HelpListInterface) => {
Column() {
Image(item.icon).width(42).height(42)
Text(`${item.title}`).fontSize(15).fontColor('#666').padding({ top: 10 })
}.width('25%').padding(5).margin({ bottom: 10 })
}, (item: HelpListInterface) => item.title)
}
.margin(10)
.backgroundColor('#fff')
.borderRadius(10)
.padding({ top: 15, bottom: 5 })
}.width('100%').height('100%').backgroundColor('#eee')
}
}

相对布局(RelativeContainer)

相对布局组件,用于复杂场景中元素对齐的布局。

RelativeContainer 就是采用相对布局的容器,支持容器内部的子元素设置相对位置关系。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个 RelativeContainer 的概念图,图中的虚线表示位置的依赖关系。

规则说明

1、容器内子组件区分水平方向,垂直方向:

​ (1)水平方向为 left,middle,right,对应容器的 HorizontalAlign.Start,HorizontalAlign.Center,HorizontalAlign.End。

​ (2)垂直方向为 top,center,bottom,对应容器的 VerticalAlign.Top,VerticalAlign.Center,VerticalAlign.Bottom。

2、子组件可以将容器或者其他子组件设为锚点:

​ (1)参与相对布局的容器内组件必须设置 id,不设置 id 的组件不显示,RelativeContainer 容器的固定 id 为 __container__。

​ (2)此子组件某一方向上的三个位置可以将容器或其它子组件的同方向三个位置为锚点,同方向上两个以上位置设置锚点以后会跳过第三个。

​ (3)前端页面设置的子组件尺寸大小不会受到相对布局规则的影响。子组件某个方向上设置两个或以上 alignRules 时不建议设置此方向尺寸大小。

​ (4)对齐后需要额外偏移可设置 offset

3、特殊情况:

​ (1)互相依赖,环形依赖时容器内子组件全部不绘制。

​ (2)同方向上两个以上位置设置锚点但锚点位置逆序时,此子组件大小为 0,即不绘制。

​ (3)容器不设置宽高时,容器与容器内子组件不绘制。

基本使用演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entry
@Component
struct Index {
build() {
Column() {
RelativeContainer() {
Row()
.width(100)
.height(100)
.backgroundColor(Color.Orange)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.id('row1')
.offset({ x: 20, y: 20 })
}.width(200).height(200).border({ width: 2, color: Color.Green }).id('rc')
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Entry
@Component
struct Index {
build() {
RelativeContainer() {
Text('1')
.id('text1')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Red)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
Text('2')
.id('text2')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Green)
.alignRules({
top: { anchor: 'text1', align: VerticalAlign.Bottom },
right: { anchor: 'text1', align: HorizontalAlign.Start }
})
Text('3')
.id('text3')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Blue)
.alignRules({
top: { anchor: 'text1', align: VerticalAlign.Bottom },
left: { anchor: 'text1', align: HorizontalAlign.Start }
})
Text('4')
.id('text4')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Yellow)
.alignRules({
top: { anchor: 'text1', align: VerticalAlign.Bottom },
left: { anchor: 'text1', align: HorizontalAlign.End }
})
Text('5')
.id('text5')
.width(100)
.height(100)
.fontSize(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.alignRules({
top: { anchor: 'text3', align: VerticalAlign.Bottom },
left: { anchor: 'text3', align: HorizontalAlign.Start }
})
}
}
}

实现底部弹出按钮案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@Entry
@Component
struct Index {
@State flag: boolean = false

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
List({ space: 10 }) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (item: number) => {
ListItem() {
Text(`${item}`)
.fontSize(30)
.width('100%')
.height(80)
.backgroundColor('#eee')
.borderRadius(10)
.textAlign(TextAlign.Center)
}
})
}.width('100%').height('100%').padding(10)

RelativeContainer() {
Button({ stateEffect: true }) {
Text('+').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('add')
.width(80)
.height(80)
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})

if (this.flag) {
Button() {
Text('A').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('A')
.width(80)
.height(80)
.backgroundColor(Color.Orange)
.alignRules({
bottom: { anchor: 'add', align: VerticalAlign.Top },
right: { anchor: 'add', align: HorizontalAlign.End }
})
.offset({ y: -30 })
.opacity(0.8)

Button() {
Text('B').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('B')
.width(80)
.height(80)
.backgroundColor(Color.Gray)
.alignRules({
bottom: { anchor: 'add', align: VerticalAlign.Top },
right: { anchor: 'add', align: HorizontalAlign.Start }
})
.offset({ x: -10, y: -10 })
.opacity(0.8)

Button() {
Text('C').textAlign(TextAlign.Center).fontColor(Color.White).fontSize(30)
}
.id('C')
.width(80)
.height(80)
.backgroundColor(Color.Green)
.alignRules({
top: { anchor: 'add', align: VerticalAlign.Top },
right: { anchor: 'add', align: HorizontalAlign.Start }
})
.offset({ x: -30 })
.opacity(0.8)
}

}
.width(88)
.height(88)
.onClick(() => {
this.flag = !this.flag
})
}
}
}
}

List 组件详解

List 适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。

List 包含 ListItem、ListItemGroup 子组件。

List 接口

1
List(value?:{space?: number | string, initialIndex?: number, scroller?: Scroller})

从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。

参数:

参数名 参数类型 必填 参数描述
space number | string 子组件主轴方向的间隔。默认值:0。说明:设置为除 -1 外其它负数或百分比时,按默认值显示。space 参数值小于 List 分割线宽度时,子组件主轴方向的间隔取分割线宽度。
initialIndex number 设置当前 List 初次加载时视口起始位置显示的 item 的索引值。默认值:0。说明:设置为除 -1 外其它负数或超过了当前 List 最后一个 item 的索引值时视为无效取值,无效取值按默认值显示。
scroller Scroller 可滚动组件的控制器。用于与可滚动组件进行绑定。说明:不允许和其它滚动类组件绑定同一个滚动控制对象。

List 属性

名称 参数类型 描述
listDirection Axis 设置 List 组件排列方向。默认值:Axis.Vertical。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
divider { strokeWidth: Length; color?: ResourceColor; startMargin?: Length; endMargin?: Length; } | null 设置 ListItem 分割线样式,默认无分割线。- strokeWidth:分割线的线宽。- color:分割线的颜色。- startMargin:分割线与列表侧边起始端的距离。- endMargin:分割线与列表侧边结束端的距离。从 API version 9 开始,该接口支持在ArkTS卡片中使用。endMargin + startMargin 不能超过列宽度。startMargin 和 endMargin 不支持设置百分比。List 的分割线画在主轴方向两个子组件之间,第一个子组件上方和最后一个子组件下方不会绘制分割线。多列模式下,ListItem 与 ListItem 之间的分割线起始边距从每一列的交叉轴方向起始边开始计算,其它情况从 List 交叉轴方向起始边开始计算。
scrollBar BarState 设置滚动条状态。默认值:BarState.Off 从 API version 9 开始,该接口支持在 ArkTS卡片中使用。
cachedCount number 设置列表中 ListItem / ListItemGroup 的预加载数量,其中 ListItemGroup 将作为一个整体进行计算,ListItemGroup 中的所有 ListItem 会一次性全部加载出来。具体使用可参考减少应用白块说明。默认值:1。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。说明:单列模式下,会在 List 显示的 ListItem 前后各缓存 cachedCount 个 ListItem。多列模式下,会在 List 显示的 ListItem 前后各缓存 cachedCount 个 ListItem。
editMode(deprecated) boolean 声明当前 List 组件是否处于可编辑模式。可参考示例 3 实现删除选中的 list 项。从 API version9 开始废弃。默认值:false。
edgeEffect EdgeEffect 设置组件的滑动效果。默认值:EdgeEffect.Spring。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
chainAnimation boolean 设置当前 List 是否启用链式联动动效,开启后列表滑动以及顶部和底部拖拽时会有链式联动的效果。链式联动效果:List 内的 listitem 间隔一定距离,在基本的滑动交互行为下,主动对象驱动从动对象进行联动,驱动效果遵循弹簧物理动效。默认值:false。- false:不启用链式联动。- true:启用链式联动。从 API version 9 开始,该接口支持在 ArkTS 卡片中 使用。
multiSelectable8+ boolean 是否开启鼠标框选。默认值:false。- false:关闭框选。- true:开启框选。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
lanes9+ number | LengthConstrain 以列模式为例(listDirection 为 Axis.Vertical):lanes 用于决定 List 组件在交叉轴方向按几列布局。默认值:1。规则如下:- lanes 为指定的数量时,根据指定的数量与 List 组件的交叉轴尺寸除以列数作为列的宽度。- lanes 设置了 {minLength,maxLength} 时, 根据 List 组件的宽度自适应决定 lanes 数量(即列数),保证缩放过程中 lane 的宽度符合 {minLength,maxLength} 的限制。其中,minLength 条件会被优先满足,即优先保证符合 ListItem 的交叉轴尺寸符合最小限制。- lanes 设置了 {minLength,maxLength},如果父组件交叉轴方向尺寸约束为无穷大时,固定按一列排列,列宽度按显示区域内最大的 ListItem 计算。- ListItemGroup 在多列模式下也是独占一行,ListItemGroup 中的 ListItem 按照 List 组件的 lanes 属性设置值来布局。- lanes 设置了 {minLength,maxLength} 时,计算列数会按照 ListItemGroup 的交叉轴尺寸计算。当 ListItemGroup 交叉轴尺寸与 List 交叉轴尺寸不一致时,ListItemGroup 中的列数与 List 中的列数可能不一样。该接口支持在 ArkTS 卡片中使用。
alignListItem9+ ListItemAlign List 交叉轴方向宽度大于 ListItem 交叉轴宽度 * lanes 时,ListItem 在 List 交叉轴方向的布局方式,默认为首部对齐。默认值:ListItemAlign.Start。该接口支持在 ArkTS 卡片中使用。
sticky9+ StickyStyle 配合 ListItemGroup 组件使用,设置 ListItemGroup 中 header 和 footer 是否要吸顶或吸底。默认值:StickyStyle.None。该接口支持在 ArkTS 卡片中使用。说明:sticky 属性可设置为 StickyStyle.Header | StickyStyle.Footer 以同时支持 header 吸顶和 footer 吸底。

List 事件

名称 功能描述
onItemDelete(deprecated) (event: (index: number) => boolean) 当 List 组件在编辑模式时,点击 ListItem 右边出现的删除按钮时触发。从 API version9 开始废弃。- index:被删除的列表项的索引值。
onScroll(event: (scrollOffset: number, scrollState: ScrollState) => void) 列表滑动时触发。- scrollOffset:滑动偏移量。- scrollState:当前滑动状态。使用控制器调用 ScrollEdge 和 ScrollToIndex 时不会触发,其余情况有滚动就会触发该事件。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
onScrollIndex(event: (start: number, end: number) => void) 列表滑动时触发。计算索引值时,ListItemGroup 作为一个整体占一个索引值,不计算 ListItemGroup 内部 ListItem 的索引值。- start:滑动起始位置索引值。- end:滑动结束位置索引值。触发该事件的条件:列表初始化时会触发一次,List 显示区域内第一个子组件的索引值或后一个子组件的索引值有变化时会触发。List 的边缘效果为弹簧效果时,在 List 滑动到边缘继续滑动和松手回弹过程不会触发 onScrollIndex 事件。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
onReachStart(event: () => void) 列表到达起始位置时触发。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。说明:List 初始化时如果 initialIndex 为 0 会触发一次,List 滚动到起始位置时触发一次。List 边缘效果为弹簧效果时,滑动经过起始位置时触发一次,回弹回起始位置时再触发 一次。
onReachEnd(event: () => void) 列表到达末尾位置时触发。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。说明:List 边缘效果为弹簧效果时,滑动经过末尾位置时触发一次,回弹回末尾位置时再触发一次。
onScrollFrameBegin9+ (event: (offset: number, state: ScrollState) => { offsetRemain }) 列表开始滑动时触发,事件参数传入即将发生的滑动量,事件处理函数中可根据应用场景计算实际需要的滑动量并作为事件处理函数的返回值返回,列表将按照返回值的实际滑动量进行滑动。- offset:即将发生的滑动量,单位 vp。- state:当前滑动状态。- offsetRemain:实际滑动量,单位 vp。触发该事件的条件:手指拖动 List、List 惯性滑动时每帧开始时触发;List 超出边缘回弹、 使用滚动控制器的滚动不会触发。该接口支持在 ArkTS 卡片中使用。说明:当 listDirection 的值为 Axis.Vertical 时,返回垂直方向滑动量,当 listDirection 的值为 Axis.Horizontal 时,返回水平方向滑动量。
onScrollStart9+(event: () => void) 列表滑动开始时触发。手指拖动列表或列表的滚动条触发的滑动开始时,会触发该事件。使用 Scroller 滑动控制器触发的带动画的滑动,动画开始时会触发该事件。该接口支持在 ArkTS 卡片中使用。
onScrollStop(event: () => void) 列表滑动停止时触发。手拖动列表或列表的滚动条触发的滑动,手离开屏幕并且滑动停止时会触发该事件;使用 Scroller 滑动控制器触发的带动画的滑动,动画停止会触发该事件。从 API version 9 开始,该接口支持在 ArkTS 卡片中使用。
onItemMove(event: (from: number, to: number) => boolean) 列表元素发生移动时触发。- from:移动前索引值。- to:移动后索引值。
onItemDragStart(event: (event: ItemDragInfo, itemIndex: number) => ((() => any) | void) 开始拖拽列表元素时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:被拖拽列表元素索引值。
onItemDragEnter(event: (event: ItemDragInfo) => void) 拖拽进入列表元素范围内时触发。- event:见 ItemDragInfo 对象说明。
onItemDragMove(event: (event: ItemDragInfo, itemIndex: number, insertIndex: number) => void) 拖拽在列表元素范围内移动时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:拖拽起始位置。- insertIndex:拖拽插入位置。
onItemDragLeave(event: (event: ItemDragInfo, itemIndex: number) => void) 拖拽离开列表元素时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:拖拽离开的列表元素索引值。
onItemDrop(event: (event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => void) 绑定该事件的列表元素可作为拖拽释放目标,当在列表元素内停止拖拽时触发。- event:见 ItemDragInfo 对象说明。- itemIndex:拖拽起始位置。- insertIndex:拖拽插入位置。- isSuccess:是否成功释放。说明:跨 List 拖拽时,当拖拽释放的位置绑定了 onItemDrop 时会返回 true,否则为 false。List 内部拖拽时,isSuccess 为 onItemMove 事件的返回值。

List 普通垂直列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entry
@Component
struct Index {
private arr: number[] = [0, 1, 2, 3, 5, 6, 7, 8]

build() {
Column() {
List({ space: 10, initialIndex: 0 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(item.toString())
.width('100%')
.height(100)
.fontSize(24)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
}, (item: number) => item.toString())
}
.listDirection(Axis.Vertical) // 排列方向
.divider({ strokeWidth: 2, color: 0xFF0000, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
.edgeEffect(EdgeEffect.None) // 滑动到边缘无效果
.width('100%')
.height('100%')
.padding(10)
}.width('100%').height('100%').backgroundColor(Color.Gray)
}
}

List 微信用户中心布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
interface UserListInterface {
title: string,
img: string
}

@Entry
@Component
struct Index {
@State list: UserListInterface[] = [
{
title: "服务",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/01.jpg"
},
{
title: "收藏",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/02.jpg"
},
{
title: "朋友圈",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/03.jpg"
},
{
title: "视频号",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/04.jpg"
},
{
title: "卡包",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/05.jpg"
},
{
title: "表情",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/06.jpg"
},
{
title: "设置",
img: "https://gitee.com/zch0304/images/raw/master/note/userlist/07.jpg"
}
]

build() {
Column() {
List({ space: 10, initialIndex: 0 }) {
ListItem() {
RelativeContainer() {
Image("https://gitee.com/zch0304/images/raw/master/note/test_icon4.jpg")
.id('avatar')
.width(80)
.height(80)
.objectFit(ImageFit.Fill)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.margin({ left: 20, top: 30 })

Text("明年今日")
.id('name')
.fontSize(18)
.alignRules({
left: { anchor: 'avatar', align: HorizontalAlign.End },
top: { anchor: 'avatar', align: VerticalAlign.Top }
})
.margin({ left: 10 })

Text("微信号:zhich")
.id('account')
.fontSize(14)
.alignRules({
left: { anchor: 'avatar', align: HorizontalAlign.End },
bottom: { anchor: 'avatar', align: VerticalAlign.Bottom }
})
.margin({ left: 10 })
}.width('100%').height('200vp').backgroundColor(Color.White)
}

ListItem() {
CustomItem({ model: this.list[0] })
}

ListItem() {
Column() {
ForEach(this.list, (item: UserListInterface, key) => {
if (key > 0 && key < this.list.length - 1) {
CustomItem({ model: item })
Divider()
.strokeWidth(1)
.color("#eee")
.padding({ left: 20, right: 20 })
}
}, (item: UserListInterface) => item.img)
}.backgroundColor(Color.White)
}

ListItem() {
CustomItem({ model: this.list[this.list.length-1] })
}

}
.edgeEffect(EdgeEffect.Spring)
.width('100%')
.height('100%')
}.width('100%').height('100%').backgroundColor(0xeeeeee)
}
}

@Component
struct CustomItem {
model: UserListInterface = { title: "", img: "" }

build() {
Row() {
Row() {
Image(this.model.img)
.width(28)
.height(28)
.objectFit(ImageFit.Cover)
Text(`${this.model.title}`).fontSize(16)
}

Image("https://gitee.com/zch0304/images/raw/master/note/userlist/arrow_forward.jpg")
.width(28)
.height(28)
}
.width('100%')
.backgroundColor(Color.White)
.padding(10)
.justifyContent(FlexAlign.SpaceBetween)
}
}

List 文章列表布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class Article {
key: string;
title: string;
img: string;
author: string;
date: string;

constructor(key: string, title: string, img: string, author: string, date: string) {
this.key = key;
this.title = title;
this.img = img;
this.author = author;
this.date = date;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { Article } from '../model/Article'

@Entry
@Component
struct Index {
@State articleList: Array<Article> = [
new Article(
'1',
'新手攻略|开启关怀模式,与家人更亲近~',
'https://gitee.com/zch0304/images/raw/master/note/article/01.png',
'国家电网',
'2024-12-2'
),
new Article(
'2',
'反诈课堂|光伏骗局套路多听我给您细细说!',
'https://gitee.com/zch0304/images/raw/master/note/article/02.png',
'国家电网',
'2024-4-2'
),
new Article(
'3',
'新手攻略| 联合办、网上办一次办,这些地方的用户注',
'https://gitee.com/zch0304/images/raw/master/note/article/03.png',
'国家电网',
'2024-1-12'
),
new Article(
'4',
'新手攻略|轻轻一点,电费一键查询',
'https://gitee.com/zch0304/images/raw/master/note/article/04.png',
'国家电网',
'2024-12-2'
),
new Article(
'5',
'关注|局地降温超16C!寒潮天气来袭,注意防寒保暖!',
'https://gitee.com/zch0304/images/raw/master/note/article/05.png',
'国家电网',
'2024-2-6'
),
]

build() {
Column() {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
Row() {
Column() {
Text(item.title).fontSize(16).fontWeight(FontWeight.Bold)
Text(`${item.author} ${item.date}`).fontSize(14)
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(HorizontalAlign.Start)

Image(item.img).width(100).height(68).margin({ left: 5 }).borderRadius(10)
}.height(80).alignItems(VerticalAlign.Center).padding({ bottom: 10, top: 10 })
}
}, (item: Article) => item.key)
}
.margin(10)
.padding(10)
.backgroundColor('#fff')
.divider({ strokeWidth: 1, color: '#eee' })
}.width('100%').height('100%').backgroundColor('#eee')
}
}

List 水平滑动列表

1
2
3
4
5
6
7
8
9
10
11
export class ListItemModel {
key: string;
title: string;
img: string

constructor(key: string, title: string, img: string) {
this.key = key;
this.title = title;
this.img = img;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { ListItemModel } from '../model/ListItemModel'

@Entry
@Component
struct Index {
@State list: Array<ListItemModel> = [
new ListItemModel(
'1',
'关怀模式',
'https://gitee.com/zch0304/images/raw/master/note/article/01.png'
),
new ListItemModel(
'2',
'光伏骗局套',
'https://gitee.com/zch0304/images/raw/master/note/article/02.png'
),
new ListItemModel(
'3',
'网办小助手',
'https://gitee.com/zch0304/images/raw/master/note/article/03.png'
),
new ListItemModel(
'4',
'电费一键查询',
'https://gitee.com/zch0304/images/raw/master/note/article/04.png'
)
]

build() {
Column() {
List({ space: 10 }) {
ForEach(this.list, (item: ListItemModel) => {
ListItem() {
Column() {
Image(item.img).width(100).height(68).margin({ top: 10 }).borderRadius(10)
Text(item.title).fontSize(16).margin({ top: 10 })
}
.width(100)
.height(110)
.alignItems(HorizontalAlign.Center)
}
}, (item: ListItemModel) => item.key)
}
.width('100%')
.height(120)
.padding({ left: 10, right: 10 })
.listDirection(Axis.Horizontal)
.alignListItem(ListItemAlign.Center)
.border({ width: 1, color: '#ccc' })
}.width('100%').height('100%').backgroundColor('#eee')
}
}

list Scroller 控制滚动位置

List 组件初始化时,可以通过 scroller 参数绑定一个 Scroller 对象,进行列表的滚动控制。通过 Scroller 对象的 scrollToIndex 方法使列表滚动到指定的列表项索引位置。如实现列表返回顶部,可以设置 scrollToIndex 为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entry
@Component
struct Index {
@State list: number[] = []
private listScroller: Scroller = new Scroller()

onPageShow(): void {
for (let i = 0; i < 10; i++) {
this.list.push(i)
}
}

build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
List({ space: 20, scroller: this.listScroller }) {
ForEach(this.list, (item: number) => {
ListItem() {
Text(`${item}`)
.width('100%')
.height(160)
.fontSize(28)
.textAlign(TextAlign.Center)
.backgroundColor('#fff')
.borderRadius(20)
}
}, (item: number) => item.toString())
}.width('100%').height('100%')

Button() {
Image('https://gitee.com/zch0304/images/raw/master/note/article/arrow_top.png')
.width(30)
.height(30)
}
.width(80)
.height(80)
.margin({ bottom: 10 })
.backgroundColor('#ccc')
.onClick(() => {
this.listScroller.scrollToIndex(0)
})
}
}.width('100%').height('100%').backgroundColor('#eee').padding(12)
}
}

ListItem 的 swipeAction 响应列表项侧滑

侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
interface SwipeInterface {
username: string,
info: string,
date: string,
icon: string,
}

@Entry
@Component
struct Index {
@State list: SwipeInterface[] = [
{
username: "小明",
info: "吃饭了吗",
date: "10-23",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon1.jpg"
},
{
username: "中明",
info: "你在哪里",
date: "10-22",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon2.jpg"
},
{
username: "大明",
info: "现在出发",
date: "10-22",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon3.jpg"
},
{
username: "明明",
info: "哈哈哈",
date: "10-21",
icon: "https://gitee.com/zch0304/images/raw/master/note/test_icon4.jpg"
},
]

build() {
Column() {
List({ space: 20 }) {
ForEach(this.list, (item: SwipeInterface, key) => {
ListItem() {
Row() {
Row() {
if (key == 1) {
Image(item.icon)
.width(60)
.height(60)
.margin({ right: 10 })
.borderRadius(100)
} else {
Badge({
count: 1,
position: BadgePosition.RightTop,
style: { badgeSize: 16, badgeColor: '#FA2A2D' }
}) {
Image(item.icon)
.width(60)
.height(60)
.margin({ right: 10 })
.borderRadius(100)
}
}
Column() {
Text(`${item.username}`).fontSize(16).fontWeight(FontWeight.Bold)
Text(`${item.info}`).fontSize(14).fontColor("#666")
}
.height('72%')
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.SpaceBetween)
}

Text(`${item.date}`)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.height(60)
.margin({ bottom: 10, top: 10 })
.swipeAction({ end: this.itemEnd.bind(this, key) }) // 设置侧滑属性
})
}
.padding(10)
.divider({
strokeWidth: 1, color: '#eee'
})
}.width('100%').height('100%')
}

@Builder
itemEnd(index: number) {
// 侧滑后尾端出现的组件
Button({ type: ButtonType.Circle }) {
Image("https://gitee.com/zch0304/images/raw/master/note/delete.png")
.width(26)
.height(26)
}
.onClick(() => {
this.list.splice(index, 1);
})
.width(40)
.height(40)
.margin({ left: 20 })
.backgroundColor(Color.Red)
}
}

Badge 给列表项添加标记

添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息。

1
2
3
4
5
6
7
8
9
Badge({
 count: 1,
 position: BadgePosition.RightTop,
 style: { badgeSize: 16, badgeColor: '#FA2A2D' }
}) {
 // Image组件实现消息联系人头像

 ...
}

ListItemGroup 汽车之家选车页面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class CarModel {
alphabet: string
carItem: Array<CarItemModel>

constructor(alphabet: string, carItem: Array<CarItemModel>) {
this.alphabet = alphabet
this.carItem = carItem
}
}

export class CarItemModel {
title: string

constructor(title: string) {
this.title = title
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { CarItemModel, CarModel } from '../model/CarModel'

const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

@Entry
@Component
struct Index {
@State selectedIndex: number = 0
private listScroller: Scroller = new Scroller()
@State carList: CarModel[] = [
new CarModel(
"A",
[
new CarItemModel("奥迪"),
new CarItemModel("奥拓"),
new CarItemModel("爱驰"),
new CarItemModel("阿尔特")
]
),
new CarModel(
"B",
[
new CarItemModel("奔驰"),
new CarItemModel("比亚迪"),
new CarItemModel("宝马"),
new CarItemModel("保时捷"),
new CarItemModel("标致")
]
),
new CarModel(
"C",
[
new CarItemModel("长安"),
new CarItemModel("长城"),
new CarItemModel("宝马"),
new CarItemModel("曹操汽车"),
new CarItemModel("成功汽车")
]
),
new CarModel(
"D",
[
new CarItemModel("大众"),
new CarItemModel("东风"),
new CarItemModel("大运"),
new CarItemModel("东南"),
new CarItemModel("大帝")
]
),
new CarModel(
"F",
[
new CarItemModel("丰田"),
new CarItemModel("福特"),
new CarItemModel("法拉利")
]
)
]

build() {
Column() {
Stack({ alignContent: Alignment.End }) {
List({ scroller: this.listScroller }) {
ForEach(this.carList, (car: CarModel) => {
ListItemGroup({ header: this.itemHead(car.alphabet) }) {
// 循环渲染 ListItem
ForEach(car.carItem, (item: CarItemModel) => {
ListItem() {
Text(`${item.title}`).width('100%').height(60).fontSize(16)
}.padding({ left: 10 })
})
}
})
}
.sticky(StickyStyle.Header) // 设置吸顶,实现粘性标题效果
.onScrollIndex((firstIndex: number) => { // 获取滚动的索引值
this.selectedIndex = firstIndex
})

// 字母表索引组件
AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
.selected(this.selectedIndex)
.onSelect((index: number) => {
this.listScroller.scrollToIndex(index - 1)
})
}
}.width('100%').height('100%')
}

@Builder
itemHead(text: string) {
// 列表分组的头部组件,对应联系人分组 A、B 等位置的组件
Text(text)
.fontSize(22)
.width('100%')
.padding(5)
.backgroundColor('#fff1f3f5')
}
}

Grid 网格布局

网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局 。

Grid 接口

参数:

参数名 参数类型 必填 参数描述
scroller Scroller 可滚动组件的控制器。用于与可滚动组件进行绑定。说明:不允许和其它滚动类组件绑定同一个滚动控制对象。

Grid 属性

名称 参数类型 描述
columnsTemplate string 设置当前网格布局列的数量,不设置时默认 1 列。例如, ‘1fr 1fr 2fr’ 是将父组件分 3 列,将父组件允许的宽分为 4 等份,第一列占 1 份,第二列占 1 份,第三列占 2 份。说明:设置为 ‘0fr’ 时,该列的列宽为 0,不显示 GridItem。设置为其它非法值时,GridItem 显示为固定 1 列。
rowsTemplate string 设置当前网格布局行的数量,不设置时默认 1 行。例如, ‘1fr 1fr 2fr’ 是将父组件分 3 行,将父组件允许的高分为 4 等份,第一行占 1 份,第二行占 1 份,第三行占 2 份。说明:设置为 ‘0fr’ 时,该行的行高为 0,这一行不显示 GridItem。设置为其它非法值时,GridItem 显示为固定 1 行。
columnsGap Length 设置列与列的间距。默认值:0。说明:设置为小于 0 的值时,按默认值显示。
rowsGap Length 设置行与行的间距。默认值:0。说明:设置为小于 0 的值时,按默认值显示。
scrollBar BarState 设置滚动条状态。默认值:BarState.Off。
scrollBarColor string | number | Color 设置滚动条的颜色。
scrollBarWidth string | number 设置滚动条的宽度。宽度设置后,滚动条正常状态和按压状态宽度均为滚动条的宽度值。默认值:4,单位:vp。
cachedCount number 设置预加载的 GridItem 的数量,只在 LazyForEach 中生效。具体使用可参考减少应用白块说明。默认值:1。说明:设置缓存后会在 Grid 显示区域上下各缓存 cachedCount * 列数个 GridItem。LazyForEach 超出显示和缓存范围的 GridItem 会被释放。设置为小于 0 的值时,按默认值显示。
editMode8+ boolean 设置 Grid 是否进入编辑模式,进入编辑模式可以拖拽 Grid 组件内部 GridItem。默认值:false。
layoutDirection8+ GridDirection 设置布局的主轴方向。默认值:GridDirection.Row。
maxCount8+ number 当 layoutDirection 是 Row/RowReverse 时,表示可显示的最大列数。当 layoutDirection 是 Column/ColumnReverse 时,表示可显示的最大行数。默认值:Infinity。说明:当 maxCount 小于 minCount 时,maxCount 和 minCount 都按默认值处理。设置为小于 0 的值时,按默认值显示。
minCount8+ number 当 layoutDirection 是 Row/RowReverse 时,表示可显示的最小列数。当 layoutDirection 是 Column/ColumnReverse 时,表示可显示的最小行数。默认值:1。说明:设置为小于 0 的值时,按默认值显示。
cellLength8+ number 当 layoutDirection 是 Row/RowReverse 时,表示一行的高度。当 layoutDirection 是 Column/ColumnReverse 时,表示一列的宽度。默认值:第一个元素的大小。
multiSelectable8+ boolean 是否开启鼠标框选。默认值:false。- false:关闭框选。- true:开启框选。
supportAnimation8+ boolean 是否支持动画。当前支持 GridItem 拖拽动画。默认值:false。

Grid 组件根据 rowsTemplate、columnsTemplate 属性的设置情况,可分为以下三种布局模式:

  • 行、列数量与占比同时设置:Grid 只展示固定行列数的元素,其余元素不展示,且 Grid 不可滚动。
  • 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。
  • 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且 Grid 不可滚动。

固定数量的网格

行、列数量与占比同时设置:Grid 只展示固定行列数的元素,其余元素不展示,且 Grid 不可滚动。通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid 组件提供了 rowsTemplate 和 columnsTemplate 属性用于设置网格布局行列数量与尺寸占比。rowsTemplate 和 columnsTemplate 属性值是一个由多个空格和 ‘数字+fr’ 间隔拼接的字符串,fr 的个数即网格布局的行或列数,fr 前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列的宽度。

如上图所示,构建的是一个三行三列的的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。 只要将 rowsTemplate 的值为 ‘1fr 1fr 1fr’,同时将 columnsTemplate 的值为 ‘1fr 2fr 1fr’,即可实现上述网格布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Entry
@Component
struct Index {
private arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']

build() {
Column() {
Grid() {
ForEach(this.arr, (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
}
}, (item: string) => item)
}
.height(300)
.columnsTemplate('1fr 2fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}.width('100%').padding(12).backgroundColor('#eee')
}
}

上下滚动的网格

只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entry
@Component
struct Index {
private arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']

build() {
Column() {
Grid() {
ForEach(this.arr, (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height(160)
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}.width('100%').padding(12).backgroundColor('#eee')
}
}

不均匀网格布局

在单个网格单元中,rowStart 和 rowEnd 属性表示指定当前元素起始行号和终点行号,columnStart 和 columnEnd 属性表示指定当前元素的起始列号和终点列号。 区块 3 横跨第三列和第四列,只要将区块 3 对应 GridItem 的 columnStart 和 columnEnd 设为 3 和 4。区块 4 横跨第二行和第三行,只要将区块 3 对应 GridItem 的 rowStart 和 rowEnd 设为 1 和 2。整个区块的高度是由 Grid 的高度决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@Entry
@Component
struct Index {
private arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']

build() {
Column() {
Grid() {
GridItem() {
Text('1')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('2')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('3')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}.columnStart(3).columnEnd(4)

GridItem() {
Text('4')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}.rowStart(2).rowEnd(3)

GridItem() {
Text('5')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('6')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('7')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}

GridItem() {
Text('8')
.width('100%')
.height('100%')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1e90ff')
.textAlign(TextAlign.Center)
}.columnStart(1).columnEnd(4)
}
.height(300)
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}.width('100%').padding(12).backgroundColor('#eee')
}
}

水平滚动的网格

可滚动的网格布局常用在文件管理、购物或视频列表等页面中如下图所示。在设置 Grid 的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置 rowsTemplate 或仅设置 columnsTemplate 属性,网格单元按照设置的方向排列,超出 Grid 显示区域后,Grid 拥有可滚动能力。

如果设置的是 columnsTemplate,Grid 的滚动方向为垂直方向;如果设置的是 rowsTemplate,Grid 的滚动方向为水平方向。

如上图所示的横向可滚动网格布局,只要设置 rowsTemplate 属性的值且不设置 columnsTemplate 属性,当内容超出 Grid 组件宽度时,Grid 可横向滚动进行内容展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Entry
@Component
struct Index {
@State arr: Array<string> = ['直播', '进口', '分类', '充值', '领券', '抽奖', '会员', '抽奖', '积分', '更多']
@State colors: Array<Color> = [
Color.Brown,
Color.Red,
Color.Orange,
Color.Blue,
Color.Grey,
Color.Pink,
Color.Red,
Color.Brown,
Color.Orange,
Color.Blue,
]

build() {
Column() {
Grid() {
ForEach(this.arr, (item: string, index) => {
GridItem() {
Row() {
Text(`${item}`)
.width(68)
.height(68)
.fontSize(16)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.backgroundColor(this.colors[index])
.borderRadius(68)
}.height('100%')
}.width('25%')
}, (item: string) => item)
}
.height(168)
.rowsTemplate('1fr 1fr')
}.width('100%').padding(12).backgroundColor('#eee')
}
}

组件导航和页面路由

组件导航

详细介绍请查看官方文档

Tabs

Tabs 组件的接口

参数:

参数名 参数类型 必填 参数描述
barPosition BarPosition 设置 Tabs 的页签位置。默认值:BarPosition.Start。
index number 设置当前显示页签的索引。默认值:0。说明:设置为小于 0 的值时按默认值显示。可选值为 [0, TabContent 子节点数量 - 1]。设置不同值时,默认生效切换动效,可以设置 animationDuration 为 0 关闭动画。
controller TabsController 设置 Tabs 控制器。
Tabs 组件属性
名称 参数类型 描述
vertical boolean 设置为 false 是为横向 Tabs,设置为 true 时为纵向 Tabs。默认值:false。
scrollable boolean 设置为 true 时可以通过滑动页面进行页面切换,为 false 时不可滑动切换页面。默认值:true。
barMode BarMode TabBar 布局模式,具体描述见 BarMode 枚举说明。默认值:BarMode.Fixed。Scrollable 每一个 TabBar 均使用实际布局宽度,超过总长度(横向 Tabs 的 barWidth,纵向 Tabs 的 barHeight)后可滑动。 Fixed 所有 TabBar 平均分配 barWidth 宽度(纵向时平均分配 barHeight 高度)。
barWidth number | Length8+ TabBar 的宽度值。默认值:未设置带样式的 TabBar 且 vertical 属性为 false 时,默认值为 Tabs 的宽度。未设置带样式的 TabBar 且 vertical 属性为 true 时,默认值为 56vp。 设置 SubTabbarStyle 样式且 vertical 属性为 false 时,默认值为 Tabs 的宽度。设置 SubTabbarStyle 样式且 vertical 属性为 true 时,默认值为 56vp。设置 BottomTabbarStyle 样式且 vertical 属性为 true 时,默认值为 96vp。设置 BottomTabbarStyle 样式且 vertical 属性为 false 时,默认值为 Tabs 的宽度。说明:设置为小于 0 或大于 Tabs 宽度值时,按默认值显示。
barHeight number | Length8+ TabBar 的高度值。默认值:未设置带样式的 TabBar 且 vertical 属性为 false 时,默认值为 56vp。未设置带样式的 TabBar 且 vertical 属性为 true 时,默认值为 Tabs 的高度。 设置 SubTabbarStyle 样式且 vertical 属性为 false 时,默认值为 56vp。设置 SubTabbarStyle 样式且 vertical 属性为 true 时,默认值为 Tabs 的高度。设置 BottomTabbarStyle 样式且 vertical 属性为 true 时,默认值为 Tabs 的高度。设置 BottomTabbarStyle 样式且 vertical 属性为 false 时,默认值为 56vp。说明:设置为小于 0 或大于 Tabs 高度值时,按默认值显示。
animationDuration number 点击 TabBar 页签切换 TabContent 的动画时长。不设置时,点击 TabBar 页签切换 TabContent 无动画。默认值:300。说明:该参数不支持百分比设置;设置为小于 0 时, 按默认值 300ms 显示。
基本布局

Tabs 组件的页面组成包含两个部分,分别是 TabContent 和 TabBar。TabContent 是内容页,TabBar 是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。

提示:

TabContent 组件不支持设置通用宽度属性,其宽度默认撑满 Tabs 父组件。

TabContent 组件不支持设置通用高度属性,其高度由 Tabs 父组件高度与 TabBar 组件高度决定。

顶部导航

Tabs 组件默认的 barPosition 参数为 Start,即顶部导航模式。

1
2
Tabs({ barPosition: BarPosition.Start }) {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entry
@Component
struct Index {
build() {
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')

TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')

TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')

TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar('我的')
}.backgroundColor('#eee')
}
}
底部导航

底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。

设置 barPosition 为 End 即可将导航栏设置在底部。

1
2
Tabs({ barPosition: BarPosition.End }) {
}
侧边导航

侧边导航是手机应用较为少见的一种导航模式,更多适用于平板横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。

实现侧边导航栏需要设置 Tabs 的属性 vertical 为 true。在底部导航和顶部导航实现中,其默认值为 false,表明内容页和导航栏垂直方向排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entry
@Component
struct Index {
build() {
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')

TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')

TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')

TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar('我的')
}.vertical(true).barWidth(100).barHeight(200).backgroundColor('#eee')
}
}
限制导航栏的滑动切换

默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航 + 顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。

控制滑动切换的属性为 scrollable,默认值为 true,表示可以滑动,若要限制滑动切换页签则需要设置为 false。

1
2
Tabs() {
}.scrollable(false)
可以滚动导航栏

滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。

滚动导航栏需要设置 Tabs 组件的 barMode 属性,默认情况下其值为 Fixed,表示为固定导航栏,设置为 Scrollable 即可设置为可滚动导航栏。

1
2
Tabs() {
}.barMode(BarMode.Scrollable)
自定义导航栏

对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。

系统默认情况下采用了下划线标志当前活跃的页签,而自定义导航栏需要自行实现相应的样式,用于区分当前活跃页签和未活跃页签。

设置自定义导航栏需要使用 tabBar 的参数,以其支持的 CustomBuilder 的方式传入自定义的函数组件样式。例如这里声明 TabBuilder 的自定义函数组件,传入参数包括页签文字 title,对应位置 index,以及选中状态和未选中状态的图片资源。通过当前活跃的 currentIndex 和页签对应的 targetIndex 匹配与否,决定 UI 显示的样式。

在 TabContent 对应 tabBar 属性中传入自定义函数组件,并传递相应的参数。

在不使用自定义导航栏时,系统默认的 Tabs 会实现切换逻辑。在使用了自定义导航栏后,切换页签的逻辑需要手动实现。即用户点击对应页签时,屏幕需要显示相应的内容页 。

切换指定页签需要使用 TabsController,TabsController 是 Tabs 组件的控制器,用于控制 Tabs 组件进行页签切换。通过 TabsController 的 changeIndex 方法来实现跳转至指定索引值对应的 TabContent 内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Entry
@Component
struct Index {
@State currentIndex: number = 0
private imgUrlArr: Array<string> = [
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_nor.png',
]
private controller = new TabsController()

build() {
Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar(this.TabBuilder('首页', 0, this.imgUrlArr[0], this.imgUrlArr[1]))

TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar(this.TabBuilder('推荐', 1, this.imgUrlArr[2], this.imgUrlArr[3]))

TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar(this.TabBuilder('发现', 2, this.imgUrlArr[4], this.imgUrlArr[5]))

TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar(this.TabBuilder('我的', 3, this.imgUrlArr[6], this.imgUrlArr[7]))
}
.backgroundColor('#eee')
.onChange((index: number) => {
this.changePage(index)
})
}

// 自定义导航页签的样式
@Builder
TabBuilder(title: string, targetIndex: number, selImg: string, norImg: string) {
Column() {
Image(this.currentIndex == targetIndex ? selImg : norImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex == targetIndex ? '#1296db' : '#707070')
.margin({ top: 5 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.changePage(targetIndex)
})
}

changePage(pageIndex: number) {
this.currentIndex = pageIndex
this.controller.changeIndex(pageIndex)
}
}
Tabs 页面模块化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { HomePage } from './tabs/HomePage'
import { RecommendPage } from './tabs/RecommendPage'
import { DiscoveryPage } from './tabs/DiscoveryPage'
import { MinePage } from './tabs/MinePage'

@Entry
@Component
struct Index {
@State currentIndex: number = 0
private imgUrlArr: Array<string> = [
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_home_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_recommend_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_discover_nor.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_sel.png',
'https://gitee.com/zch0304/images/raw/master/note/tab/tab_mine_nor.png',
]
private controller = new TabsController()

build() {
Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
TabContent() {
HomePage()
}
.tabBar(this.TabBuilder('首页', 0, this.imgUrlArr[0], this.imgUrlArr[1]))

TabContent() {
RecommendPage()
}
.tabBar(this.TabBuilder('推荐', 1, this.imgUrlArr[2], this.imgUrlArr[3]))

TabContent() {
DiscoveryPage()
}
.tabBar(this.TabBuilder('发现', 2, this.imgUrlArr[4], this.imgUrlArr[5]))

TabContent() {
MinePage()
}
.tabBar(this.TabBuilder('我的', 3, this.imgUrlArr[6], this.imgUrlArr[7]))
}
.backgroundColor('#eee')
.onChange((index: number) => {
this.changePage(index)
})
}

// 自定义导航页签的样式
@Builder
TabBuilder(title: string, targetIndex: number, selImg: string, norImg: string) {
Column() {
Image(this.currentIndex == targetIndex ? selImg : norImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex == targetIndex ? '#1296db' : '#707070')
.margin({ top: 5 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.changePage(targetIndex)
})
}

changePage(pageIndex: number) {
this.currentIndex = pageIndex
this.controller.changeIndex(pageIndex)
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct HomePage {
build() {
Column() {
AppBar({ title: '首页' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct RecommendPage {
build() {
Column() {
AppBar({ title: '推荐' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct DiscoveryPage {
build() {
Column() {
AppBar({ title: '发现' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
import { AppBar } from '../widget/AppBar'

@Component
export struct MinePage {
build() {
Column() {
AppBar({ title: '我的' })
}.width('100%').height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
export struct AppBar {
title: string = ""

build() {
Text(this.title)
.size({ width: '100%', height: 50 })
.backgroundColor('#d81e06')
.fontColor('#ffffff')
.textAlign(TextAlign.Center)
.fontSize(18)
}
}

页面路由(router)

页面路由指在应用程序中实现不同页面之间的跳转和数据传递。HarmonyOS 提供了 Router 模块,通过不同的 url 地址,可以方便地进行页面路由,轻松地访问不同的页面 。

Router 适用于模块间与模块内页面切换,通过每个页面的 url 实现模块间解耦。模块内页面跳转时,为了实现更好的转场动效场景不建议使用该模块,推荐使用 Navigation

路由跳转的几种方法

Router 模块提供了两种跳转模式,分别是 router.pushUrl() 和 router.replaceUrl()。这两种模式决定了目标页是否会替换当前页。

  • pushUrl:目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用 router.back() 方法返回到当前页。
  • replaceUrl:目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

页面栈的最大容量为 32 个页面。如果超过这个限制,可以调用 router.clear() 方法清空历史页面栈,释放内存空间。

同时,Router 模块提供了两种实例模式,分别是 Standard 和 Single。这两种模式决定了目标 url 是否会对应多个实例。

  • Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。
  • Single:单实例模式。即如果目标页的 url 在页面栈中已经存在同 url 页面,则离栈顶最近的同 url 页面会被移动到栈顶,并重新加载;如果目标页的 url 在页面栈中不存在同 url 页面,则按照标准模式跳转。
pushUrl + Standard模式

希望从主页点击一个按钮,跳转到另一个页面。同时,需要保留主页在页面栈中,以便返回时恢复状态。这种场景下,可以使用 pushUrl() 方法,并且使用 Standard 实例模式(或者省略)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import router from '@ohos.router'

@Entry
@Component
struct Index {
build() {
Column() {
Button("新闻页").onClick(() => {
this.onJumpClick()
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}

onJumpClick() {
router.pushUrl({ url: 'pages/News' }, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`)
return
}
console.info('Invoke pushUrl succeeded.');
})
}
}
replaceUrl+ Standard模式

有一个登录页(Login)和一个个人中心页(Profile),希望从登录页成功登录后,跳转到个人中心页。同时,销毁登录页,在返回时直接退出应用。这种场景下,可以使用 replaceUrl() 方法,并且使用 Standard 实例模式(或者省略)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import router from '@ohos.router'

@Entry
@Component
struct Login {
build() {
Column() {
Button("个人中心页").onClick(() => {
this.onJumpClick()
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}

onJumpClick() {
router.replaceUrl({ url: 'pages/Profile' }, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`)
return
}
console.info('Invoke pushUrl succeeded.');
})
}
}
pushUrl + Single模式

有一个设置页(Setting)和一个主题切换页(Theme),希望从设置页点击主题选项,跳转到主题切换页。同时,需要保证每次只有一个主题切换页存在于页面栈中,在返回时直接回到设置页。这种场景下,可以使用 pushUrl() 方法,并且使用 Single 实例模式。

replaceUrl + Single模式

有一个搜索结果列表页(SearchResult)和一个搜索结果详情页(SearchDetail),希望从搜索结果列表页点击某一项结果,跳转到搜索结果详情页。同时,如果该结果已经被查看过,则不需要再新建一个详情页,而是直接跳转到已经存在的详情页。这种场景下,可以使用 replaceUrl() 方法,并且使用 Single 实例模式。

路由跳转传值

如果需要在跳转时传递一些数据给目标页,则可以在调用 Router 模块的方法时,添加一个 params 属性,并指定一个对象作为参数。例如:

1
2
3
4
5
6
7
8
export class DataModel {
id: number = 0
info: DataModelInfo | null = null
}

export class DataModelInfo {
name: string | null = null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import router from '@ohos.router'
import { DataModel } from '../model/DataModel'

@Entry
@Component
struct Index {
build() {
Column() {
Button("新闻页").onClick(() => {
this.onJumpClick()
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}

onJumpClick() {
let param: DataModel = {
id: 123,
info: {
name: '张三'
}
};
router.pushUrl({ url: 'pages/News', params: param }, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`)
return
}
console.info('Invoke pushUrl succeeded.');
})
}
}

在目标页面中,可以通过调用 Router 模块的 getParams() 方法来获取传递过来的参数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import router from '@ohos.router'
import { DataModel } from '../model/DataModel'

@Entry
@Component
struct News {
@State message: string = '新闻页面'

onPageShow(): void {
const dataModel = router.getParams() as DataModel
const id = dataModel.id
const name = dataModel.info?.name
}

build() {
}
}

页面返回

当用户在一个页面完成操作后,通常需要返回到上一个页面或者指定页面,这就需要用到页面返回功能。在返回的过程中,可能需要将数据传递给目标页面,这就需要用到数据传递功能。

可以使用以下几种方式返回页面:

  • 方式一:返回到上一个页面。

    1
    2
    import router from '@ohos.router'
    router.back();

    这种方式会返回到上一个页面,即上一个页面在页面栈中的位置。但是,上一个页面必须存在于页面栈中才能够返回,否则该方法将无效。

  • 方式二:返回到指定页面。

    返回普通页面:

    1
    2
    3
    4
    import router from '@ohos.router'
    router.back({
    url: 'pages/Home'
    });

    返回命名路由页面:

    1
    2
    3
    4
    import router from '@ohos.router'
    router.back({
    url: 'myPage' // myPage 为返回的命名路由页面别名
    });

    这种方式可以返回到指定页面,需要指定目标页面的路径。目标页面必须存在于页面栈中才能够返回。

  • 方式三:返回到指定页面,并传递自定义参数信息。

    返回到普通页面:

    1
    2
    3
    4
    5
    6
    7
    import router from '@ohos.router'
    router.back({
    url: 'pages/Home',
    params: {
    info: '来自Home页'
    }
    });

    返回命名路由页面:

    1
    2
    3
    4
    5
    6
    7
    import router from '@ohos.router'
    router.back({
    url: 'myPage', // myPage 为返回的命名路由页面别名
    params: {
    info: '来自Home页'
    }
    });

    这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义参数信息。这些参数信息可以在目标页面中通过调用 router.getParams() 方法进行获取和解析。

在目标页面中,在需要获取参数的位置调用 router.getParams() 方法即可,例如在 onPageShow() 生命周期回调中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import router from '@ohos.router'

@Entry
@Component
struct Home {
@State message: string = 'Hello World'

onPageShow() {
const params = router.getParams() as Record<string, string>; // 获取传递过来的参数对象
if (params) {
const info: string = params.info as string; // 获取 info 属性的值
}
}
...
}

当使用 router.back() 方法返回到指定页面时,原栈顶页面(包括)到指定页面(不包括)之间的所有页面栈都将从栈中弹出并销毁。另外,如果使用 router.back() 方法返回到原来的页面,原页面不会被重复创建,因此使用 @State 声明的变量不会重复声明,也不会触发页面的 aboutToAppear() 生命周期回调。如果需要在原页面中使用返回页面传递的自定义参数,可以在需要的位置进行参数解析。例如,在 onPageShow() 生命周期回调中进行参数解析。

页面返回前增加一个询问框

在开发应用时,为了避免用户误操作或者丢失数据,有时候需要在用户从一个页面返回到另一个页面之前,弹出一个询问框,让用户确认是否要执行这个操作。

系统默认询问框

如果想要在目标界面开启页面返回询问框,需要在调用 router.back() 方法之前,通过调用 router.showAlertBeforeBackPage() 方法设置返回询问框的信息。例如,在支付页面中定义一个返回按钮的点击事件处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import router from '@ohos.router'
import { BusinessError } from '@ohos.base'

// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
// 调用 router.showAlertBeforeBackPage() 方法,设置返回询问框的信息
try {
router.showAlertBeforeBackPage({
message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
});
} catch (err) {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`Invoke showAlertBeforeBackPage failed, code is ${code}, message is ${message}`)
}

// 调用 router.back() 方法,返回上一个页面
router.back()
}

其中,router.showAlertBeforeBackPage() 方法接收一个对象作为参数,该对象包含以下属性:

  • message:string 类型,表示询问框的内容。

如果调用成功,则会在目标界面开启页面返回询问框;如果调用失败,则会抛出异常,并通过 err.code 和 err.message 获取错误码和错误信息。

当用户点击“返回”按钮时,会弹出确认对话框,询问用户是否确认返回。选择“取消”将停留在当前页目标页面;选择“确认”将触发 router.back() 方法,并根据参数决定如何执行跳转。

自定义询问框

自定义询问框的方式,可以使用弹窗或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。本文以弹窗为例,介绍如何实现自定义询问框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import router from '@ohos.router'
import promptAction from '@ohos.promptAction'
import { BusinessError } from '@ohos.base'

function onBackClick() {
// 弹出自定义的询问框
promptAction.showDialog({
message: '您还没有完成支付,确定要返回吗?',
buttons: [
{
text: '取消',
color: '#FF0000'
},
{
text: '确认',
color: '#0099FF'
}
]
}).then((result:promptAction.ShowDialogSuccessResponse) => {
if (result.index === 0) {
// 用户点击了“取消”按钮
console.info('User canceled the operation.')
} else if (result.index === 1) {
// 用户点击了“确认”按钮
console.info('User confirmed the operation.')
// 调用 router.back() 方法,返回上一个页面
router.back();
}
}).catch((err:Error) => {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`Invoke showDialog failed, code is ${code}, message is ${message}`)
})
}

当用户点击“返回”按钮时,会弹出自定义的询问框,询问用户是否确认返回。选择“取消”将停留在当前页目标页面;选择“确认”将触发 router.back() 方法,并根据参数决定如何执行跳转。

命名路由

在开发中为了跳转到共享包 Har 或者 Hsp 中的页面(即共享包中路由跳转),可以使用 router.pushNamedRoute() 来实现。

在想要跳转到的共享包 Har 或者 Hsp 页面里,给 @Entry 修饰的自定义组件 命名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// library/src/main/ets/pages/Index.ets
// library 为新建共享包自定义的名字
@Entry({ routeName: 'myPage' })
@Component
export struct MyComponent {
build() {
Row() {
Column() {
Text('Library Page')
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}

配置成功后需要在跳转的页面中引入命名路由的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import router from '@ohos.router'
import { BusinessError } from '@ohos.base'
import('@ohos/library/src/main/ets/pages/Index') // 引入共享包中的命名路由页面
@Entry
@Component
struct Index {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('Hello World')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.margin({ top: 20 })
.backgroundColor('#ccc')
.onClick(() => { // 点击跳转到其他共享包中的页面
try {
router.pushNamedRoute({
name: 'myPage',
params: {
data1: 'message',
data2: {
data3: [123, 456, 789]
}
}
})
} catch (err) {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`);
}
})
}
.width('100%')
.height('100%')
}
}

使用命名路由方式跳转时,需要在当前应用包的 oh-package.json5 文件中配置依赖。例如:

1
2
3
4
"dependencies": {
"@ohos/library": "file:../library",
...
}

状态管理

管理组件拥有的状态

@State、@Prop、@Link、@Provide、@Consume、@Observed、@ObjectLink 和 @Watch 用于管理页面级变量的状态。

装饰器 装饰内容 说明
@State 基本数据类型,类,数组 修饰的状态数据被修改时会触发组件的 build 方法进行UI界面更新。
@Prop 基本数据类型 修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的 UI。
@Link 基本数据类型,类,数组 父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。
@Provide 基本数据类型,类,数组 @Provide 作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。
@Consume 基本数据类型,类,数组 @Consume 装饰的变量在感知到 @Provide 装饰的变量更新后,会触发当前自定义组件的重新渲染。
@Observed @Observed 应用于类,表示该类中的数据变更被 UI 页面管理。(用不到)
@ObjectLink 被 @Observed 所装饰类的对象 装饰的状态数据被修改时,在父组件或者其它兄弟组件内与它关联的状态数据所在的组件都会更新 UI。(用不到)
@Watch 基本数据类型,类,数组 @Watch 应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用 @Watch 为状态变量设置回调函数。

@State

修饰的状态数据被修改时会触发组件的 build 方法进行UI界面更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Entry
@Component
struct Index {
@State count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
Box({ count: this.count, color: Color.Pink }).margin({ left: 10 })
Box({ count: this.count, color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct Box {
count: number = 0
color: Color = Color.Black

build() {
Column() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
}
}

Index 组件的 count 改变了,不会改变 Box 组件的 count。

@Prop

修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entry
@Component
struct Index {
@State count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
Box({ count: this.count, color: Color.Pink }).margin({ left: 10 })
Box({ count: this.count, color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct Box {
@Prop count: number
color: Color = Color.Black

build() {
Column() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.count++ // 只有当前组件有效,无法改变父组件的 count
})
}
}

Index 组件的 count 改变了,会改变到 Box 组件的 count。但是 Box 组件的 count 改变了,不会改变到 Index 组件的 count。

注意:传参使用 $。

父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Entry
@Component
struct Index {
@State count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
Box({ count: $count, color: Color.Pink }).margin({ left: 10 })
Box({ count: $count, color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct Box {
@Link count: number
color: Color = Color.Black

build() {
Column() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.count++ // 当前组件有效,也改变父组件的 count
})
}
}

Index 组件和 Box 组件的 count 相互绑定,改变一方的 count 也会改变到另一方的 count。

@Provide @Consume

@Provide 和 @Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide 和 @Consume 摆脱参数传递机制的束缚,实现跨层级传递。

Index 组件调用了 SecondBox 组件,SecondBox 组件调用了 ThirdBox 组件,我们要实现的功能是 Index 组件和 ThirdBox 组件直接的数据相互绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Entry
@Component
struct Index {
@Provide('count') count: number = 0

build() {
Row() {
Text(`${this.count}`).fontSize(30)
SecondBox({ color: Color.Pink }).margin({ left: 10 })
SecondBox({ color: Color.Brown }).margin({ left: 10 })
Button() {
Text("+").fontWeight(FontWeight.Bold).fontSize(30).fontColor(Color.White)
}
.width(60)
.height(60)
.margin({ left: 50 })
.onClick(() => {
this.count++
})
}
.width('100%')
.height('100%')
.backgroundColor('#eee')
.justifyContent(FlexAlign.Center)
}
}

@Component
struct SecondBox {
color: Color = Color.Black

build() {
Column() {
ThirdBox()
}
.width(60)
.height(60)
.backgroundColor(this.color)
.justifyContent(FlexAlign.Center)
}
}

@Component
struct ThirdBox {
@Consume('count') count: number

build() {
Text(`${this.count}`).fontSize(30).fontColor(Color.White)
.onClick(() => {
this.count++
})
}
}

Index 组件到 ThirdBox 组件之间的 count 相互绑定。

@Watch

@Watch 用于监听状态变量的变化,当状态变量变化时,@Watch 的回调方法将被调用。@Watch 在 ArkUI 框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为 false 的情况下,就会触发 @Watch 的回调。

@Watch 可用于购物车计算总价,或者实现计算器功能等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Entry
@Component
struct Index {
@State @Watch('change') count: number = 1
@State @Watch('change') pow: number = 2
@State res: number = 1

change() {
this.res = Math.pow(this.count, this.pow)
}

build() {
Column() {
Row() {
Text('基数:').fontSize(20)
TextInput({ text: this.count.toString() })
.type(InputType.Number)
.layoutWeight(1)
.height(40)
.borderRadius(10)
.onChange((value) => {
if (value == null || value == '' || value == undefined) {
value = '0'
}
this.count = parseInt(value)
})
.margin({ bottom: 20 })
}

Row() {
Text('次幂:').fontSize(20)
TextInput({ text: this.pow.toString() })
.type(InputType.Number)
.layoutWeight(1)
.height(40)
.borderRadius(10)
.onChange((value) => {
if (value == null || value == '' || value == undefined) {
value = '0'
}
this.pow = parseInt(value)
})
.margin({ bottom: 20 })
}

Row() {
Text('结果:' + this.res).fontSize(30)
}
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#eee')
}
}

管理应用拥有的状态

概述

上一节介绍的装饰器仅能在页面内,即一个组件树上共享状态变量。如果要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS 根据不同特性,提供了多种应用状态管理的能力:

  • LocalStorage:页面级UI状态存储,通常用于 UIAbility 内、页面间的状态共享。
  • AppStorage:特殊的单例 LocalStorage 对象,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。
  • PersistentStorage:持久化存储 UI 状态,通常和 AppStorage 配合使用,选择 AppStorage 存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。
  • Environment:应用程序运行的设备的环境参数,环境参数会同步到 AppStorage 中,可以和 AppStorage 搭配使用。

LocalStorage:页面级 UI 状态存储

LocalStorage 是页面级的 UI 状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。LocalStorage 支持 UIAbility 实例内多个页面间状态共享。

概述

LocalStorage 是 ArkTS 为构建页面级别状态变量提供存储的内存内“数据库”。

  • 应用程序可以创建多个 LocalStorage 实例,LocalStorage 实例可以在页面内共享,也可以通过 GetShared 接口,实现跨页面、UIAbility 实例内共享。
  • 组件树的根节点,即被 @Entry 装饰的 @Component,可以被分配一个 LocalStorage 实例,此组件的所有子组件实例将自动获得对该 LocalStorage 实例的访问权限。
  • 被 @Component 装饰的组件最多可以访问一个 LocalStorage 实例和 AppStorage,未被 @Entry 装饰的组件不可被独立分配 LocalStorage 实例,只能接受父组件通过 @Entry 传递来的 LocalStorage 实例。一个LocalStorage 实例在组件树上可以被分配给多个组件。
  • LocalStorage 中的所有属性都是可变的。

应用程序决定 LocalStorage 对象的生命周期。当应用释放最后一个指向 LocalStorage 的引用时,比如销毁最后一个自定义组件,LocalStorage 将被 JS Engine 垃圾回收。

LocalStorage 根据与 @Component 装饰的组件的同步类型不同,提供了两个装饰器:

  • @LocalStorageProp:@LocalStorageProp 装饰的变量与 LocalStorage 中给定属性建立单向同步关系。
  • @LocalStorageLink:@LocalStorageLink 装饰的变量与 LocalStorage 中给定属性建立双向同步关系。
限制条件
  • LocalStorage 创建后,命名属性的类型不可更改。后续调用 Set 时必须使用相同类型的值。
  • LocalStorage 是页面级存储,getShared 接口仅能获取当前 Stage 通过 windowStage.loadContent 传入的 LocalStorage 实例,否则返回 undefined。例子可见 将 LocalStorage 实例从 UIAbility 共享到一个或多个视图
@LocalStorageProp

要建立 LocalStorage 和自定义组件的联系,需要使用 @LocalStorageProp 和 @LocalStorageLink 装饰器。使用 @LocalStorageProp(key)/@LocalStorageLink(key) 装饰组件内的变量,key 标识了 LocalStorage 的属性。

当自定义组件初始化的时候,@LocalStorageProp(key)/@LocalStorageLink(key) 装饰的变量会通过给定的 key,绑定 LocalStorage 对应的属性,完成初始化。本地初始化是必要的,因为无法保证 LocalStorage 一定存在给定的 key。

@LocalStorageProp(key) 是和 LocalStorage 中 key 对应的属性建立单向数据同步,修改 @LocalStorageProp(key) 在本地的值,不会同步回 LocalStorage 中。相反,如果 LocalStorage 中 key 对应的属性值发生改变,例如通过 set 接口对 LocalStorage 中的值进行修改,改变会同步给 @LocalStorageProp(key),并覆盖掉本地的值。

装饰器使用规则说明:

@LocalStorageProp变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和LocalStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见LocalStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@LocalStorageProp(“AA”) a: number | null = null是推荐的,不推荐@LocalStorageProp(“AA”) a: number = null。
同步类型 单向同步:从LocalStorage的对应属性到组件的状态变量。组件本地的修改是允许的,但是LocalStorage中给定的属性一旦发生变化,将覆盖本地的修改。
被装饰变量的初始值 必须指定,如果LocalStorage实例中不存在属性,则用该初始值初始化该属性,并存入LocalStorage中。

变量的传递/访问规则说明:

传递/访问 说明
从父节点初始化和更新 禁止,@LocalStorageProp不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。
初始化子节点 支持,可用于初始化@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现:

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  • 被@LocalStorageProp装饰的变量的值的变化不会同步回LocalStorage里。
  • @LocalStorageProp装饰的变量变化会使当前自定义组件中关联的组件刷新。
  • LocalStorage(key)中值的变化会引发所有被@LocalStorageProp对应key装饰的变量的变化,会覆盖@LocalStorageProp本地的改变。

如果需要将自定义组件的状态变量的更新同步回 LocalStorage,就需要用到 @LocalStorageLink。

@LocalStorageLink(key) 是和 LocalStorage 中 key 对应的属性建立双向数据同步

  • 本地修改发生,该修改会被写回 LocalStorage 中。

  • LocalStorage 中的修改发生后,该修改会被同步到所有绑定 LocalStorage 对应 key 的属性上,包括单向(@LocalStorageProp 和通过 prop 创建的单向绑定变量)、双向(@LocalStorageLink 和通过 link 创建的双向绑定变量)变量。

装饰器使用规则说明:

@LocalStorageLink变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和LocalStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见LocalStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@LocalStorageLink(“AA”) a: number | null = null是推荐的,不推荐@LocalStorageLink(“AA”) a: number = null。
同步类型 双向同步:从LocalStorage的对应属性到自定义组件,从自定义组件到LocalStorage对应属性。
被装饰变量的初始值 必须指定,如果LocalStorage实例中不存在属性,则用该初始值初始化该属性,并存入LocalStorage中。

变量的传递/访问规则说明:

传递/访问 说明
从父节点初始化和更新 禁止,@LocalStorageLink不支持从父节点初始化,只能从LocalStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。
初始化子节点 支持,可用于初始化@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现:

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  1. 当@LocalStorageLink(key)装饰的数值改变被观察到时,修改将被同步回LocalStorage对应属性键值key的属性中。
  2. LocalStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@LocalStorageLink和单向@LocalStorageProp)都将同步修改。
  3. 当@LocalStorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回LocalStorage中,还会引起所属的自定义组件的重新渲染。
使用场景
应用逻辑使用 LocalStorage
1
2
3
4
5
6
7
8
9
let para: Record<string,number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para); // 创建新实例并使用给定对象初始化
let propA: number | undefined = storage.get('PropA') // propA == 47
let link1: SubscribedAbstractProperty<number> = storage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = storage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = storage.prop('PropA'); // prop.get() == 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
从 UI 内部使用 LocalStorage

除了应用程序逻辑使用 LocalStorage,还可以借助 LocalStorage 相关的两个装饰器 @LocalStorageProp 和 @LocalStorageLink,在 UI 组件内部获取到 LocalStorage 实例中存储的状态变量。

本示例以 @LocalStorageLink 为例,展示了:

  • 使用构造函数创建 LocalStorage 实例 storage;
  • 使用 @Entry 装饰器将 storage 添加到 CompA 顶层组件中;
  • @LocalStorageLink 绑定 LocalStorage 对给定的属性,建立双向数据同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class PropB {
code: number;

constructor(code: number) {
this.code = code;
}
}
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
storage.setOrCreate('PropB', new PropB(50));

@Component
struct Child {
// @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
@LocalStorageLink('PropA') childLinkNumber: number = 1;
// @LocalStorageLink变量装饰器与LocalStorage中的'PropB'属性建立双向绑定
@LocalStorageLink('PropB') childLinkObject: PropB = new PropB(0);

build() {
Column() {
Button(`Child from LocalStorage ${this.childLinkNumber}`) // 更改将同步至LocalStorage中的'PropA'以及Parent.parentLinkNumber
.onClick(() => {
this.childLinkNumber += 1;
})
Button(`Child from LocalStorage ${this.childLinkObject.code}`) // 更改将同步至LocalStorage中的'PropB'以及Parent.parentLinkObject.code
.onClick(() => {
this.childLinkObject.code += 1;
})
}
}
}
// 使LocalStorage可从@Component组件访问
@Entry(storage)
@Component
struct CompA {
// @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
@LocalStorageLink('PropA') parentLinkNumber: number = 1;
// @LocalStorageLink变量装饰器与LocalStorage中的'PropB'属性建立双向绑定
@LocalStorageLink('PropB') parentLinkObject: PropB = new PropB(0);

build() {
Column({ space: 15 }) {
Button(`Parent from LocalStorage ${this.parentLinkNumber}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already
.onClick(() => {
this.parentLinkNumber += 1;
})

Button(`Parent from LocalStorage ${this.parentLinkObject.code}`) // initial value from LocalStorage will be 50, because 'PropB' initialized already
.onClick(() => {
this.parentLinkObject.code += 1;
})
// @Component子组件自动获得对CompA LocalStorage实例的访问权限。
Child()
}
}
}
@LocalStorageProp 和 LocalStorage 单向同步的简单场景

在下面的示例中,CompA 组件和 Child 组件分别在本地创建了与 storage 的 ‘PropA’ 对应属性的单向同步的数据,我们可以看到:

  • CompA 中对 this.storageProp1 的修改,只会在 CompA 中生效,并没有同步回 storage;
  • Child 组件中,Text 绑定的 storageProp2 依旧显示 47。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
// 使LocalStorage可从@Component组件访问
@Entry(storage)
@Component
struct CompA {
// @LocalStorageProp变量装饰器与LocalStorage中的'PropA'属性建立单向绑定
@LocalStorageProp('PropA') storageProp1: number = 1;

build() {
Column({ space: 15 }) {
// 点击后从47开始加1,只改变当前组件显示的storageProp1,不会同步到LocalStorage中
Button(`Parent from LocalStorage ${this.storageProp1}`)
.onClick(() => {
this.storageProp1 += 1
})
Child()
}
}
}

@Component
struct Child {
// @LocalStorageProp变量装饰器与LocalStorage中的'PropA'属性建立单向绑定
@LocalStorageProp('PropA') storageProp2: number = 2;

build() {
Column({ space: 15 }) {
// 当CompA改变时,当前storageProp2不会改变,显示47
Text(`Parent from LocalStorage ${this.storageProp2}`)
}
}
}

下面的示例展示了 @LocalStorageLink 装饰的数据和 LocalStorage 双向同步的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 构造LocalStorage实例
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
// 调用link(api9以上)接口构造'PropA'的双向同步数据,linkToPropA 是全局变量
let linkToPropA: SubscribedAbstractProperty<object> = storage.link('PropA');

@Entry(storage)
@Component
struct CompA {

// @LocalStorageLink('PropA')在CompA自定义组件中创建'PropA'的双向同步数据,初始值为47,因为在构造LocalStorage已经给“PropA”设置47
@LocalStorageLink('PropA') storageLink: number = 1;

build() {
Column() {
Text(`incr @LocalStorageLink variable`)
// 点击“incr @LocalStorageLink variable”,this.storageLink加1,改变同步回storage,全局变量linkToPropA也会同步改变

.onClick(() => {
this.storageLink += 1
})

// 并不建议在组件内使用全局变量linkToPropA.get(),因为可能会有生命周期不同引起的错误。
Text(`@LocalStorageLink: ${this.storageLink} - linkToPropA: ${linkToPropA.get()}`)
}
}
}
兄弟组件之间同步状态变量

下面的示例展示了通过 @LocalStorageLink 双向同步兄弟组件之间的状态。

先看 Parent 自定义组件中发生的变化:

  1. 点击 “playCount ${this.playCount} dec by 1”,this.playCount 减 1,修改同步回 LocalStorage 中,Child 组件中的 playCountLink 绑定的组件会同步刷新;
  2. 点击 “countStorage ${this.playCount} incr by 1”,调用 LocalStorage 的 set 接口,更新 LocalStorage 中 “countStorage” 对应的属性,Child 组件中的 playCountLink 绑定的组件会同步刷新;
  3. Text 组件 “playCount in LocalStorage for debug ${storage.get(‘countStorage’)}” 没有同步刷新,因为 storage.get(‘countStorage’) 返回的是常规变量,常规变量的更新并不会引起 Text 组件的重新渲染。

Child 自定义组件中的变化:

  1. playCountLink 的刷新会同步回 LocalStorage,并且引起兄弟组件和父组件相应的刷新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
let ls: Record<string, number> = { 'countStorage': 1 }
let storage: LocalStorage = new LocalStorage(ls);

@Component
struct Child {
// 子组件实例的名字
label: string = 'no name';
// 和LocalStorage中“countStorage”的双向绑定数据
@LocalStorageLink('countStorage') playCountLink: number = 0;

build() {
Row() {
Text(this.label)
.width(50).height(60).fontSize(12)
Text(`playCountLink ${this.playCountLink}: inc by 1`)
.onClick(() => {
this.playCountLink += 1;
})
.width(200).height(60).fontSize(12)
}.width(300).height(60)
}
}

@Entry(storage)
@Component
struct Parent {
@LocalStorageLink('countStorage') playCount: number = 0;

build() {
Column() {
Row() {
Text('Parent')
.width(50).height(60).fontSize(12)
Text(`playCount ${this.playCount} dec by 1`)
.onClick(() => {
this.playCount -= 1;
})
.width(250).height(60).fontSize(12)
}.width(300).height(60)

Row() {
Text('LocalStorage')
.width(50).height(60).fontSize(12)
Text(`countStorage ${this.playCount} incr by 1`)
.onClick(() => {
storage.set<number | undefined>('countStorage', Number(storage.get<number>('countStorage')) + 1);
})
.width(250).height(60).fontSize(12)
}.width(300).height(60)

Child({ label: 'ChildA' })
Child({ label: 'ChildB' })

Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`)
.width(300).height(60).fontSize(12)
}
}
}
将 LocalStorage 实例从 UIAbility 共享到一个或多个视图

上面的实例中,LocalStorage 的实例仅仅在一个 @Entry 装饰的组件和其所属的子组件(一个页面)中共享,如果希望其在多个视图中共享,可以在所属 UIAbility 中创建 LocalStorage 实例,并调用 windowStage.loadContent

1
2
3
4
5
6
7
8
9
10
11
12
// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
para:Record<string, number> = { 'PropA': 47 };
storage: LocalStorage = new LocalStorage(this.para);

onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', this.storage);
}
}

说明:

在 UI 页面通过 getShared 接口获取通过 loadContent 共享的 LocalStorage 实例。

LocalStorage.getShared() 只在模拟器或者实机上才有效,在 Previewer 预览器中使用不生效。

在下面的用例中,Index 页面中的 propA 通过 getShared() 方法获取到共享的 LocalStorage 实例。点击 Button 跳转到 Page 页面,点击 Change propA 改变 propA 的值,back 回 Index 页面后,页面中 propA 的值也同步修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// index.ets
import router from '@ohos.router';

// 通过getShared接口获取stage共享的LocalStorage实例
let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Index {
// can access LocalStorage instance using
// @LocalStorageLink/Prop decorated variables
@LocalStorageLink('PropA') propA: number = 1;

build() {
Row() {
Column() {
Text(`${this.propA}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("To Page")
.onClick(() => {
router.pushUrl({
url: 'pages/Page'
})
})
}
.width('100%')
}
.height('100%')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Page.ets
import router from '@ohos.router';

let storage = LocalStorage.getShared()

@Entry(storage)
@Component
struct Page {
@LocalStorageLink('PropA') propA: number = 2;

build() {
Row() {
Column() {
Text(`${this.propA}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)

Button("Change propA")
.onClick(() => {
this.propA = 100;
})

Button("Back Index")
.onClick(() => {
router.back()
})
}
.width('100%')
}
}
}

说明:

对于开发者更建议使用这个方式来构建 LocalStorage 的实例,并且在创建 LocalStorage 实例的时候就写入默认值,因为默认值可以作为运行异常的备份,也可以用作页面的单元测试。

自定义组件接收LocalStorage实例

除了根节点可通过@Entry来接收LocalStorage实例,自定义组件(子节点)也可以通过构造参数来传递LocalStorage实例。

本示例以@LocalStorageLink为例,展示了:

  • 父组件中的Text,显示LocalStorage实例localStorage1中PropA的值为“PropA”。
  • Child组件中,Text绑定的PropB,显示LocalStorage实例localStorage2中PropB的值为“PropB”。

说明:

从API version 12开始,自定义组件支持接收LocalStorage实例。

当自定义组件作为子节点,定义了成员属性时,LocalStorage实例必须要放在第二个参数位置传递,否则会报类型不匹配的编译问题。

当在自定义组件中定义了属性时,暂时不支持只有一个LocalStorage实例作为入参。如果没定义属性,可以只传入一个LocalStorage实例作为入参

如果定义的属性不需要从父组件初始化变量,则第一个参数需要传{}

作为构造参数传给子组件的LocalStorage实例在初始化时就会被决定,可以通过@LocalStorageLink或者LocalStorage的API修改LocalStorage实例中保存的属性值,但LocalStorage实例自身不能被动态修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
// 'PropA',和localStorage1中'PropA'的双向同步
@LocalStorageLink('PropA') PropA: string = 'Hello World';
@State count: number = 0;

build() {
Row() {
Column() {
Text(this.PropA)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 使用LocalStorage 实例localStorage2
Child({ count: this.count }, localStorage2)
}
.width('100%')
}
.height('100%')
}
}

@Component
struct Child {
@Link count: number;
// 'Hello World',和localStorage2中'PropB'的双向同步,localStorage2中没有'PropB',则使用默认值'Hello World'
@LocalStorageLink('PropB') PropB: string = 'Hello World';

build() {
Text(this.PropB)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
当自定义组件没有定义属性时,可以只传入一个LocalStorage实例作为入参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
// 'PropA',和localStorage1中'PropA'的双向同步
@LocalStorageLink('PropA') PropA: string = 'Hello World';
@State count: number = 0;

build() {
Row() {
Column() {
Text(this.PropA)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 使用LocalStorage 实例localStorage2
Child(localStorage2)
}
.width('100%')
}
.height('100%')
}
}

@Component
struct Child {
build() {
Text("hello")
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
当定义的属性不需要从父组件初始化变量时,第一个参数需要传{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
// 'PropA',和localStorage1中'PropA'的双向同步
@LocalStorageLink('PropA') PropA: string = 'Hello World';
@State count: number = 0;

build() {
Row() {
Column() {
Text(this.PropA)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 使用LocalStorage 实例localStorage2
Child({}, localStorage2)
}
.width('100%')
}
.height('100%')
}
}

@Component
struct Child {
@State count: number = 5;
// 'Hello World',和localStorage2中'PropB'的双向同步,localStorage2中没有'PropB',则使用默认值'Hello World'
@LocalStorageLink('PropB') PropB: string = 'Hello World';

build() {
Text(this.PropB)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}

可以通过传递不同的LocalStorage实例给自定义组件,从而实现在navigation跳转到不同的页面时,绑定不同的LocalStorage实例,显示对应绑定的值。

本示例以@LocalStorageLink为例,展示了:

  • 点击父组件中的Button “Next Page”,创建并跳转到name为”pageOne”的子页面,Text显示信息为LocalStorage实例localStorageA中绑定的PropA的值,为”PropA”。
  • 继续点击页面上的Button “Next Page”,创建并跳转到name为”pageTwo”的子页面,Text显示信息为LocalStorage实例localStorageB中绑定的PropB的值,为”PropB”。
  • 继续点击页面上的Button “Next Page”,创建并跳转到name为”pageTree”的子页面,Text显示信息为LocalStorage实例localStorageC中绑定的PropC的值,为”PropC”。
  • 继续点击页面上的Button “Next Page”,创建并跳转到name为”pageOne”的子页面,Text显示信息为LocalStorage实例localStorageA中绑定的PropA的值,为”PropA”。
  • NavigationContentMsgStack自定义组件中的Text组件,共享对应自定义组件树上LocalStorage实例绑定的PropA的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
let localStorageA: LocalStorage = new LocalStorage();
localStorageA.setOrCreate('PropA', 'PropA');

let localStorageB: LocalStorage = new LocalStorage();
localStorageB.setOrCreate('PropB', 'PropB');

let localStorageC: LocalStorage = new LocalStorage();
localStorageC.setOrCreate('PropC', 'PropC');

@Entry
@Component
struct MyNavigationTestStack {
@Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();

@Builder
PageMap(name: string) {
if (name === 'pageOne') {
// 传递不同的LocalStorage实例
pageOneStack({}, localStorageA)
} else if (name === 'pageTwo') {
pageTwoStack({}, localStorageB)
} else if (name === 'pageThree') {
pageThreeStack({}, localStorageC)
}
}

build() {
Column({ space: 5 }) {
Navigation(this.pageInfo) {
Column() {
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈
})
}
}.title('NavIndex')
.navDestination(this.PageMap)
.mode(NavigationMode.Stack)
.borderWidth(1)
}
}
}

@Component
struct pageOneStack {
@Consume('pageInfo') pageInfo: NavPathStack;
@LocalStorageLink('PropA') PropA: string = 'Hello World';

build() {
NavDestination() {
Column() {
NavigationContentMsgStack()
// 显示绑定的LocalStorage中PropA的值'PropA'
Text(`${this.PropA}`)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPathByName('pageTwo', null);
})
}.width('100%').height('100%')
}.title('pageOne')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}

@Component
struct pageTwoStack {
@Consume('pageInfo') pageInfo: NavPathStack;
@LocalStorageLink('PropB') PropB: string = 'Hello World';

build() {
NavDestination() {
Column() {
NavigationContentMsgStack()
// 绑定的LocalStorage中没有PropA,显示本地初始化的值 'Hello World'
Text(`${this.PropB}`)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPathByName('pageThree', null);
})

}.width('100%').height('100%')
}.title('pageTwo')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}

@Component
struct pageThreeStack {
@Consume('pageInfo') pageInfo: NavPathStack;
@LocalStorageLink('PropC') PropC: string = 'pageThreeStack';

build() {
NavDestination() {
Column() {
NavigationContentMsgStack()

// 绑定的LocalStorage中没有PropA,显示本地初始化的值 'pageThreeStack'
Text(`${this.PropC}`)
Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageInfo.pushPathByName('pageOne', null);
})

}.width('100%').height('100%')
}.title('pageThree')
.onBackPressed(() => {
this.pageInfo.pop();
return true;
})
}
}

@Component
struct NavigationContentMsgStack {
@LocalStorageLink('PropA') PropA: string = 'Hello';

build() {
Column() {
Text(`${this.PropA}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
}
LocalStorage支持联合类型

在下面的示例中,变量A的类型为number | null,变量B的类型为number | undefined。Text组件初始化分别显示为null和undefined,点击切换为数字,再次点击切换回null和undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
struct LocalStorLink {
@LocalStorageLink("AA") A: number | null = null;
@LocalStorageLink("BB") B: number | undefined = undefined;

build() {
Column() {
Text("@LocalStorageLink接口初始化,@LocalStorageLink取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Green)

}
}

@Component
struct LocalStorProp {
@LocalStorageProp("AAA") A: number | null = null;
@LocalStorageProp("BBB") B: number | undefined = undefined;

build() {
Column() {
Text("@LocalStorageProp接口初始化,@LocalStorageProp取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Yellow)

}
}

let storage1: LocalStorage = new LocalStorage();

@Entry(storage1)
@Component
struct TestCase3 {
build() {
Row() {
Column() {
LocalStorLink()
LocalStorProp()
}
.width('100%')
}
.height('100%')
}
}
装饰Date类型变量

说明:

从API version 12开始,LocalStorage支持Date类型。

在下面的示例中,@LocalStorageLink装饰的selectedDate类型为Date,点击Button改变selectedDate的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct LocalDateSample {
@LocalStorageLink("date") selectedDate: Date = new Date('2021-08-08');

build() {
Column() {
Button('set selectedDate to 2023-07-08')
.margin(10)
.onClick(() => {
this.selectedDate = new Date('2023-07-08');
})
Button('increase the year by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
})
Button('increase the month by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
})
Button('increase the day by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setDate(this.selectedDate.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: $$this.selectedDate
})
}.width('100%')
}
}
装饰Map类型变量

说明:

从API version 12开始,LocalStorage支持Map类型。

在下面的示例中,@LocalStorageLink装饰的message类型为Map<number, string>,点击Button改变message的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct LocalMapSample {
@LocalStorageLink("map") message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);

build() {
Row() {
Column() {
ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
Text(`${item[0]}`).fontSize(30)
Text(`${item[1]}`).fontSize(30)
Divider()
})
Button('init map').onClick(() => {
this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]);
})
Button('set new one').onClick(() => {
this.message.set(4, "d");
})
Button('clear').onClick(() => {
this.message.clear();
})
Button('replace the existing one').onClick(() => {
this.message.set(0, "aa");
})
Button('delete the existing one').onClick(() => {
this.message.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
装饰Set类型变量

说明:

从API version 12开始,LocalStorage支持Set类型。

在下面的示例中,@LocalStorageLink装饰的memberSet类型为Set,点击Button改变memberSet的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct LocalSetSample {
@LocalStorageLink("set") memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);

build() {
Row() {
Column() {
ForEach(Array.from(this.memberSet.entries()), (item: [number, string]) => {
Text(`${item[0]}`)
.fontSize(30)
Divider()
})
Button('init set')
.onClick(() => {
this.memberSet = new Set([0, 1, 2, 3, 4]);
})
Button('set new one')
.onClick(() => {
this.memberSet.add(5);
})
Button('clear')
.onClick(() => {
this.memberSet.clear();
})
Button('delete the first one')
.onClick(() => {
this.memberSet.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}

AppStorage:应用全局的 UI 状态存储

AppStorage 是应用全局的 UI 状态存储,是和应用的进程绑定的,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。

和 AppStorage 不同的是,LocalStorage 是页面级的,通常应用于页面内的数据共享。而 AppStorage 是应用级的全局状态共享,还相当于整个应用的“中枢”,持久化数据 PersistentStorage环境变量 Environment 都是通过 AppStorage 中转,才可以和 UI 交互。

概述

AppStorage 是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage 将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。

AppStorage 可以和 UI 组件同步,且可以在应用业务逻辑中被访问。

AppStorage 支持应用的主线程内多个 UIAbility 实例间的状态共享。

AppStorage 中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见 PersistentStorage)。这些数据是通过业务逻辑中实现,与 UI 解耦,如果希望这些数据在 UI 中使用,需要用到 @StorageProp@StorageLink

@StorageProp

要建立 AppStorage 和自定义组件的联系,需要使用 @StorageProp 和 @StorageLink 装饰器。使用 @StorageProp(key)/@StorageLink(key) 装饰组件内的变量,key 标识了 AppStorage 的属性。

当自定义组件初始化的时候,会使用 AppStorage 中对应 key 的属性值将 @StorageProp(key)/@StorageLink(key) 装饰的变量初始化。由于应用逻辑的差异,无法确认是否在组件初始化之前向 AppStorage 实例中存入了对应的属性,所以 AppStorage 不一定存在 key 对应的属性,因此 @StorageProp(key)/@StorageLink(key) 装饰的变量进行本地初始化是必要的。

@StorageProp(key) 是和 AppStorage 中 key 对应的属性建立单向数据同步,允许本地改变,但是对于 @StorageProp,本地的修改永远不会同步回 AppStorage 中,相反,如果 AppStorage 给定 key 的属性发生改变,改变会被同步给 @StorageProp,并覆盖掉本地的修改。

装饰器使用规则说明

@StorageProp变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见AppStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@StorageProp(“AA”) a: number | null = null是推荐的,不推荐@StorageProp(“AA”) a: number = null。
同步类型 单向同步:从AppStorage的对应属性到组件的状态变量。组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。
被装饰变量的初始值 必须指定,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。

变量的传递/访问规则说明

传递/访问 说明
从父节点初始化和更新 禁止,@StorageProp不支持从父节点初始化,只能AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化
初始化子节点 支持,可用于初始化@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用appstorage和localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  • 当@StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回AppStorage对应属性键值key的属性中。
  • 当前@StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该key的数据不会同步改变。
  • 当@StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回AppStorage中,但是会引起所属的自定义组件的重新渲染。
  • 当AppStorage中key对应的属性发生改变时,会同步给所有@StorageProp(key)装饰的数据,@StorageProp(key)本地的修改将被覆盖。

@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步

  1. 本地修改发生,该修改会被写回AppStorage中;
  2. AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(@StorageProp和通过Prop创建的单向绑定变量)、双向(@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。

装饰器使用规则说明

@StorageLink变量装饰器 说明
装饰器参数 key:常量字符串,必填(字符串需要有引号)。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。API12及以上支持Map、Set、Date类型。嵌套类型的场景请参考观察变化和行为表现。类型必须被指定,建议和AppStorage中对应属性类型相同,否则会发生类型隐式转换,从而导致应用行为异常。不支持any,API12及以上支持undefined和null类型。API12及以上支持上述支持类型的联合类型,比如string | number, string | undefined 或者 ClassA | null,示例见AppStorage支持联合类型注意当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:@StorageLink(“AA”) a: number | null = null是推荐的,不推荐@StorageLink(“AA”) a: number = null。
同步类型 双向同步:从AppStorage的对应属性到自定义组件,从自定义组件到AppStorage对应属性。
被装饰变量的初始值 必须指定,如果AppStorage实例中不存在属性,则用该初始值初始化该属性,并存入AppStorage中。

变量的传递/访问规则说明

传递/访问 说明
从父节点初始化和更新 禁止。
初始化子节点 支持,可用于初始化常规变量、@State、@Link、@Prop、@Provide。
是否支持组件外访问 否。

观察变化和行为表现

观察变化

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到对象整体赋值和对象属性变化(详见从ui内部使用appstorage和localstorage)。
  • 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
  • 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。详见装饰Date类型变量
  • 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见装饰Map类型变量
  • 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见装饰Set类型变量

框架行为

  1. 当@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。
  2. AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@StorageLink和单向@StorageProp)都将同步修改。
  3. 当@StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回AppStorage中,还会引起所属的自定义组件的重新渲染。
使用场景
从应用逻辑使用AppStorage和LocalStorage

AppStorage是单例,它的所有API都是静态的,使用方法类似于LocalStorage中对应的非静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AppStorage.setOrCreate('PropA', 47);

let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('PropA',17);
let propA: number | undefined = AppStorage.get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17
let link1: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = AppStorage.link('PropA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = AppStorage.prop('PropA'); // prop.get() == 47

link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49

storage.get<number>('PropA') // == 17
storage.set('PropA', 101);
storage.get<number>('PropA') // == 101

AppStorage.get<number>('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49
从UI内部使用AppStorage和LocalStorage

@StorageLink变量装饰器与AppStorage配合使用,正如@LocalStorageLink与LocalStorage配合使用一样。此装饰器使用AppStorage中的属性创建双向数据同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class PropB {
code: number;

constructor(code: number) {
this.code = code;
}
}

AppStorage.setOrCreate('PropA', 47);
AppStorage.setOrCreate('PropB', new PropB(50));
let storage = new LocalStorage();
storage.setOrCreate('PropA', 48);
storage.setOrCreate('PropB', new PropB(100));

@Entry(storage)
@Component
struct CompA {
@StorageLink('PropA') storageLink: number = 1;
@LocalStorageLink('PropA') localStorageLink: number = 1;
@StorageLink('PropB') storageLinkObject: PropB = new PropB(1);
@LocalStorageLink('PropB') localStorageLinkObject: PropB = new PropB(1);

build() {
Column({ space: 20 }) {
Text(`From AppStorage ${this.storageLink}`)
.onClick(() => {
this.storageLink += 1;
})

Text(`From LocalStorage ${this.localStorageLink}`)
.onClick(() => {
this.localStorageLink += 1;
})

Text(`From AppStorage ${this.storageLinkObject.code}`)
.onClick(() => {
this.storageLinkObject.code += 1;
})

Text(`From LocalStorage ${this.localStorageLinkObject.code}`)
.onClick(() => {
this.localStorageLinkObject.code += 1;
})
}
}
}
不建议借助@StorageLink的双向同步机制实现事件通知

不建议开发者使用@StorageLink和AppStorage的双向同步的机制来实现事件通知,因为AppStorage中的变量可能绑定在多个不同页面的组件中,但事件通知则不一定需要通知到所有的这些组件。并且,当这些@StorageLink装饰的变量在UI中使用时,会触发UI刷新,带来不必要的性能影响。

示例代码中,TapImage中的点击事件,会触发AppStorage中tapIndex对应属性的改变。因为@StorageLink是双向同步,修改会同步回AppStorage中,所以,所有绑定AppStorage的tapIndex自定义组件里都能感知到tapIndex的变化。使用@Watch监听到tapIndex的变化后,修改状态变量tapColor从而触发UI刷新(此处tapIndex并未直接绑定在UI上,因此tapIndex的变化不会直接触发UI刷新)。

使用该机制来实现事件通知需要确保AppStorage中的变量尽量不要直接绑定在UI上,且需要控制@Watch函数的复杂度(如果@Watch函数执行时间长,会影响UI刷新效率)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// xxx.ets
class ViewData {
title: string;
uri: Resource;
color: Color = Color.Black;

constructor(title: string, uri: Resource) {
this.title = title;
this.uri = uri
}
}

@Entry
@Component
struct Gallery2 {
dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
scroller: Scroller = new Scroller()

build() {
Column() {
Grid(this.scroller) {
ForEach(this.dataList, (item: ViewData, index?: number) => {
GridItem() {
TapImage({
uri: item.uri,
index: index
})
}.aspectRatio(1)

}, (item: ViewData, index?: number) => {
return JSON.stringify(item) + index;
})
}.columnsTemplate('1fr 1fr')
}

}
}

@Component
export struct TapImage {
@StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
@State tapColor: Color = Color.Black;
private index: number = 0;
private uri: Resource = {
id: 0,
type: 0,
moduleName: "",
bundleName: ""
};

// 判断是否被选中
onTapIndexChange() {
if (this.tapIndex >= 0 && this.index === this.tapIndex) {
console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`)
this.tapColor = Color.Red;
} else {
console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, black`)
this.tapColor = Color.Black;
}
}

build() {
Column() {
Image(this.uri)
.objectFit(ImageFit.Cover)
.onClick(() => {
this.tapIndex = this.index;
})
.border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
}

}
}

相比借助@StorageLink的双向同步机制实现事件通知,开发者可以使用emit订阅某个事件并接收事件回调的方式来减少开销,增强代码的可读性。

说明:

emit接口不支持在Previewer预览器中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// xxx.ets
import emitter from '@ohos.events.emitter';

let NextID: number = 0;

class ViewData {
title: string;
uri: Resource;
color: Color = Color.Black;
id: number;

constructor(title: string, uri: Resource) {
this.title = title;
this.uri = uri
this.id = NextID++;
}
}

@Entry
@Component
struct Gallery2 {
dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
scroller: Scroller = new Scroller()
private preIndex: number = -1

build() {
Column() {
Grid(this.scroller) {
ForEach(this.dataList, (item: ViewData) => {
GridItem() {
TapImage({
uri: item.uri,
index: item.id
})
}.aspectRatio(1)
.onClick(() => {
if (this.preIndex === item.id) {
return
}
let innerEvent: emitter.InnerEvent = { eventId: item.id }
// 选中态:黑变红
let eventData: emitter.EventData = {
data: {
"colorTag": 1
}
}
emitter.emit(innerEvent, eventData)

if (this.preIndex != -1) {
console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`)
let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }
// 取消选中态:红变黑
let eventData: emitter.EventData = {
data: {
"colorTag": 0
}
}
emitter.emit(innerEvent, eventData)
}
this.preIndex = item.id
})
}, (item: ViewData) => JSON.stringify(item))
}.columnsTemplate('1fr 1fr')
}

}
}

@Component
export struct TapImage {
@State tapColor: Color = Color.Black;
private index: number = 0;
private uri: Resource = {
id: 0,
type: 0,
moduleName: "",
bundleName: ""
};

onTapIndexChange(colorTag: emitter.EventData) {
if (colorTag.data != null) {
this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black
}
}

aboutToAppear() {
//定义事件ID
let innerEvent: emitter.InnerEvent = { eventId: this.index }
emitter.on(innerEvent, data => {
this.onTapIndexChange(data)
})
}

build() {
Column() {
Image(this.uri)
.objectFit(ImageFit.Cover)
.border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
}
}
}

以上通知事件逻辑简单,也可以简化成三元表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// xxx.ets
class ViewData {
title: string;
uri: Resource;
color: Color = Color.Black;

constructor(title: string, uri: Resource) {
this.title = title;
this.uri = uri
}
}

@Entry
@Component
struct Gallery2 {
dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]
scroller: Scroller = new Scroller()

build() {
Column() {
Grid(this.scroller) {
ForEach(this.dataList, (item: ViewData, index?: number) => {
GridItem() {
TapImage({
uri: item.uri,
index: index
})
}.aspectRatio(1)

}, (item: ViewData, index?: number) => {
return JSON.stringify(item) + index;
})
}.columnsTemplate('1fr 1fr')
}

}
}

@Component
export struct TapImage {
@StorageLink('tapIndex') tapIndex: number = -1;
private index: number = 0;
private uri: Resource = {
id: 0,
type: 0,
moduleName: "",
bundleName: ""
};

build() {
Column() {
Image(this.uri)
.objectFit(ImageFit.Cover)
.onClick(() => {
this.tapIndex = this.index;
})
.border({
width: 5,
style: BorderStyle.Dotted,
color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
})
}
}
}
AppStorage支持联合类型

在下面的示例中,变量A的类型为number | null,变量B的类型为number | undefined。Text组件初始化分别显示为null和undefined,点击切换为数字,再次点击切换回null和undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
struct StorLink {
@StorageLink("AA") A: number | null = null;
@StorageLink("BB") B: number | undefined = undefined;

build() {
Column() {
Text("@StorageLink接口初始化,@StorageLink取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Red)

}
}

@Component
struct StorProp {
@StorageProp("AAA") A: number | null = null;
@StorageProp("BBB") B: number | undefined = undefined;

build() {
Column() {
Text("@StorageProp接口初始化,@StorageProp取值")
Text(this.A + "").fontSize(20).onClick(() => {
this.A ? this.A = null : this.A = 1;
})
Text(this.B + "").fontSize(20).onClick(() => {
this.B ? this.B = undefined : this.B = 1;
})
}
.borderWidth(3).borderColor(Color.Blue)
}
}

@Entry
@Component
struct TestCase3 {
build() {
Row() {
Column() {
StorLink()
StorProp()
}
.width('100%')
}
.height('100%')
}
}
装饰Date类型变量

说明:

从API version 12开始,AppStorage支持Date类型。

在下面的示例中,@StorageLink装饰的selectedDate类型为Date,点击Button改变selectedDate的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct DateSample {
@StorageLink("date") selectedDate: Date = new Date('2021-08-08');

build() {
Column() {
Button('set selectedDate to 2023-07-08')
.margin(10)
.onClick(() => {
AppStorage.setOrCreate("date", new Date('2023-07-08'));
})
Button('increase the year by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
})
Button('increase the month by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
})
Button('increase the day by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setDate(this.selectedDate.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: $$this.selectedDate
})
}.width('100%')
}
}
装饰Map类型变量

说明:

从API version 12开始,AppStorage支持Map类型。

在下面的示例中,@StorageLink装饰的message类型为Map<number, string>,点击Button改变message的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@Component
struct MapSample {
@StorageLink("map") message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);

build() {
Row() {
Column() {
ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
Text(`${item[0]}`).fontSize(30)
Text(`${item[1]}`).fontSize(30)
Divider()
})
Button('init map').onClick(() => {
this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]);
})
Button('set new one').onClick(() => {
this.message.set(4, "d");
})
Button('clear').onClick(() => {
this.message.clear();
})
Button('replace the existing one').onClick(() => {
this.message.set(0, "aa");
})
Button('delete the existing one').onClick(() => {
AppStorage.get<Map<number, string>>("map")?.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
装饰Set类型变量

说明:

从API version 12开始,AppStorage支持Set类型。

在下面的示例中,@StorageLink装饰的memberSet类型为Set,点击Button改变memberSet的值,视图会随之刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Entry
@Component
struct SetSample {
@StorageLink("set") memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);

build() {
Row() {
Column() {
ForEach(Array.from(this.memberSet.entries()), (item: [number, string]) => {
Text(`${item[0]}`)
.fontSize(30)
Divider()
})
Button('init set')
.onClick(() => {
this.memberSet = new Set([0, 1, 2, 3, 4]);
})
Button('set new one')
.onClick(() => {
AppStorage.get<Set<number>>("set")?.add(5);
})
Button('clear')
.onClick(() => {
this.memberSet.clear();
})
Button('delete the first one')
.onClick(() => {
this.memberSet.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
限制条件

AppStorage与PersistentStorage以及Environment配合使用时,需要注意以下几点:

  • 在AppStorage中创建属性后,调用PersistentStorage.persistProp()接口时,会使用在AppStorage中已经存在的值,并覆盖PersistentStorage中的同名属性,所以建议要使用相反的调用顺序,反例可见在PersistentStorage之前访问AppStorage中的属性
  • 如果在AppStorage中已经创建属性后,再调用Environment.envProp()创建同名的属性,会调用失败。因为AppStorage已经有同名属性,Environment环境变量不会再写入AppStorage中,所以建议AppStorage中属性不要使用Environment预置环境变量名。
  • 状态装饰器装饰的变量,改变会引起UI的渲染更新,如果改变的变量不是用于UI更新,只是用于消息传递,推荐使用 emitter方式。例子可见不建议借助@StorageLink的双向同步机制实现事件通知

PersistentStorage:持久化存储UI状态

PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。

概述

PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。

PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。

限制条件

PersistentStorage允许的类型和值有:

  • number, string, boolean, enum 等简单类型。
  • 可以被JSON.stringify()和JSON.parse()重构的对象,以及对象的属性方法不支持持久化。
  • API12及以上支持Map类型,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。且更新的值被持久化存储。详见装饰Map类型变量
  • API12及以上支持Set类型,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。且更新的值被持久化存储。详见装饰Set类型变量
  • API12及以上支持Date类型,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。且更新的值被持久化存储。详见装饰Date类型变量
  • API12及以上支持undefined 和 null。
  • API12及以上支持联合类型

PersistentStorage不允许的类型和值有:

  • 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。

持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:

  • 持久化大型数据集。
  • 持久化经常变化的变量。

PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。

PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后(即loadContent传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败。

1
2
3
4
5
6
7
8
9
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
return;
}
PersistentStorage.persistProp('aProp', 47);
});
}
使用场景
从AppStorage中访问PersistentStorage初始化的属性
  1. 初始化PersistentStorage:

    1
    PersistentStorage.persistProp('aProp', 47);
  2. 在AppStorage获取对应属性:

    1
    AppStorage.get<number>('aProp'); // returns 47

    或在组件内部定义:

    1
    @StorageLink('aProp') aProp: number = 48;

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PersistentStorage.persistProp('aProp', 47);

@Entry
@Component
struct Index {
@State message: string = 'Hello World'
@StorageLink('aProp') aProp: number = 48

build() {
Row() {
Column() {
Text(this.message)
// 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
Text(`${this.aProp}`)
.onClick(() => {
this.aProp += 1;
})
}
}
}
}
  • 新应用安装后首次启动运行:

    1. 调用persistProp初始化PersistentStorage,首先查询在PersistentStorage本地文件中是否存在“aProp”,查询结果为不存在,因为应用是第一次安装。
    2. 接着查询属性“aProp”在AppStorage中是否存在,依旧不存在。
    3. 在AppStorge中创建名为“aProp”的number类型属性,属性初始值是定义的默认值47。
    4. PersistentStorage将属性“aProp”和值47写入磁盘,AppStorage中“aProp”对应的值和其后续的更改将被持久化。
    5. 在Index组件中创建状态变量@StorageLink(‘aProp’) aProp,和AppStorage中“aProp”双向绑定,在创建的过程中会在AppStorage中查找,成功找到“aProp”,所以使用其在AppStorage找到的值47。
  • 触发点击事件后:

    1. 状态变量@StorageLink(‘aProp’) aProp改变,触发Text组件重新刷新。
    2. @StorageLink装饰的变量是和AppStorage中建立双向同步的,所以@StorageLink(‘aProp’) aProp的变化会被同步回AppStorage中。
    3. AppStorage中“aProp”属性的改变会同步到所有绑定该“aProp”的单向或者双向变量,在本示例中没有其他的绑定“aProp”的变量。
    4. 因为“aProp”对应的属性已经被持久化,所以在AppStorage中“aProp”的改变会触发PersistentStorage,将新的改变写入本地磁盘。后续启动应用:
  • 后续启动应用:

    1. 执行PersistentStorage.persistProp(‘aProp’, 47),在首先查询在PersistentStorage本地文件查询“aProp”属性,成功查询到。
    2. 将在PersistentStorage查询到的值写入AppStorage中。
    3. 在Index组件里,@StorageLink绑定的“aProp”为PersistentStorage写入AppStorage中的值,即为上一次退出应用存入的值。
在PersistentStorage之前访问AppStorage中的属性

该示例为反例。在调用PersistentStorage.persistProp或者persistProps之前使用接口访问AppStorage中的属性是错误的,因为这样的调用顺序会丢失上一次应用程序运行中的属性值:

1
2
let aProp = AppStorage.setOrCreate('aProp', 47);
PersistentStorage.persistProp('aProp', 48);

应用在非首次运行时,先执行AppStorage.setOrCreate(‘aProp’, 47):属性“aProp”在AppStorage中创建,其类型为number,其值设置为指定的默认值47。“aProp”是持久化的属性,所以会被写回PersistentStorage磁盘中,PersistentStorage存储的上次退出应用的值丢失。

PersistentStorage.persistProp(‘aProp’, 48):在PersistentStorage中查找到“aProp”,值为刚刚使用AppStorage接口写入的47。

在PersistentStorage之后访问AppStorage中的属性

开发者可以先判断是否需要覆盖上一次保存在PersistentStorage中的值,如果需要覆盖,再调用AppStorage的接口进行修改,如果不需要覆盖,则不调用AppStorage的接口。

1
2
3
4
5
PersistentStorage.persistProp('aProp', 48);
if (AppStorage.get('aProp') > 50) {
// 如果PersistentStorage存储的值超过50,设置为47
AppStorage.setOrCreate('aProp',47);
}

示例代码在读取PersistentStorage储存的数据后判断“aProp”的值是否大于50,如果大于50的话使用AppStorage的接口设置为47。

支持联合类型

PersistentStorage支持联合类型和undefined和null,在下面的示例中,使用persistProp方法初始化”P”为undefined。通过@StorageLink(“P”)绑定变量p,类型为number | undefined | null,点击Button改变P的值,视图会随之刷新。且P的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
PersistentStorage.persistProp("P", undefined);

@Entry
@Component
struct TestCase6 {
@StorageLink("P") p: number | undefined | null = 10;

build() {
Row() {
Column() {
Text(this.p + "")
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("changeToNumber").onClick(() => {
this.p = 10;
})
Button("changeTo undefined").onClick(() => {
this.p = undefined;
})
Button("changeTo null").onClick(() => {
this.p = null;
})
}
.width('100%')
}
.height('100%')
}
}
装饰Date类型变量

在下面的示例中,@StorageLink装饰的persistedDate类型为Date,点击Button改变persistedDate的值,视图会随之刷新。且persistedDate的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
PersistentStorage.persistProp("persistedDate", new Date());

@Entry
@Component
struct PersistedDate {
@StorageLink("persistedDate") persistedDate: Date = new Date();

updateDate() {
this.persistedDate = new Date();
}

build() {
List() {
ListItem() {
Column() {
Text(`Persisted Date is ${this.persistedDate.toString()}`)
.margin(20)

Text(`Persisted Date month is ${this.persistedDate.getMonth()}`)
.margin(20)

Text(`Persisted Date day is ${this.persistedDate.getDay()}`)
.margin(20)

Text(`Persisted Date time is ${this.persistedDate.toLocaleTimeString()}`)
.margin(20)

Button() {
Text('Update Date')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.updateDate();
})

}.width('100%')
}
}
}
}
装饰Map类型变量

在下面的示例中,@StorageLink装饰的persistedMapString类型为Map<number, string>,点击Button改变persistedMapString的值,视图会随之刷新。且persistedMapString的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PersistentStorage.persistProp("persistedMapString", new Map<number, string>([]));

@Entry
@Component
struct PersistedMap {
@StorageLink("persistedMapString") persistedMapString: Map<number, string> = new Map<number, string>([]);

persistMapString() {
this.persistedMapString = new Map<number, string>([[3, "one"], [6, "two"], [9, "three"]]);
}

build() {
List() {
ListItem() {
Column() {
Text(`Persisted Map String is `)
.margin(20)
ForEach(Array.from(this.persistedMapString.entries()), (item: [number, string]) => {
Text(`${item[0]} ${item[1]}`)
})

Button() {
Text('Persist Map String')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.persistMapString();
})

}.width('100%')
}
}
}
}
装饰Set类型变量

在下面的示例中,@StorageLink装饰的persistedSet类型为Set,点击Button改变persistedSet的值,视图会随之刷新。且persistedSet的值被持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
PersistentStorage.persistProp("persistedSet", new Set<number>([]));

@Entry
@Component
struct PersistedSet {
@StorageLink("persistedSet") persistedSet: Set<number> = new Set<number>([]);

persistSet() {
this.persistedSet = new Set<number>([33, 1, 3]);
}

clearSet() {
this.persistedSet.clear();
}

build() {
List() {
ListItem() {
Column() {
Text(`Persisted Set is `)
.margin(20)
ForEach(Array.from(this.persistedSet.entries()), (item: [number, string]) => {
Text(`${item[1]}`)
})

Button() {
Text('Persist Set')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.persistSet();
})

Button() {
Text('Persist Clear')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('60%')
.height('5%')
.onClick(() => {
this.clearSet();
})

}
.width('100%')
}
}
}
}

Environment:设备环境查询

开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。

Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。

Environment内置参数
数据类型 描述
accessibilityEnabled boolean 获取无障碍屏幕读取是否启用。
colorMode ColorMode 色彩模型类型:选项为ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色。
fontScale number 字体大小比例,范围: [0.85, 1.45]。
fontWeightScale number 字体粗细程度,范围: [0.6, 1.6]。
layoutDirection LayoutDirection 布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左。
languageCode string 当前系统语言值,取值必须为小写字母, 例如zh。
使用场景
从UI中访问Environment参数
  • 从UI中访问Environment参数

    1
    2
    // 将设备的语言code存入AppStorage,默认值为en
    Environment.envProp('languageCode', 'en');
  • 可以使用@StorageProp链接到Component中。

    1
    @StorageProp('languageCode') lang : string = 'en';

设备环境到Component的更新链:Environment –> AppStorage –>Component。

说明:

@StorageProp关联的环境参数可以在本地更改,但不能同步回AppStorage中,因为应用对环境变量参数是不可写的,只能在Environment中查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将设备languageCode存入AppStorage中
Environment.envProp('languageCode', 'en');

@Entry
@Component
struct Index {
@StorageProp('languageCode') languageCode: string = 'en';

build() {
Row() {
Column() {
// 输出当前设备的languageCode
Text(this.languageCode)
}
}
}
}
应用逻辑使用Environment
1
2
3
4
5
6
7
8
9
10
// 使用Environment.EnvProp将设备运行languageCode存入AppStorage中;
Environment.envProp('languageCode', 'en');
// 从AppStorage获取单向绑定的languageCode的变量
const lang: SubscribedAbstractProperty<string> = AppStorage.prop('languageCode');

if (lang.get() === 'zh') {
console.info('你好');
} else {
console.info('Hello!');
}
限制条件

Environment和UIContext相关联,需要在UIContext明确的时候才可以调用。可以通过在runScopedTask里明确上下文。如果没有在UIContext明确的地方调用,将导致无法查询到设备环境数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index');
let window = windowStage.getMainWindow()
window.then(window => {
let uicontext = window.getUIContext()
uicontext.runScopedTask(() => {
Environment.envProp('languageCode', 'en');
})
})
}
}

协程是什么

协程基于线程,它是轻量级线程。

  • 协程让异步逻辑同步化,杜绝回调地狱。
  • 协程最核心的点就是,函数或者一段程序能够被挂起,稍后再在挂起的位置恢复

在 Android 中协程用来解决什么问题

  • 处理耗时任务,这种任务常常会阻塞主线程。
  • 保证主线程安全,即安全地从主线程调用任务 suspend 函数。

协程的挂起和恢复

常规函数基础操作包括:invoke(或 call)和 return,协程增加了 suspend 和 resume。

  • suspend:也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量。
  • resume:用于让已暂停的协程从其暂停处继续执行。

挂起与阻塞的区别

1
2
3
4
5
6
7
8
9
GlobalScope.launch(Dispatchers.Main) {
// 挂起
delay(5000)
Log.d("tag", "${Thread.currentThread().name}:after delay.")
}

// 阻塞
Thread.sleep(5000)
Log.d("tag", "${Thread.currentThread().name}:after sleep.")

挂起函数

  • 使用 suspend 关键字修饰的函数叫做挂起函数。
  • 挂起函数只能在协程体内其它挂起函数内调用。

协程的两部分

Kotlin 的协程实现分为两个层次:

  • 基础设施层,标准库的协程 API,主要对协程提供了概念和语义上最基本的支持。
  • 业务框架层,协程的上层框架支持。

基础设施层的一个 demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import kotlin.coroutines.*

// 协程体
val continuation = suspend {
5
}.createCoroutine(object : Continuation<Int> {

override val context: CoroutineContext = EmptyCoroutineContext

override fun resumeWith(result: Result<Int>) {
println("Coroutine End: $result")
}
})

continuation.resume(Unit)

基础设施层使用的是 kotlin.coroutines.* 而业务框架层使用的是 kotlinx.coroutines.*

调度器

所有协程必须在调度器中运行,即使它们在主线程上运行也是如此。

  • Dispatchers.Main。Android 上的主线程,用来处理 UI 交互和一些轻量级任务(调用 suspend 函数;调用 UI 函数;更新 LiveData)。

  • Dispatchers.IO。非主线程,专为磁盘和网络 IO 进行了优化(数据库;文件读写;网络处理)。

  • Dispatchers.Default。非主线程,专为 CPU 密集型任务进行了优化(数组排序;JSON 数据解析;处理差异判断)。

任务泄漏

  • 当某个协程任务丢失,无法追踪,会导致内存、CPU、磁盘等资源浪费,甚至发送一个无用的网络请求,这种情况称为任务泄漏。
  • 为了能够避免协程泄漏,Kotlin 引入了结构化并发机制。

结构化并发

使用结构化并发可以做到:

  • 取消任务,当某项任务不再需要时,取消它。
  • 追踪任务,当任务正在执行时,追踪它。
  • 发出错误信号,当协程失败时,发出错误信号表明有错误发生。

CoroutineScope

1、定义协程必须指定其 CoroutineScope,它会跟踪所有协程,同样它还可以取消由它所启动的所有协程

2、常用的相关 API 有:

  • GlobalScope,生命周期是 process 级别的,即使 Activity 或 Fragment 已经被销毁,协程仍然在执行。
  • MainScope,在 Activity 中使用,可以在 onDestroy() 中取消协程。
  • viewModelScope,只能在 ViewModel 中使用,绑定 ViewModel 生命周期。
  • lifecycleScope,只能在 Activity、Fragment 中使用,会绑定 Activity 和 Fragment 的生命周期。

MainScope 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CoroutineActivity : AppCompatActivity(), CoroutineScope by MainScope() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine)

submit()
}

private fun submit() {
btnSubmit?.setOnClickListener {
launch {
try {
delay(5000)
} catch (e: Exception) {
/**
* 取消协程会抛出异常
* kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@7e7375b
*/
e.printStackTrace()
}
}
}
}

override fun onDestroy() {
cancel()
super.onDestroy()
}
}

协程构建器

launch 和 async 构建器都可以用来启动新协程:

  • launch,返回一个 Job 并且不附带任何结果值。
  • async,返回一个 Deferred,Deferred 也是一个 Job,可以使用 .await() 在一个延期的值上得到它的最终结果。

等待一个作业:

  • join 和 await。
  • 组合并发。

测试构建器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
fun `coroutine builder`() = runBlocking {
val job1 = launch {
delay(200)
println("job1 finished.")
}

val job2 = async {
delay(200)
println("job2 finished.")
"job2 result"
}
println(job2.await())
}

// 打印结果
job1 finished.
job2 finished.
job2 result

runBlocking 把当前线程包装成一个协程,它会阻塞当前线程等待子协程执行完再结束。

join 和 await 等待协程作业

测试 join:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
fun `coroutine join`() = runBlocking {
val job1 = launch {
delay(2000)
println("One")
}
job1.join()
val job2 = launch {
delay(200)
println("Two")
}
val job3 = launch {
delay(200)
println("Three")
}
}

// 打印结果
One
Two
Three

测试 await:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
fun `coroutine await`() = runBlocking {
val job1 = async {
delay(2000)
println("One")
}
job1.await()
val job2 = async {
delay(200)
println("Two")
}
val job3 = async {
delay(200)
println("Three")
}
}

// 打印结果
One
Two
Three

async 组合并发

测试同步 sync:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
fun `coroutine sync`() = runBlocking {
val time = measureTimeMillis {
val one = doOne()
val two = doTwo()
println("The result:${one + two}")
}
println("Completed in $time ms")
}

private suspend fun doOne(): Int {
delay(1000)
return 1
}

private suspend fun doTwo(): Int {
delay(1000)
return 1
}

// 打印结果
The result:2
Completed in 2018 ms

测试异步 async:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
fun `coroutine async`() = runBlocking {
val time = measureTimeMillis {
val one = async {
doOne()
}
val two = async {
doTwo()
}
println("The result:${one.await() + two.await()}")

/* 以下形式,结果是 2m。
val one = async {
doOne()
}.await()
val two = async {
doTwo()
}.await()
println("The result:${one + two}")*/
}
println("Completed in $time ms")
}

// 打印结果
The result:2
Completed in 1034 ms

协程的启动模式

  • DEFAULT:协程创建后,立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    fun `start mode DEFAULT`() = runBlocking {
    val job = launch(start = CoroutineStart.DEFAULT) {
    delay(5000)
    println("Job finished.")
    }
    delay(1000)
    job.cancel()
    }

    // 什么也不打印
  • ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Test
    fun `start mode ATOMIC`() = runBlocking {
    val job = launch(start = CoroutineStart.ATOMIC) {
    var count = 0
    val time = measureTimeMillis {
    for (i in 0 until 2100000000) {
    if (i % 2 == 0) {
    count++
    }
    }
    }
    println("Before delay $time ms. count=$count")
    delay(5000)
    println("Job finished.")
    }
    delay(1000)
    job.cancel()
    }

    // 打印结果
    Before delay 1513 ms, count=1050000000.
  • LAZY:只有协程被需要时,包括主动调用协程的 start、join 或者 await 等函数时才会开始调度,如果调度前就被取消,那么该协程将直接进入异常结束状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    fun `start mode LAZY`() = runBlocking {
    val job = async (start = CoroutineStart.LAZY) {
    delay(2000)
    println("Job finished.")
    }
    // 调度前就取消,会抛出异常 kotlinx.coroutines.JobCancellationException
    job.cancel()
    job.await()
    }
  • UNDISPATCHED:协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    fun `start mode UNDISPATCHED`() = runBlocking {
    val job = async(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
    println("thread1: ${Thread.currentThread().name}")
    delay(1000)
    println("thread2: ${Thread.currentThread().name}")
    }
    }

    // 打印结果
    thread1: Test worker @coroutine#2
    thread2: DefaultDispatcher-worker-1 @coroutine#2

协程的作用域构建器

coroutineScope 与 runBlocking

  • runBlocking 是常规函数,而 coroutineScope 是挂起函数。
  • 它们都会等待其协程体结束,主要区别在于 runBlocking 会阻塞当前线程来等待,而 coroutineScope 只是挂起,会释放底层线程用于其它用途。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
fun `coroutine coroutineScope builder`() = runBlocking {
coroutineScope {
val job1 = launch {
delay(400)
println("job1 finished.")
}
val job2 = async {
delay(200)
println("job2 finished.")
throw IllegalArgumentException()
}
}
}

// 打印结果
job2 finished.

java.lang.IllegalArgumentException...

coroutineScope 与 supervisorScope

  • coroutineScope:一个协程失败了,所有其它兄弟协程也会被取消。
  • supervisorScope:一个协程失败了,不会影响其它兄弟协程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
fun `coroutine supervisorScope builder`() = runBlocking {
supervisorScope {
val job1 = launch {
delay(400)
println("job1 finished.")
}
val job2 = async {
delay(200)
println("job2 finished.")
throw IllegalArgumentException()
}
}
}

// 打印结果
job2 finished.
job1 finished.

Job 对象

  • 对于每一个创建的协程(通过 launch 或者 async),会返回一个 Job 实例,该实例是协程的唯一标示,并且负责管理协程的生命周期。
  • 一个任务可以包含一系列状态:新创建(New)、活跃(Active)、完成中(Completing)、已完成(Completed)、取消中(Cancelling)和已取消(Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问 Job 的属性:isActive、isCancelled 和 isCompleted。

Job 的生命周期

如果协程处于活跃状态,协程运行出错或者调用 job.cancel() 都会将当前任务置为取消中(Cancelling)状态(isActive = false,isCancelled = true)。当所有的子协程都完成后,协程会进入已取消(Cancelled)状态,此时 isCompleted = true。

协程的取消

  • 取消作用域会取消它的子协程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Test
    fun `scope cancel`() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
    delay(1000)
    println("job1.")
    }
    scope.launch {
    delay(1000)
    println("job2.")
    }
    delay(100)
    scope.cancel()
    delay(2000)
    }

    // 无打印结果
  • 被取消的子协程不会影响其余兄弟协程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Test
    fun `brother cancel`() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch {
    delay(1000)
    println("job1.")
    }
    val job2 = scope.launch {
    delay(1000)
    println("job2.")
    }
    delay(100)
    job1.cancel()
    delay(2000)
    }

    // 打印结果
    job2.
  • 协程通过抛出一个特殊的异常 CancellationException 来处理取消操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Test
    fun `CancellationException`() = runBlocking {
    val job1 = GlobalScope.launch {
    try {
    delay(1000)
    println("job1.")
    } catch (e: Exception) {
    e.printStackTrace()
    }
    }
    delay(100)
    /* job1.cancel(CancellationException("取消"))
    job1.join()*/
    job1.cancelAndJoin()
    }

    // 打印结果
    kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled;
  • 所有 kotlinx.coroutines 中的挂起函数(withContext, delay 等)都是可取消的。

CPU 密集型任务取消

  • isActive 是一个可以被使用在 CoroutineScope 中的扩展属性,检查 Job 是否处于活跃状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Test
    fun `cancel cpu task by isActive`() = runBlocking {
    val job = launch(Dispatchers.Default) {
    var nextPrintTime = System.currentTimeMillis()
    var i = 0
    while (i < 5 && isActive) {
    if (System.currentTimeMillis() >= nextPrintTime) {
    println("job: I'm sleeping ${i++} ...")
    nextPrintTime += 500
    }
    }
    }
    delay(1300)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
    }

    // 打印结果
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    main: Now I can quit.
  • ensureActive(),如果 Job 处于非活跃状态,这个方法会抛出异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Test
    fun `cancel cpu task by ensureActive`() = runBlocking {
    val job = launch(Dispatchers.Default) {
    var nextPrintTime = System.currentTimeMillis()
    var i = 0
    while (i < 5) {
    ensureActive() // 抛出 CancellationException 异常,但被静默处理掉了。
    if (System.currentTimeMillis() >= nextPrintTime) {
    println("job: I'm sleeping ${i++} ...")
    nextPrintTime += 500
    }
    }
    }
    delay(1300)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
    }

    // 打印结果
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    main: Now I can quit.
  • yield 函数会检查所在协程的状态,如果已经取消,则抛出 CancellationException 予以响应。此外,它还会尝试出让线程的执行权,给其它协程提供执行机会。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Test
    fun `cancel cpu task by yield`() = runBlocking {
    val job = launch(Dispatchers.Default) {
    var nextPrintTime = System.currentTimeMillis()
    var i = 0
    while (i < 5) {
    yield()
    if (System.currentTimeMillis() >= nextPrintTime) {
    println("job: I'm sleeping ${i++} ...")
    nextPrintTime += 500
    }
    }
    }
    delay(1300)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
    }

    // 打印结果
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    main: Now I can quit.

协程取消的副作用

  • 在 finally 中释放资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Test
    fun `release resources`() = runBlocking {
    val job = launch {
    try {
    repeat(1000) { i ->
    println("job: I'm sleeping $i ...")
    delay(500)
    }
    } finally {
    println("job: I'm running finally.")
    }
    }
    delay(1300)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
    }

    // 打印结果
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    job: I'm running finally.
    main: Now I can quit.
  • use 函数:该函数只能被实现了 Closeable 的对象使用,程序结束的时候会自动调用 close 方法,适合文件对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Test
    fun `use function`() = runBlocking {
    /*val br = BufferedReader(FileReader("E:\\Hello.txt"))
    var line: String?
    try {
    while (true) {
    line = br.readLine() ?: break
    println(line)
    }
    } catch (e: Exception) {
    } finally {
    try {
    br.close()
    } catch (e: Exception) {
    }
    }*/

    BufferedReader(FileReader("E:\\Hello.txt")).use {
    var line: String?
    while (true) {
    line = it.readLine() ?: break
    println(line)
    }
    }
    }

不能取消的任务

处于取消中状态的协程不能够挂起(运行不能取消的代码),当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于 NonCancellable CoroutineContext 中。这样会挂起运行中的代码,并保持协程取消中状态直到任务处理完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
fun `cancel with NonCancelable`() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally.")
delay(1000)
println("job: And I've just delayed for 1 sec because I'm non-cancellable.")
}
}
}
delay(1300)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}

// 打印结果
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally.
job: And I've just delayed for 1 sec because I'm non-cancellable.
main: Now I can quit.

NonCancellable 可用于常驻任务。

超时任务

  • 很多情况下取消一个协程的理由是它有可能超时。用 withTimeout 进行超时操作,如果规定时间内未完成任务则会抛出异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Test
    fun `deal with withTimeout`() = runBlocking {
    withTimeout(1300) {
    repeat(1000) { i ->
    println("job: I'm sleeping $i ...")
    delay(500)
    }
    }
    }

    // 打印结果
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...

    Timed out waiting for 1300 ms
    kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
  • withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Test
    fun `deal with withTimeoutOrNull`() = runBlocking {
    val result = withTimeoutOrNull(1300) {
    repeat(1000) { i ->
    println("job: I'm sleeping $i ...")
    delay(500)
    }
    "Done"
    } ?: "Undone"
    println("Result is $result.")
    }

    // 打印结果
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    Result is Undone.

协程的上下文

CoroutineContext 是一组用于定义协程行为的元素,它由如下几项构成:

  • Job:控制协程的生命周期。
  • CoroutineDispatcher:向合适的线程分发任务。
  • CoroutineName:协程的名称,调试的时候很有用。
  • CoroutineExceptionHandler:处理未被捕获的异常。

组合上下文中的元素

有时我们需要在协程上下文中定义多个元素,我们可以使用 “+” 操作符来实现。比如,我们可以显示指定一个调度器来启动协程并且同时显示指定一个命名:

1
2
3
4
5
6
7
8
9
@Test
fun `CoroutineContext`() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("hello")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
}

// 打印结果
I'm working in thread DefaultDispatcher-worker-1 @hello#2

协程上下文的继承

对于新创建的协程,它的 CoroutineContext 会包含一个全新的 Job 实例,它会帮助我们控制协程的生命周期。而剩下的元素会从 CoroutineContext 的父类继承,该父类可能是另外一个协程或者创建该协程的 CoroutineScope。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
fun `CoroutineContext extend`() = runBlocking<Unit> {
val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("hello"))
val job = scope.launch {
// 新的协程会将 CoroutineScope 作为父级
println("${coroutineContext[Job]} ${Thread.currentThread().name}")
val result = async {
// 通过 async 创建的新协程会将当前协程作为父级
println("${coroutineContext[Job]} ${Thread.currentThread().name}")
"OK"
}.await()
}
job.join()
}

// 打印结果
"hello#2":StandaloneCoroutine{Active}@55b8c583 DefaultDispatcher-worker-1 @hello#2
"hello#3":DeferredCoroutine{Active}@64e55ef0 DefaultDispatcher-worker-3 @hello#3

协程上下文的公式

协程上下文 = 默认值 + 继承的 CoroutineContext + 参数

  • 一些元素包含默认值:Dispatchers.Default 是默认的 CoroutineDispatcher,以及 “coroutine” 作为默认的 CoroutineName。
  • 继承的 CoroutineContext 是 CoroutineScope 或者其父协程的 CoroutineContext。
  • 传入协程构建器的参数的优先级高于继承的上下文,隐藏会覆盖对应参数值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
fun `CoroutineContext extend2`() = runBlocking<Unit> {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, excption ->
println("CoroutineExceptionHandler got $excption")
}
val scope = CoroutineScope(Job() + Dispatchers.Main + coroutineExceptionHandler)
// 新的 CoroutineContext = 父级 CoroutineContext + Job()
val job = scope.launch(Dispatchers.IO) {
// 新协程
println("${coroutineContext[Job]} ${Thread.currentThread().name}")
}
job.join()
}

// 打印结果
"coroutine#2":StandaloneCoroutine{Active}@78ec7afa DefaultDispatcher-worker-1 @coroutine#2

协程的异常处理

异常处理的必要性

当应用出现一些意外情况时,给用户提供合适的体验非常重要。一方面,目睹应用崩溃是个很糟糕的体验,另一方面,当用户操作失败时,也必须要能给出正确的提示信息。

异常的传播

协程构建器有两种形式:自动传播异常(launch 与 actor),向用户暴露异常(async 与 produce)。当这些构建器用于创建一个根协程时(该协程不是另一个协程的子协程),前者这类构建器,异常会在它发生的第一时间被抛出,而后者则依赖用户来最终消费异常,例如通过 await 或 receive。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
fun `exception propagation`() = runBlocking<Unit> {
val job = GlobalScope.launch {
try {
throw IndexOutOfBoundsException()
} catch (e: Exception) {
println("Caught IndexOutOfBoundsException")
}
}
job.join()

val deferred = GlobalScope.async {
throw ArithmeticException()
}
try {
deferred.await()
} catch (e: Exception) {
println("Caught ArithmeticException")
}
}

// 打印结果
Caught IndexOutOfBoundsException
Caught ArithmeticException

非根协程的异常

其它协程所创建的协程中,产生的异常总是会被传播。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
fun `exception propagation2`() = runBlocking<Unit> {
val scope = CoroutineScope(Job())
val job = scope.launch {
async {
// 如果 async 抛出异常,launch 就会立刻抛出异常,而不会调用 .await()
throw IllegalArgumentException()
}
}
job.join()
}

// 打印结果
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.IllegalArgumentException

异常的传播特性

当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来,父级会进行下面几步操作:

  • 取消它的子级。
  • 取消它自己。
  • 将异常传播并传递给它的父级。

SupervisorJob

  • 使用 SupervisorJob 时,一个子协程的运行失败不会影响到其它子协程。SupervisorJob 不会传播异常给它的父级,它会让子协程自己处理异常
  • 这种需求常见于在作用域内定义作业的 UI 组件,如果任一个 UI 的子作业执行失败了,它并不总是有必要取消整个 UI 组件,但是如果 UI 组件被销毁了,由于它的结果不再被需要了,它就有必要使所有的子作业执行失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
fun `test SupervisorJob`() = runBlocking<Unit> {
val supervisor = CoroutineScope(SupervisorJob())
val job1 = supervisor.launch {
delay(100)
println("child 1.")
throw IllegalArgumentException()
}
val job2 = supervisor.launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("child 2 finished.")
}
}
// delay(200)
// supervisor.cancel()
joinAll(job1, job2)
}

// 打印结果
child 1.
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IllegalArgumentException
一直运行中......

supervisorScope

当作业自身执行失败的时候,所有子作业将会全部被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
fun `test supervisorScope`() = runBlocking<Unit> {
supervisorScope {
launch {
delay(100)
println("child 1.")
throw IllegalArgumentException()
}
try {
delay(Long.MAX_VALUE)
} finally {
println("child 2 finished.")
}
}
}

// 打印结果
child 1.
Exception in thread "Test worker @coroutine#2" java.lang.IllegalArgumentException
一直运行中......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
fun `test supervisorScope2`() = runBlocking<Unit> {
supervisorScope {
launch {
try {
println("The child is sleeping.")
delay(Long.MAX_VALUE)
} finally {
println("The child is cancelled.")
}
}
yield()
println("Throwing an exception from the scope.")
throw AssertionError()
}
}

// 打印结果
The child is sleeping.
Throwing an exception from the scope.
The child is cancelled.

java.lang.AssertionError

异常的捕获

  • 使用 CoroutineExceptionHandler 对协程的异常进行捕获。
  • 以下的条件被满足时,异常就会被捕获。
    • 时机:异常是被自动抛出异常的协程所抛出的(使用 launch,而不是 async 时)。
    • 位置:在 CoroutineScope 的 CoroutineContext 中或在一个根协程(CoroutineScope 或者 supervisorScope 的直接子协程)中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
fun `test coroutineExceptionHandler`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, excption ->
println("Caught $excption")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException()
}
job.join()
deferred.await()
}

// 打印结果
Caught java.lang.AssertionError

java.lang.ArithmeticException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
fun `test coroutineExceptionHandler2`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, excption ->
println("Caught $excption")
}
val scope = CoroutineScope(Job())
val job = scope.launch(handler) {
launch {
throw ArithmeticException()
}
}
job.join()
}

// 打印结果
Caught java.lang.ArithmeticException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
fun `test coroutineExceptionHandler3`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, excption ->
println("Caught $excption")
}
val scope = CoroutineScope(Job())
val job = scope.launch {
// handler 放这里捕获不到异常
launch(handler) {
throw ArithmeticException()
}
}
job.join()
}

// 打印结果
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.ArithmeticException

Android 中全局异常处理

  • 全局异常处理器可以获取到所有协程未处理的未捕获异常,不过它并不能对异常进行捕获,虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的用处。
  • 我们需要在 classpath 下面创建 META-INF/services 目录,并在其中创建一个名为 kotlinx.coroutines.CoroutineExceptionHandler 的文件,文件内容就是我们的全局异常处理器的全类名。

正常情况捕获异常(程序不崩溃):

1
2
3
4
5
6
7
8
9
10
11
12
val handler = CoroutineExceptionHandler { _, excption ->
Log.d("tag", "Caught $excption")
}

btnGlobalCoroutineExceptionHandler?.setOnClickListener {
GlobalScope.launch(handler) {
"abc".substring(10)
}
}

// 打印结果
D/tag: Caught java.lang.StringIndexOutOfBoundsException: length=3; index=10

全局异常处理器获取未被捕获异常(程序崩溃):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
btnGlobalCoroutineExceptionHandler?.setOnClickListener {
GlobalScope.launch {
"abc".substring(10)
}
}

// 打印结果
D/tag: Unhandled Coroutine Exception: java.lang.StringIndexOutOfBoundsException: length=3; index=10

--------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.zch.kotlin, PID: 3436
java.lang.StringIndexOutOfBoundsException: length=3; index=10
at java.lang.String.substring(String.java:1899)

取消与异常

  • 取消与异常紧密相关,协程内部使用 CancellationException 来进行取消,这个异常会被忽略。
  • 当子协程被取消时,不会取消它的父协程。
  • 如果一个协程遇到了 CancellationException 以外的异常,它将使用该异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
fun `test cancel and exception`() = runBlocking<Unit> {
val job = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
}finally {
println("Child is cancelled.")
}
}
yield()
println("Cancelling child.")
child.cancelAndJoin()
yield()
println("Parent is not cancelled.")
}
job.join()
}

// 打印结果
Cancelling child.
Child is cancelled.
Parent is not cancelled.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
fun `test cancel and exception2`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, excption ->
println("Caught $excption")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children termination.")
delay(100)
println("The first child finished its non cancellable block.")
}
}
}
launch {
delay(10)
println("Second child throws an exception.")
throw ArithmeticException()
}
}
job.join()
}

// 打印结果
Second child throws an exception.
Children are cancelled, but exception is not handled until all children termination.
The first child finished its non cancellable block.
Caught java.lang.ArithmeticException

异常聚合

当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在第一个异常之后发生的所有其它异常,都将被绑定到第一个异常之上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
fun `test exception aggreation`() = runBlocking<Unit> {
val handler = CoroutineExceptionHandler { _, excption ->
println("Caught $excption ${excption.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw ArithmeticException()
}
}
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw IndexOutOfBoundsException()
}
}
launch {
delay(100)
throw IOException()
}
}
job.join()
}

// 打印结果
Caught java.io.IOException [java.lang.IndexOutOfBoundsException, java.lang.ArithmeticException]

Flow

异步返回多个值的方案

集合、序列、挂起函数、Flow。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 返回了多个值,但不是异步
private fun simpleList() = listOf(1, 2, 3)

// 返回了多个值,但不是异步(一次返回一个值)
private fun simpleSequence() = sequence {
for (i in 1..3) {
Thread.sleep(1000) // 阻塞
// delay(1000) // 不能用
yield(i)
}
}

// 返回了多个值,是异步(一次性返回了多个值)
private suspend fun simpleList2(): List<Int> {
delay(1000)
return listOf(1, 2, 3)
}

// 返回了多个值,是异步(一次返回一个值)
private fun simpleFlow() = flow {
for (i in 1..3) {
delay(1000) // 模拟耗时操作
emit(i) // 发射,返回一个元素
}
}

@Test
fun `test multiple values`() {
// simpleList().forEach { println(it) }
simpleSequence().forEach { println(it) }
}

@Test
fun `test multiple values2`() = runBlocking {
simpleList2().forEach { println(it) }
}

@Test
fun `test multiple values3`() = runBlocking {
// 启动另外一个协程,证明 simpleFlow 没有阻塞主线程
launch {
for (i in 1..3) {
println("I'm not blocked $i.")
delay(1500)
}
}
simpleFlow().collect { println(it) }
}

Flow 与其它方式的区别

  • 名为 flow 的 Flow 类型构建器函数。
  • flow{…} 构建块中的代码可以挂起。
  • 函数 simpleFlow 不再标有 suspend 修饰符。
  • 流使用 emit 函数发射值。
  • 流使用 collect 函数收集值。

冷流

Flow 是一种类似于序列的冷流,flow 构建器中的代码直到流被收集的时候才运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private fun simpleFlow2() = flow {
println("Flow started.")
for (i in 1..3) {
delay(1000)
emit(i)
}
}

@Test
fun `test flow is cold`() = runBlocking {
val flow = simpleFlow2()
println("Calling collect...")
flow.collect { println(it) }
println("Calling collect again...")
flow.collect { println(it) }
}

// 打印结果
Calling collect...
Flow started.
1
2
3
Calling collect again...
Flow started.
1
2
3

流的连续性

  • 流的每次单独收集都是按顺序执行的,除非使用特殊操作符。
  • 从上游到下游每个过渡操作符都会处理每个发射出的值,然后再交给末端操作符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
fun `test flow continuation`() = runBlocking {
(1..5).asFlow().filter {
it % 2 == 0
}.map {
"string $it"
}.collect {
println("Collect $it.")
}
}

// 打印结果
Collect string 2.
Collect string 4.

流构建器

  • flowOf 构建器定义了一个发射固定值集的流。
  • 使用 .asFlow() 扩展函数,可以将各种集合与序列转换为流。
1
2
3
4
5
6
7
8
@Test
fun `test flow builder`() = runBlocking {
flowOf("one", "two", "three")
.onEach { delay(1000) }
.collect { println(it) }

(1..3).asFlow().collect { println(it) }
}

流上下文

  • 流的收集总是在调用协程的上下文中发生,流的该属性称为上下文保存
  • flow{…} 构建器中的代码必须遵循上下文保存属性,并且不允许从其它上下文中发射(emit)。
  • flowOn 操作符,该函数用于更改流发射的上下文。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun simpleFlow3() = flow {
println("Flow started ${Thread.currentThread().name}.")
for (i in 1..3) {
delay(1000)
emit(i)
}
}.flowOn(Dispatchers.Default)

@Test
fun `test flow on`() = runBlocking {
simpleFlow3().collect { println("Collected $it ${Thread.currentThread().name}") }
}

// 打印结果
Flow started DefaultDispatcher-worker-1 @coroutine#2.
Collected 1 Test worker @coroutine#1
Collected 2 Test worker @coroutine#1
Collected 3 Test worker @coroutine#1

如果去掉 .flowOn(Dispatchers.Default),那么发射和收集都是在 Test worker 线程中。

在指定协程中收集流

使用 launchIn 替换 collect,我们可以在单独的协程中启动流的收集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private fun events() = (1..3).asFlow().onEach { delay(100) }.flowOn(Dispatchers.Default)

@Test
fun `test flow launchIn`() = runBlocking {
val job = events()
.onEach { println("Event: $it ${Thread.currentThread().name}") }
// .launchIn(CoroutineScope(Dispatchers.IO))
.launchIn(this)

// delay(200)
// job.cancelAndJoin()
}

// 打印结果
Event: 1 Test worker @coroutine#2
Event: 2 Test worker @coroutine#2
Event: 3 Test worker @coroutine#2

流的取消

流采用与协程同样的协作取消。像往常一样,流的收集可以是当流在一个可取消的挂起函数(例如 delay)中挂起的时候取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private fun simpleFlow4() = flow {
for (i in 1..3) {
delay(1000)
println("Emitting $i.")
emit(i)
}
}

@Test
fun `test cancel flow`() = runBlocking {
withTimeoutOrNull(2500) {
simpleFlow4().collect { println(it) }
}
println("Done.")
}

// 打印结果
Emitting 1.
1
Emitting 2.
2
Done.

流的取消检测

  • 为方便起见,流构建器对每个发射值执行附加的 ensureActive 检测以进行取消,这意味着从 flow{…} 发出的繁忙循环是可以取消的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private fun simpleFlow5() = flow {
    for (i in 1..5) {
    println("Emitting $i.")
    emit(i)
    }
    }

    @Test
    fun `test cancel flow check`() = runBlocking {
    simpleFlow5().collect {
    println(it)
    if (it == 3) cancel()
    }
    }

    // 打印结果
    Emitting 1.
    1
    Emitting 2.
    2
    Emitting 3.
    3

    BlockingCoroutine was cancelled
    kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@5da3da3d
  • 出于性能原因,大多数其它流操作不会自动执行其它取消检测,在协程处于繁忙循环的情况下,必须明确检测是否取消,通过 cancellable 操作符来执行此操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    fun `test cancel flow check`() = runBlocking {
    (1..5).asFlow().cancellable().collect {
    println(it)
    if (it == 3) cancel()
    }
    }

    // 打印结果
    1
    2
    3

    BlockingCoroutine was cancelled
    kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@63bbd396

背压

1、使用缓冲与 flowOn 处理背压

  • buffer():并发运行流中发射元素的代码。

  • 当必须更改 CoroutineDispatcher 时,flowOn 操作符使用了相同的缓冲机制,但是 buffer 函数显示地请求缓冲而不改变执行上下文

2、合并与处理最新值

  • conflate():合并发射项,不对每个值进行处理。
  • collectLastest():取消并重新发射最后一个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private fun simpleFlow6() = flow {
for (i in 1..3) {
delay(100)
emit(i)
println("Emitting $i ${Thread.currentThread().name}.")
}
}

@Test
fun `test flow back pressure`() = runBlocking {
val time = measureTimeMillis {
simpleFlow6()
// .buffer(50) // 并行发射 3 个元素,需要 100 ms
// .flowOn(Dispatchers.Default) // 让发射在后台线程,也是并行发射,需要 100 ms
// .conflate()
// .collect {
.collectLatest {
delay(300) // 处理这个元素消耗 300 ms
println("Collected $it ${Thread.currentThread().name}")
}
}
println("Collected in $time ms.")
}

不处理背压情况下,上面程序大概需要消耗 (100 + 300)* 3 = 1200 ms。

  • 使用 buffer(),大概需要 100 + 3 * 300 = 1000 ms,因为该方式发射元素是异步的。
  • 使用 flowOn 操作符,也是大概需要 100 + 3 * 300 = 1000 ms,该方式发射元素也是异步,但是发射元素切换到了后台线程。
  • 使用 conflate,合并发射项,不对每个值进行处理,大概需要 100 + 2 * 300 = 700 ms。该方式发射元素是异步的,收集过程忽略了中间的元素,只处理了前后两个元素。
  • 使用 collectLastest,上面程序大概需要消耗 700 ms。该方式发射元素也是异步,它会取消并重新发射最后一个值。

操作符

转换操作符
  • 可以使用操作符转换流,就像使用集合与序列一样。
  • 转换操作符应用于上游流,并返回下游流。
  • 这些操作符也是冷操作符,就像流一样。这类操作符本身不是挂起函数。
  • 它运行的速度很快,返回新的转换流的定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private suspend fun performRequest(request: Int): String {
delay(1000)
return "response $request"
}

@Test
fun `test transform flow operator`() = runBlocking {
(1..3).asFlow()
// .map { performRequest(it) }
.transform {
emit("Making request $it")
emit(performRequest(it))
}
.collect { println(it) }
}

map 转换一次。transform 可以转换发射多次。

限长操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private fun numbers() = flow {
try {
emit(1)
emit(2)
println("This line will not execute.")
emit(3)
} finally {
println("Finally in numbers.")
}
}

@Test
fun `test limit length operator`() = runBlocking {
numbers().take(2).collect { println(it) }
}

// 打印结果
1
2
Finally in numbers.
末端流操作符

末端流操作符是在流上用于启动流收集的挂起函数。collect 是最基础的末端操作符,但是还有另外一些更方便的使用的末端操作符:

  • 转化为各种集合,例如 toList 与 toSet。
  • 获取第一个(first)值与确保流发射单个(Single)值的操作符。
  • 使用 reduce 与 fold 将流规约到单个值。
1
2
3
4
5
6
7
8
9
10
@Test
fun `test terminal operator`() = runBlocking {
val sum = (1..5).asFlow()
.map { it * it }
.reduce { a, b -> a + b }
println(sum)
}

// 打印结果
55
组合操作符

就像 Kotlin 标准库中的 Sequence.zip 扩展函数一样,流拥有一个 zip 操作符用于组合两个流中的相关值。

1
2
3
4
5
6
7
8
9
10
11
@Test
fun `test zip operator`() = runBlocking {
val numbs = (1..3).asFlow()
val strs = flowOf("One", "Two", "Three")
numbs.zip(strs) { a, b -> "$a -> $b" }.collect { println(it) }
}

// 打印结果
1 -> One
2 -> Two
3 -> Three
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
fun `test zip2 operator`() = runBlocking {
val numbs = (1..3).asFlow().onEach { delay(300) }
val strs = flowOf("One", "Two", "Three").onEach { delay(400) }
val startTime = System.currentTimeMillis()
numbs.zip(strs) { a, b -> "$a -> $b" }.collect {
println("$it at ${System.currentTimeMillis() - startTime} ms from start.")
}
}

// 打印结果
1 -> One at 442 ms from start.
2 -> Two at 839 ms from start.
3 -> Three at 1246 ms from start.
展平操作符

流表示异步接收的值序列,所以很容易遇到这样的情况:每个值都会触发对另一个值序列的请求,然而,由于流具有异步的性质,因此需要不同的展平模式,存在一系列的流展平操作符:

  • flatMapConcat 连接模式。
  • flatMapMerge 合并模式。
  • flatMapLastest 最新展平模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private fun requestFlow(i: Int) = flow<String> {
emit("$i: First.")
delay(500)
emit("$i: Second.")
}

@Test
fun `test flatMapConcat operator`() = runBlocking {
val startTime = System.currentTimeMillis()
(1..3).asFlow()
.onEach {
delay(100)
}.flatMapConcat {
requestFlow(it)
}.collect {
println("$it at ${System.currentTimeMillis() - startTime} ms from start.")
}
}

// 打印结果
1: First. at 138 ms from start.
1: Second. at 665 ms from start.
2: First. at 774 ms from start.
2: Second. at 1289 ms from start.
3: First. at 1398 ms from start.
3: Second. at 1911 ms from start.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
fun `test flatMapMerge operator`() = runBlocking {
val startTime = System.currentTimeMillis()
(1..3).asFlow()
.onEach {
delay(100)
}.flatMapMerge {
requestFlow(it)
}.collect {
println("$it at ${System.currentTimeMillis() - startTime} ms from start.")
}
}

// 打印结果
1: First. at 192 ms from start.
2: First. at 293 ms from start.
3: First. at 404 ms from start.
1: Second. at 698 ms from start.
2: Second. at 806 ms from start.
3: Second. at 919 ms from start.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
fun `test flatMapLatest operator`() = runBlocking {
val startTime = System.currentTimeMillis()
(1..3).asFlow()
.onEach {
delay(100)
}.flatMapLatest {
requestFlow(it)
}.collect {
println("$it at ${System.currentTimeMillis() - startTime} ms from start.")
}
}

// 打印结果
1: First. at 178 ms from start.
2: First. at 326 ms from start.
3: First. at 441 ms from start.
3: Second. at 943 ms from start.

流的异常处理

当运算符中的发射器或代码抛出异常时,有几种处理异常的方法:

  • try/catch 块。
  • catch 函数。

1、捕获下游异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private fun exceptionFlow() = flow {
for (i in 1..3) {
println("Emitting $i.")
emit(i)
}
}

@Test
fun `test flow exception`() = runBlocking {
try {
exceptionFlow().collect {
println(it)
check(it <= 1) {
"Collect $it."
}
}
} catch (e: Exception) {
println("Caught $e")
}
}

// 打印结果
Emitting 1.
1
Emitting 2.
2
Caught java.lang.IllegalStateException: Collect 2.

2、捕获上游异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
fun `test flow exception2`() = runBlocking {
flow {
emit(1)
throw ArithmeticException("Div 0.")
}.catch {
println("Caught $it")
}.flowOn(Dispatchers.IO)
.collect {
println(it)
}
}

// 打印结果
1
Caught java.lang.ArithmeticException: Div 0.

3、恢复异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
fun `test flow exception3`() = runBlocking {
flow {
throw ArithmeticException("Div 0.")
emit(1)
}.catch {
println("Caught $it")
emit(10)
}.flowOn(Dispatchers.IO)
.collect {
println(it)
}
}

// 打印结果
Caught java.lang.ArithmeticException: Div 0.
10

流的完成

当流收集完成时(普通情况或异常情况),它可能需要执行一个动作。

  • 命令式 finally 块。
  • onCompletion 声明式处理。

1、finally 形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
fun `test flow complete in finally`() = runBlocking {
fun simpleFlow() = (1..3).asFlow()

try {
simpleFlow().collect {
println(it)
}
} finally {
println("Done.")
}
}

// 打印结果
1
2
3
Done.

2、onCompletion 函数可以拿到上游异常信息,但捕获异常还是需要 catch 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
fun `test flow complete in onCompletion`() = runBlocking {
fun simpleFlow() = flow {
emit(1)
throw RuntimeException()
}
simpleFlow()
.onCompletion { exception ->
if (exception != null) {
println("Flow completed exceptionally.")
}
}
.catch { exception ->
println("Caught $exception.")
}
.collect {
println(it)
}
}

// 打印结果
1
Flow completed exceptionally.
Caught java.lang.RuntimeException.

3、onCompletion 函数也能拿到下游异常信息,但捕获异常需要用 try/catch 形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
fun `test flow complete in onCompletion2`() = runBlocking {
fun simpleFlow() = flow {
emit(1)
throw RuntimeException()
}
try {
simpleFlow()
.onCompletion { exception ->
if (exception != null) {
println("Flow completed exceptionally.")
}
}.collect {
println(it)
check(it <= 1) {
"Collect $it."
}
}
} catch (e: Exception) {
println("Caught $e.")
}
}

// 打印结果
1
Flow completed exceptionally.
Caught java.lang.RuntimeException.

Channel

什么是 Channel

Channel 实际上是一个并发安全的队列,它可以用来连接协程,实现不同协程的通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
fun `test know Channel`() = runBlocking {
val channel = Channel<Int>()
// 生产者
val producer = GlobalScope.launch {
var i = 1
while (true) {
delay(1000)
println("send $i.")
channel.send(i++)
}
}
// 消费者
val consumer = GlobalScope.launch {
while (true) {
// delay(3000)
val element = channel.receive()
println("receive $element.")
}
}
joinAll(producer, consumer)
}

// 打印结果
send 1.
receive 1.
send 2.
receive 2.
......

Channel 的容量

Channel 实际上就是一个队列,队列中一定存在缓冲区,那么一旦这个缓冲区满了,并且一直没有人调用 receive 并取走函数,send 就需要挂起。故意让接收端的节奏放慢,发现 send 总是会挂起,直到 receive 之后才会继续往下执行。

比如上面的代码让消费者延迟 3s,那么生产者 send 一个元素后,需要等消费者在 3s 之后 receive 到元素才能 send 下一个元素。因为 Channel 默认的容量是 0。

迭代 Channel

Channel 本身确实像序列,所以我们在读取的时候可以直接获取一个 Channel 的 iterator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Test
fun `test iterator Channel`() = runBlocking {
val channel = Channel<Int>(Channel.UNLIMITED)
// 生产者
val producer = GlobalScope.launch {
for (i in 1..5) {
val element = i * i
println("send $element.")
channel.send(element)
}
}
// 消费者
val consumer = GlobalScope.launch {
// val it = channel.iterator()
// while (it.hasNext()) {
// val element = it.next()
// println("receive $element.")
// delay(2000)
// }

for (element in channel) {
println("receive $element.")
delay(2000)
}
}
joinAll(producer, consumer)
}

// 打印结果
send 1.
send 4.
send 9.
send 16.
send 25.
receive 1.
receive 4.
receive 9.
receive 16.
receive 25.

produce 与 actor

  • 构造生产者与消费者的便捷方法
  • 我们可以通过 produce 方法启动一个生产者协程,并返回一个 ReceiveChannel,其它协程就可以用这个 Channel 来接收数据了。反过来,我们可以用 actor 启动一个消费者协程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
fun `test fast producer Channel`() = runBlocking {
val receiveChannel: ReceiveChannel<Int> = GlobalScope.produce {
repeat(5) {
delay(1000)
send(it)
}
}
val consumer = GlobalScope.launch {
for (element in receiveChannel) {
println("receive $element.")
}
}
consumer.join()
}

// 打印结果
receive 0.
receive 1.
receive 2.
receive 3.
receive 4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
fun `test fast consumer Channel`() = runBlocking {
val sendChannel: SendChannel<Int> = GlobalScope.actor<Int> {
while (true) {
val element = receive()
println("receive $element.")
}
}
val producer = GlobalScope.launch {
for (i in 0..3) {
sendChannel.send(i)
}
}
producer.join()
}

// 打印结果
receive 0.
receive 1.
receive 2.
receive 3.

Channel 的关闭

  • produce 和 actor 返回的 Channel 都会随着对应的协程执行完毕而关闭,也正是这样,Channel 才被称为热数据流
  • 对于一个 Channel,如果我们调用了它的 close 方法,它会立即停止接收新元素,也就是说这时它的 isClosedForSend 会立即返回 true。而由于 Channel 缓冲区的存在,这时候可能还有一些元素没有被处理完,因此要等所有的元素都被读取之后 isCloseForReceive 才会返回 true。
  • Channel 的生命周期最好由主导方来维护,建议由主导的一方实现关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
fun `test close Channel`() = runBlocking {
val channel = Channel<Int>(3)
// 生产者
val producer = GlobalScope.launch {
List(3) {
println("send $it.")
channel.send(it)
}
channel.close()
println("Close Channel. | - ClosedForSend: ${channel.isClosedForSend}. | - ClosedForReceive: ${channel.isClosedForReceive}.")
}
// 消费者
val consumer = GlobalScope.launch {
for (element in channel) {
println("receive $element.")
delay(1000)
}
println("After Consuming. | - ClosedForSend: ${channel.isClosedForSend}. | - ClosedForReceive: ${channel.isClosedForReceive}.")
}
joinAll(producer, consumer)
}

// 打印结果
send 0.
send 1.
send 2.
receive 0.
Close Channel. | - ClosedForSend: true. | - ClosedForReceive: false.
receive 1.
receive 2.
After Consuming. | - ClosedForSend: true. | - ClosedForReceive: true.

BroadcastChannel

前面提到,发送端和接收端在 Channel 中存在一对多的情形,从数据处理本身来讲,虽然有多个接收端,但是同一个元素只会被一个接收端读到。广播则不然,多个接收端不存在互斥行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
fun `test broadcastChannel`() = runBlocking {
// val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
val channel = Channel<Int>()
val broadcastChannel = channel.broadcast(3)
val producer = GlobalScope.launch {
List(3) {
delay(100)
broadcastChannel.send(it)
}
broadcastChannel.close()
}
List(3) { index ->
GlobalScope.launch {
val receiveChannel = broadcastChannel.openSubscription()
for (i in receiveChannel) {
println("[#$index] receive: $i.")
}
}
}.joinAll()
}

// 打印结果
[#2] receive: 0.
[#0] receive: 0.
[#1] receive: 0.
[#1] receive: 1.
[#2] receive: 1.
[#0] receive: 1.
[#1] receive: 2.
[#2] receive: 2.
[#0] receive: 2.

select - 多路复用

什么是多路复用

数据通信系统或计算机网络系统中,传输媒体的带宽或容量往往会大于传输单一信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是所谓的多路复用技术(Multiplexing)。

复用多个 await

两个 API 分别从网络和本地缓存获取数据,期望哪个先返回就先用哪个做展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
data class Response<T>(val value: T, val isLocal: Boolean)

data class User(val name: String, val address: String)

fun CoroutineScope.getUserFromLocal(name: String) = async(Dispatchers.IO) {
delay(1000)
User(name, "Local")
}

fun CoroutineScope.getUserFromServer(name: String) = async(Dispatchers.IO) {
delay(2000)
User(name, "Server")
}

@Test
fun `test select await`() = runBlocking {
GlobalScope.launch {
val localRequest = getUserFromLocal("AAA")
val serverRequest = getUserFromServer("BBB")

val userResponse = select<Response<User>> {
localRequest.onAwait { Response(it, true) }
serverRequest.onAwait { Response(it, false) }
}

userResponse.value.let {
println(it)
}
}.join()
}

// 打印结果
User(name=AAA, address=Local)

复用多个 Channel

跟 await 类似,会接收到最快的那个 Channel 消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
fun `test select Channel`() = runBlocking {
val channels = listOf(Channel<Int>(), Channel<Int>())
GlobalScope.launch {
delay(100)
channels[0].send(200)
}
GlobalScope.launch {
delay(50)
channels[1].send(100)
}
val result = select<Int?> {
channels.forEach { channel ->
channel.onReceive { it }
}
}
println(result)
}

// 打印结果
100

SelectClause

我们怎么知道哪些事件可以被 select 呢?其实所有能够被 select 的事件都是 SelectClauseN 类型,包括:

  • SelectClause0:对应事件没有返回值,例如 join 没有返回值,对应的 onJoin 就是这个类型,使用时 onJoin 的参数是一个无参函数。

  • SelectClause1:对应事件有返回值,前面的 onAwait 和 onReceive 都是此类情况。

  • SelectClause2:对应事件有返回值,此外还需要额外的一个参数,例如 Channel.onSend 有两个参数,第一个就是一个 Channel 数据类型的值,表示即将发送的值,第二个是发送成功时的回调。

如果我们想要确认挂起函数是否支持 select,只需要查看其是否存在对应的 SelectClauseN 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
fun `test SelectClause0`() = runBlocking {
val job1 = GlobalScope.launch {
println("job 1")
delay(100)
}
val job2 = GlobalScope.launch {
println("job 2")
delay(10)
}
select<Unit> {
job1.onJoin {
println("job 1 onJoin")
}
job2.onJoin {
println("job 2 onJoin")
}
}
}

// 打印结果
job 1
job 2
job 2 onJoin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
fun `test SelectClause2`() = runBlocking {
val channels = listOf(Channel<Int>(), Channel<Int>())
println(channels)
launch(Dispatchers.IO) {
select {
launch {
delay(10)
channels[1].onSend(200) { sentChannel ->
println("sent on $sentChannel")
}
}
launch {
delay(100)
channels[0].onSend(100) { sentChannel ->
println("sent on $sentChannel")
}
}
}
}

GlobalScope.launch {
println(channels[0].receive())
}
GlobalScope.launch {
println(channels[1].receive())
}

delay(1000)
}

// 打印结果
[RendezvousChannel@24c4ddae{EmptyQueue}, RendezvousChannel@766653e6{EmptyQueue}]
200
sent on RendezvousChannel@766653e6{EmptyQueue}

使用 Flow 实现多路复用

多数情况下,我们可以通过构造合适的 Flow 来实现多路复用的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
fun `test select flow`() = runBlocking {
// 函数 -> 协程 -> Flow -> Flow 合并
val name = "guest"
coroutineScope {
listOf(::getUserFromLocal, ::getUserFromServer)
.map { function ->
function.call(name)
}.map { deferred ->
flow {
emit(deferred.await())
}
}.merge()
.collect { user ->
println(user)
}
}
}

// 打印结果
User(name=guest, address=Local)
User(name=guest, address=Server)

并发安全

由于协程是基于线程的,既然线程有并发问题,那么协程也一定有。

不安全的并发访问

我们使用线程在解决并发问题的时候总是会遇到线程安全问题,而 Java 平台上的 Kotlin 协程实现免不了存在并发调度的情况,因此线程安全同样值得留意。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
fun `test not safe concurrent`() = runBlocking {
var count = 0
List(10000) {
GlobalScope.launch {
count++
}
}.joinAll()
println(count)
}

// 打印结果(每次打印可能不一样)
9997

Java API 中安全的并发访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
fun `test safe concurrent`() = runBlocking {
var count = AtomicInteger(0)
List(10000) {
GlobalScope.launch {
count.incrementAndGet()
}
}.joinAll()
println(count.get())
}

// 打印结果(每次打印都一样)
10000

协程的并发工具

除了我们在线程中常用的解决并发问题的手段之外,协程框架也提供了一些并发安全的工具,包括:

  • Channel:并发安全的消息通道,我们已经非常熟悉。
  • Mutex:轻量级锁,它的 lock 和 unlock 从语义上与线程锁比较类似,之所以轻量是因为它在获取不到锁时不会阻塞线程,而是挂起等待锁的释放。
  • Semaphore:轻量级信号量,信号量可以有多个,协程在获取到信号量后即可执行并发操作。当 Semaphore 的参数为 1 时,效果等价于 Mutex。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
fun `test safe concurrent tools`() = runBlocking {
var count = 0
val mutex = Mutex()
List(10000) {
GlobalScope.launch {
mutex.withLock {
count++
}
}
}.joinAll()
println(count)
}

// 打印结果(每次打印都一样)
10000
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
fun `test safe concurrent tools2`() = runBlocking {
var count = 0
val semaphore = Semaphore(1)
List(10000) {
GlobalScope.launch {
semaphore.withPermit {
count++
}
}
}.joinAll()
println(count)
}

避免访问外部可变状态

编写函数时要求它不得访问外部状态,只能基于参数做运算,通过返回值提供运算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
fun `test avoid access outer variable`() = runBlocking {
var count = 0
val result = count + List(10000) {
GlobalScope.async { 1 }
}.map {
it.await()
}.sum()
println(result)
}

// 打印结果(每次打印都一样)
10000

冷流还是热流

  • Flow 是冷流,什么是冷流?简单来说,如果 Flow 有了订阅者 Collector 以后,发射出来的值才会实实在在的存在于内存之中,这跟懒加载的概念很像。
  • 与之相对的是热流,StateFlow 和 SharedFlow 是热流,在垃圾回收之前,都是存在于内存之中,并且处于活跃状态的。

StateFlow

StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。还可通过其 value 属性读取当前状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
class NumberViewModel : ViewModel() {

// StateFlow 与 LiveData 很像,但能使用 Flow 的操作符。
val number = MutableStateFlow(0)

fun increment() {
number.value++
}

fun decrement() {
number.value--
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class NumberFragment : Fragment() {

private val mNumberViewModel: NumberViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

btnPlus.setOnClickListener {
// 加法
mNumberViewModel.increment()
}

btnMinus.setOnClickListener {
// 减法
mNumberViewModel.decrement()
}

lifecycleScope.launch {
// 收集数据
mNumberViewModel.number.collect {
tvNumber.text = it.toString()
}
}
}
}

SharedFlow

SharedFlow 会向从其中收集值的所有使用方发出数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 使用 SharedFlow 模拟 EventBus 收发数据
*/
object LocalEventBus {

val events = MutableSharedFlow<Event>()

suspend fun postEvent(event: Event) {
// 发送数据
events.emit(event)
}
}

data class Event(val timestamp: Long)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SharedViewModel : ViewModel() {

private var job: Job? = null

/**
* 开始刷新数据
*/
fun startRefresh() {
// 开启协程
job = viewModelScope.launch(Dispatchers.IO) {
// 发送数据
while (true) {
LocalEventBus.postEvent(Event(System.currentTimeMillis()))
}
}
}

/**
* 停止刷新数据
*/
fun stopRefresh() {
// 关闭协程
job?.cancel()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 发送数据界面
*/
class SharedFlowFragment : Fragment() {

private val mSharedViewModel: SharedViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

btnStart.setOnClickListener {
// 发送[开始刷新数据]指令
mSharedViewModel.startRefresh()
}
btnStop.setOnClickListener {
// 发送[停止刷新数据]指令
mSharedViewModel.stopRefresh()
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 收集数据界面
*/
class TextsFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

lifecycleScope.launch {
// 收集数据
LocalEventBus.events.collect {
textView.text = "${tag}: ${it.timestamp}"
}
}
}
}

Flow 的应用

Flow 与文件下载的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 下载状态
*/
sealed class DownloadStatus {

// 空状态
object None : DownloadStatus()

// 下载进度
data class Progress(val value: Int) : DownloadStatus()

// 错误
data class Error(val throwable: Throwable) : DownloadStatus()

// 完成
data class Done(val file: File) : DownloadStatus()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 下载文件管理类
*/
object DownloadManager {

/**
* 下载文件
* @param url String 远程文件地址
* @param file File 下载到的本地文件
* @return Flow<DownloadStatus>
*/
fun download(url: String, file: File): Flow<DownloadStatus> {
return flow {
val request = Request.Builder().url(url).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
if (response.isSuccessful) {
response.body()!!.let { body ->
val total = body.contentLength()
// 文件读写
file.outputStream().use { outputStream ->
val inputStream = body.byteStream()
var emittedProgress = 0L
inputStream.copyTo(outputStream) { bytesCopied ->
val progress = bytesCopied * 100 / total
if (progress - emittedProgress > 5) {
delay(100)
emit(DownloadStatus.Progress(progress.toInt()))
emittedProgress = progress
}
}
}
}
emit(DownloadStatus.Done(file))
} else {
throw IOException(response.toString())
}
}.catch {
file.delete()
emit(DownloadStatus.Error(it))
}.flowOn(Dispatchers.IO)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 扩展方法 读写文件并返回下载进度
* @receiver InputStream
* @param out OutputStream
* @param bufferSize Int
* @param progress Function1<Long, Unit>
* @return Long
*/
inline fun InputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, progress: (Long) -> Unit): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)

progress(bytesCopied)
}
return bytesCopied
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 下载文件界面
*/
class DownloadFragment : Fragment() {

private val URL = "https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"

private fun download() {
// sdcard/Android/data/com.zch.flowpractice/files/pic.png
val file = File(requireActivity().getExternalFilesDir(null)?.path, "pic.jpg")
lifecycleScope.launch {
DownloadManager.download(URL, file).collect { status ->
when (status) {
is DownloadStatus.Progress -> {
progressBar.progress = status.value
tvProgress.text = "${status.value}%"
}

is DownloadStatus.Error -> {
ToastUtils.showLong("下载错误")
}

is DownloadStatus.Done -> {
progressBar.progress = 100
tvProgress.text = "100%"
ToastUtils.showShort("下载完成")
}

else -> {
ToastUtils.showShort("下载失败")
}
}
}
}
}
}

Flow 与 Room 的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Room 实体类声明
*/
@Entity
data class User(
// 主键
@PrimaryKey
val uid: Int,
// 数据库里存入的字段名
@ColumnInfo(name = "first_name")
val firstName: String,
// 数据库里存入的字段名
@ColumnInfo(name = "last_name")
val lastName: String
)
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Dao 类声明
*/
@Dao
interface UserDao {
// 这里设置了一下冲突,如果两条记录相同则会替换
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)

// 这里不需要挂起(返回 Flow 或 LiveData 都不需要)
@Query("SELECT * FROM user")
fun getAll(): Flow<List<User>>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 数据库操作类
*
* entities 数据库里存入的表,可是多个
* version 数据库的版本号
* exportSchema 是否生成 json 文件,用于查看数据库的结构
*/
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDataBase : RoomDatabase() {

/**
* Dao 对象
*/
abstract fun UserDao(): UserDao

companion object {
private var instance: AppDataBase? = null

fun getInstance(context: Context): AppDataBase {
// 对象锁
return instance ?: synchronized(this) {
Room.databaseBuilder(context, AppDataBase::class.java, "user_db")
.build().also { instance = it }
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* User ViewModel
*/
class UserViewModel(app: Application) : AndroidViewModel(app) {

/**
* 插入数据
* @param user User
*/
fun insert(user: User) {
viewModelScope.launch {
AppDataBase.getInstance(getApplication()).UserDao().insert(user)
}
}

/**
* 获取所有数据
* @return Flow<List<User>>
*/
fun getAll(): Flow<List<User>> {
return AppDataBase.getInstance(getApplication()).UserDao().getAll()
.catch { e ->
e.printStackTrace()
}.flowOn(Dispatchers.IO)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 用户界面
*/
class UserFragment : BaseBindingFragment() {

private val mBinding: FragmentUserBinding by lazy {
FragmentUserBinding.inflate(layoutInflater)
}

private val mUserAdapter: UserAdapter by lazy {
UserAdapter()
}

private val mUserViewModel: UserViewModel by viewModels()

override fun getBindingRoot() = mBinding.root

override fun initView() {
mBinding.rvUser.adapter = mUserAdapter
}

override fun initEvent() {
mBinding.btnAddUser.setOnClickListener {
mBinding.run {
// 插入数据
mUserViewModel.insert(
User(
edtUid.text.toString().toInt(),
edtFirstName.text.toString(),
edtLastName.text.toString()
)
)
}
}
}

override fun initData() {
lifecycleScope.launch {
// 获取所有数据
mUserViewModel.getAll().collect {
mUserAdapter.submitList(it)
}
}
}
}

Flow 与 Retrofit 的应用

1
2
3
4
5
6
/**
* 文章实体类
* @property id 文章 id
* @property text 文章内容
*/
data class Article(val id: Int, val text: String)
1
2
3
4
5
6
7
8
/**
* 文章接口
*/
interface ArticleApi {

@GET("article")
suspend fun searchArticle(@Query("key") key: String): List<Article>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Retrofit 网络请求管理类
*/
object RetrofitClient {

private const val URL = ""

private val instance: Retrofit by lazy {
Retrofit.Builder()
.client(OkHttpClient.Builder().build())
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val articleApi: ArticleApi by lazy {
instance.create(ArticleApi::class.java)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 文章 ViewModel
*/
class ArticleViewModel : ViewModel() {

val articles = MutableLiveData<List<Article>>()

fun searchArticle(key: String) {
viewModelScope.launch(Dispatchers.Main) {
flow {
val list = RetrofitClient.articleApi.searchArticle(key)
emit(list)
}.flowOn(Dispatchers.IO)
.catch { e -> e.printStackTrace() }
.collect {
articles.value = it
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 文章界面
*/
class ArticleFragment : BaseBindingFragment() {

private val articleViewModel by viewModels<ArticleViewModel>()

private val mBinding: FragmentArticleBinding by lazy {
FragmentArticleBinding.inflate(layoutInflater)
}

override fun getBindingRoot() = mBinding.root

override fun initView() {
}

override fun initEvent() {
}

override fun initData() {
// 请求数据
articleViewModel.searchArticle("三国演义")

// 监听数据据返回
articleViewModel.articles.observe(viewLifecycleOwner) {

}
}
}

基础类型

Java 中存在 int, float, boolean 等基础类型,这些基础类型在 Kotlin 里将全部以对象的形式继续存在。有几点变化需要注意。

1
2
3
4
5
6
7
// Int 无法自动转换为 Double, 需要自己先做类型转换(as Double, toDouble(), 方式很多)
var a: Int = 2
var b: Double = a.toDouble()

// Char 不能直接等值于其对应的 ASCII 码值,需要类型转换
var c: Char = 'a'
var x: Int = c.toInt()

变量声明

Kotlin 中使用 var 定义可读可写变量,使用 val 定义只读变量(相当于 Java 当中的 final)。定义变量时,如果满足类型推断,类型可以省略。

1
2
3
var id = "100" // 类型为 String
var name: String = "张三" // 类型为 String
val age: Int = 30

类型声明

在 Kotlin 语言中,“:” 被广泛用于变量类型的定义。

1
2
3
4
5
6
7
8
9
10
// 定义变量类型
fun test() {
var zhangSan: User
var liSi: User
}

// 定义函数参数和返回值
fun getUser(id: Int): User {
return User(100, "dd", 30)
}

“:”还被用于声明类继承或接口实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open class User {
constructor()
}

// 继承 User 类
class VipUser : User() {

}

interface DB {
fun addUser(user: User)
}

// 实现 DB 接口
class RoomDB : DB {
override fun addUser(user: User) {

}
}

类型检测

Java 中使用 instanceof 来判断某变量是否为某类型,而 Kotlin 中使用 is 来进行类型检测。

1
2
3
4
5
6
7
8
9
10
11
/**
* is 判断属于某类型
* !is 判断不属于某类型
* as 类型强转,失败时抛出类型强转失败异常
* as? 类型强转,但失败时不会抛出异常而是返回 null
*/
if (myObject is User) {
// 只要进来了,就代表 myObject 是 User 类型了,不需要强转就可以直接使用 User 的属性
myObject.id = 101
myObject.name = "Hello"
}

Any 和 Unit

  • Any: Kotlin 的顶层父类是 Any, 对应 Java 中的 Object, 但是比 Object 少了 wait()/notify() 等函数。
  • Unit: Kotlin 中的 Unit 对应 Java 中的 void。

可见性修饰符

  • 默认的可见性修饰符是 public
  • 新增的可见性修饰符 internal 表示仅当前模块(AS 中的 module)可见,其它模块不能访问。

逻辑语句

if-else 语句

Kotlin 中的 if-else 基本和 Java 一致,但还是有一些特殊的地方。比如它可以作为一个逻辑表达式使用,逻辑表达式还可以以代码块的形式出现,代码块最后的表达式作为该块的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 逻辑表达式的使用
fun getMax(x: Int, y: Int): Int {
var max = if (x > y) x else y
return max
}

// 代码块形式的逻辑表达式
fun getMax2(x: Int, y: Int): Int {
var max = if (x > y) {
println("Max num is x.")
x // 返回最后一行,即 x 的值
} else {
println("Max num is y.")
y // 返回最后一行,即 y 的值
}
return max
}

when 语句

Kotlin 中的 when 语句取代了 Java 中的 switch-case 语句,功能上要强大许多,可以有多种形式的条件表达。与 if-else 一样,Kotlin 中的 when 也可以作为逻辑表达式使用,也有返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// when 有判断参数
fun whenDemo(obj: Any) {
when (obj) {
1 -> println("是数字 1")
-1, 0 -> println("是数字 -1 或 0")
in 1..10 -> println("是不大于 10 的正整数")
"abc" -> println("是字符串 abc")
is User -> {
println("是 User 对象")
println(obj.name) // 直接可以使用 User 的属性了
}
else -> println("其它操作")
}
}

// when 没有判断参数
fun whenDemo2(position: Int) {
val columns = 10
when {
position % columns == 0 -> { // position 位于第一列
// ...
}
(position + 1) % columns == 0 -> { // position 位于最后一列
// ...
}
}
}

// when 有返回值
fun whenDemo3(score: Int) {
val result = when (score) {
in 90..100 -> "优秀"
in 80..89 -> "良好"
in 60..79 -> "及格"
else -> "不及格"
}
println("你的成绩$result")
}

循环

1
2
3
4
5
6
7
8
9
10
// 标准函数 repeat():
repeat(100) { // 执行 100 次
}

// .. 包括右边界 [0,99]
for (i in 0..99) {
}
// until 不包括右边界 [0,99]
for (i in 0 until 100) {
}

while 语句、continue 语句和 break 语句等逻辑都与 Java 基本一致,这里不再赘述。

标签

Kotlin 中可以对任意表达式进行标签标记,标签的格式为标识符后跟 @ 符号,例如 abc@、fooBar@ 都是有效的标签。这些标签,可以搭配 return、break、continue 等跳转行为来使用。

1
2
3
4
5
6
7
8
9
10
11
fun labelTest() {
la@ for (i in 1..10) {
println("outer index $i")
for (j in 1..10) {
println("inner index $j")
if (j % 2 == 0) {
break@la
}
}
}
}

数组

使用 arrayOf 创建数组,基本数据类型使用对应的 intArrayOf 等。

1
2
3
val arr1 = arrayOf<Int>()
var arr2 = intArrayOf()
var arr3 = floatArrayOf()

字符串模板

1
2
3
4
5
6
7
8
9
// Java 中字符串模板
String name = "ZhangSan";
int age = 30;
String introduction = String.format("我是s%,今年%d岁了", name, age);

// Kotlin 中字符串模板
val name = "ZhangSan"
val age = 30
val introduction = "我是${name},今年${age}岁了"

函数

Kotlin 中的函数通过关键字 fun 定义的,具体的参数和返回值定义结构如下。

1
fun myFun(para1: Int, para2: String): String { ... }

Kotlin 中的函数可以是全局函数,成员函数或者局部函数,甚至还可以作为某个对象的扩展函数临时添加。

全局函数/顶级函数

全局函数(顶级函数)是文件级别的,以包为作用域。

例如 com.zch.kotlin.biz1 包中的 Common.kt 文件中有以下方法:

1
2
3
fun sayHello() {
println("Hello")
}

那么在当前包的其它文件中就不能有相同的方法 sayHello,否则会报错。

如果 com.zch.kotlin.biz2 包中的某文件中也有以下方法:

1
2
3
fun sayHello() {
println("Hello")
}

那么在其它文件中同时调用两者就需要加上相应的包来访问。

1
2
3
4
5
6
import com.zch.kotlin.biz1.sayHello

fun hello() {
sayHello()
com.zch.kotlin.biz2.sayHello()
}

成员函数

成员函数是在类或对象内部定义的函数。

局部函数

Kotlin 支持局部函数,即一个函数在另一个函数内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun getUserInfo(): String {
val country = "中国"

// 函数中的函数,叫“局部函数”
fun getProvince(): String {
return "${country}广东省" // 局部函数可以访问外部函数的局部变量。
}

val name = "ZhangSan"
val age = 30
val userInfo = "我是${name},今年${age}岁了,我来自${getProvince()}。"

return userInfo
}

如果是频繁调用的函数,不建议声明为局部函数,因为每次调用时,就会产生一个函数对象。

函数参数默认值

函数中的某个参数可以用 “=” 号指定其默认值,调用函数方法时可不传这个参数,但其它参数需要用 “=” 号指定。

1
2
3
fun calculate(a: Int, b: Int = 10, c: Int) = a + b + c // 原本直接 return 的函数可以用 = 符号简化

val sum = calculate(5, c = 20) // sum = 35

扩展函数和扩展属性

Kotlin 支持在包范围内对已存在的类进行函数和属性扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 给 Activity 扩展一个 log 函数
fun Activity.log(msg: String) {
Log.d("tag", "Activity---$msg")
}

// 给 Context 扩展一个 log 函数
fun Context.log(msg: String) {
Log.d("tag", "Context---$msg")
}

log("hello") // 调用 Activity.log
(this as Context).log("hello2") // 调用 Context.log

// 给 ViewGroup 扩展一个 firstChild 属性
val ViewGroup.firstChild: View get() = getChildAt(0)

contentLayout.firstChild // 调用

注意:1、扩展需要在包级范围内进行,如果写在 class 内是无效的。2、已经存在的方法或属性,是无法被扩展的,依旧会调用已有的方法。3、扩展函数是静态解析的,在编译时就确定了调用函数(没有多态)。

infix 函数

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。中缀函数必须满足以下要求:

  • 它们必须是成员函数或扩展函数;
  • 它们必须只有一个参数;
  • 其参数不得接受可变数量的参数且不能有默认值。
1
2
3
4
5
6
7
infix fun Int.shl(x: Int): Int { …… }

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)
1
2
3
4
5
6
7
8
9
10
// 中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。 以下表达式是等价的:

1 shl 2 + 3 等价于 1 shl (2 + 3)
0 until n * 2 等价于 0 until (n * 2)
xs union ys as Set<*> 等价于 xs union (ys as Set<*>)

// 另一方面,中缀函数调用的优先级高于布尔操作符 && 与 ||、is- 与 in- 检测以及其他一些操作符。这些表达式也是等价的:

a && b xor c 等价于 a && (b xor c)
a xor b in c 等价于 (a xor b) in c

中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用 this;不能像常规方法调用那样省略。这是确保非模糊解析所必需的。

1
2
3
4
5
6
7
8
9
class MyStringCollection {
infix fun add(s: String) { /*……*/ }

fun build() {
this add "abc" // 正确
add("abc") // 正确
add "abc" // 错误:必须指定接收者
}
}

内联函数

  • 内联函数配合「函数类型」,可以减少「函数类型」生成的对象 。
  • 使⽤ inline 关键字声明的函数是「内联函数」,在「编译时」会将「内联函数」中的函数体直接插⼊到调⽤处。 所以在写内联函数的时候需要注意,尽量将内联函数中的代码行数减少。

noinline 可以禁止部分函数参数参与内联编译:

1
2
3
inline fun foo(inlined: () -> Unit, noinline notInlined:() -> Unit) {
//......
}

匿名函数

  • 匿名函数的特点是可以明确指定其返回值类型。
  • 它和常规函数的定义几乎相似。他们的区别在于,匿名函数没有函数名。
1
2
3
4
5
// 常规函数
fun test(x : Int , y : Int) : Int = x + y

// 匿名函数
fun(x : Int , y : Int) : Int = x + y

Lambda 表达式

Lambda 表达式的本质是 匿名函数,因为在其底层实现中还是通过匿名函数来实现的。但是我们在用的时候不必关心其底层实现。不过 Lambda 的出现确实是减少了代码量的编写,同时也使代码变得更加简洁明了。

1、Lambda 的特点:

  • Lambda 表达式总是被大括号括着。
  • 其参数(如果存在)在符号 “->” 之前声明(参数类型可以省略)。
  • 函数体(如果存在)在符号 “->” 后面。

2、Lambda 语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
1、无参数的情况:
val/var 变量名 = { 操作的代码 }

// 源代码
private fun myFun1() {
println("无参数")
}

// lambda 代码
val myFun1 = { println("无参数") }

private fun test1() {
myFun1() // 调用
}
// -------------------------------------------
2、有参数的情况:
val/var 变量名 : (参数的类型,参数类型,...) -> 返回值类型 = {参数1,参数2,... -> 操作参数的代码 }
可等价于
// 此种写法:即表达式的返回值类型会根据操作的代码自推导出来。
val/var 变量名 = { 参数1 : 类型,参数2 : 类型, ... -> 操作参数的代码 }

// 源代码
fun myFun2(a: Int, b: Int): Int {
return a + b
}

// lambda
val myFun2: (Int, Int) -> Int = { a, b -> a + b }
// 或者
val myFun2 = { a: Int, b: Int -> a + b }

fun test2() {
myFun2(1, 2) // 调用
}
// -------------------------------------------
3、lambda 表达式作为函数中的参数:
fun test(a : Int, 参数名 : (参数1 : 类型,参数2 : 类型, ... ) -> 表达式返回类型){ ... }

// 源代码
fun myFun3(a: Int, b: Int): Int {
return a + b
}

fun sum(num1: Int, num2: Int): Int {
return num1 + num2
}

// lambda
fun myFun33(a: Int, b: (num1: Int, num2: Int) -> Int): Int {
return a + b.invoke(3, 5)
}

fun test3() {
myFun33(10, { num1: Int, num2: Int -> num1 + num2 }) // 调用
}

invoke() 函数:表示为通过函数变量调用自身,因为上面例子中的变量 b 是一个匿名函数。

3、it

当一个高阶函数中 Lambda 表达式的参数只有一个的时候可以使用 it 来使用此参数。it 可表示为单个参数的隐式名称,是 Kotlin 语言约定的。

1
2
3
4
5
val arr = intArrayOf(1, 2, 3)
var sum = 0
arr.forEach {
sum += it
}

4、下划线(_)

在使用 Lambda 表达式的时候,可以用下划线 (_) 表示未使用的参数,表示不处理这个参数。

1
2
3
4
5
val map = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3")
// 不需要 key 的时候
map.forEach { _, value ->
println("$value")
}

5、带接收者的函数字面值

在 Kotlin 中,提供了指定的接受者对象调用 Lambda 表达式的功能。在函数字面值的函数体中,可以调用该接收者对象上的方法而无需任何额外的限定符。它类似于扩展函数,它允许在函数体内访问接收者对象的成员。

5.1、匿名函数作为接收者类型

匿名函数语法允许你直接指定函数字面值的接收者类型,如果你需要使用带接收者的函数类型声明一个变量,并在之后使用它,这将非常有用。

1
2
val add = fun Int.( other : Int) : Int = this + other
println(2.add(3)) // 输出 5

5.2、Lambda 表达式作为接收者类型

要用 Lambda 表达式作为接收者类型的前提是接收者类型可以从上下文中推断出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HTML {
fun body() {}
}

fun myFun5(init: HTML.() -> Unit): HTML {
val html = HTML() // 创建接收者对象
html.init() // 将该接收者对象传给该 lambda
return html
}

fun test111() {
myFun5 { // 带接收者的 lambda 由此开始
body() // 调用该接收者对象的一个方法
}
}

6、闭包

所谓闭包,即是函数中包含函数,这里的函数我们可以包含(Lambda 表达式,匿名函数,局部函数,对象表达式)。我们熟知,函数式编程是现在和未来良好的一种编程趋势,故而 Kotlin 也有这一个特性。Java 是不支持闭包的。

6.1、携带状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 让函数返回一个函数,并携带状态值
fun myFun4(b: Int): () -> Int {
var a = 3
return fun(): Int {
a++
return a + b
}
}

fun test4() {
val t = myFun4(3)
println(t()) // 输出 7
println(t()) // 输出 8
println(t()) // 输出 9
}

6.2、引用外部变量,并改变外部变量的值

1
2
3
4
5
var sum : Int = 0
val arr = arrayOf(1,3,5,7,9)
arr.filter { it < 7 }.forEach { sum += it }

println(sum) // 输出 9

7、Lambda 表达式简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果函数的最后⼀个参数是 lambda, 那么 lambda 表达式可以放在圆括号之外:
lessons.forEach(){ lesson : Lesson ->
// ...
}

// 如果你的函数传⼊参数只有⼀个 lambda 的话,那么⼩括号可以省略的:
lessons.forEach { lesson : Lesson ->
// ...
}

// 如果 lambda 表达式只有⼀个参数,那么可以省略,通过隐式的 it 来访问:
lessons.forEach { // it
// ...
}

高阶函数

Kotlin 中,高阶函数即指:将函数用作一个函数的参数或者返回值的函数。

将函数用作函数参数的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// sumBy 函数的源码
public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
var sum: Int = 0
for (element in this) {
sum += selector(element)
}
return sum
}

// 调用
fun test() {
val str = "abc"
val sum = str.sumBy { it.toInt() }
println(sum) // 输出 294。因为字符 a 对应的值为 97,b 对应 98,c 对应 99。故而该值即为 97 + 98 + 99 = 294
}

将函数用作一个函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
} finally {
lock.unlock()
}
}

// 调用
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)

// 上面的写法也可以写作:
val result = lock(lock, {sharedResource.operation()} )
// 或者
val result = lock(lock) {
sharedResource.operation()
}

::toBeSynchronized 即为对函数 toBeSynchronized() 的引用。

自定义高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义高阶函数
private fun calculation(num1: Int, num2: Int, result: (Int, Int) -> Int): Int {
return result(num1, num2)
}

// 测试调用
fun test() {
val result1 = calculation(1, 2) { num1, num2 ->
num1 + num2
}
val result2 = calculation(10, 20) { num1, num2 ->
num1 * num2
}
println("result1 = $result1") // 输出:3
println("result2 = $result2") // 输出:200
}

开发中常用的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ParamView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {

var onParamValueClickListener: (() -> Unit)? = null // 定义一个点击事件,给外部处理

init {
tvParamValue?.setOnClickListener {
onParamValueClickListener?.invoke()
}
}
}

// 外部 ParamView 控件处理点击事件
paramView?.onParamValueClickListener = {
// 处理业务逻辑...
}

标准高阶函数

Standard.kt 文件中提供了一系列标准的高阶函数。

函数特点汇总

函数 接收者(this) 传参(it) 返回值(result)
run() 当前类 编译错误 作用域中的最后一个对象
T.run() 类T 编译错误 作用域中的最后一个对象
with() 类T 编译错误 作用域中的最后一个对象
T.let() 当前类 类T 作用域中的最后一个对象
T.also() 当前类 类T 类T
T.apply() 类T 编译错误 类T

实际应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class MyStandard {
companion object {
const val TAG = "MyStandard"
}

// run()
fun runDemo1() {
var name = "AA"
run {
val name = "BB"
Log.e(TAG, name) // BB
}
Log.d(TAG, name) // AA
}

// T.run()
fun runDemo2() {
val result = "ABCDEF".run {
Log.d(TAG, "字符串的长度为 $length") // 字符串的长度为 6
substring(2)
}
Log.d(TAG, result) // CDEF
}

// with()
fun withDemo() {
val result = with("ABCDEF") {
substring(2)
}
Log.d(TAG, result) // CDEF
}

// T.let()
fun letDemo() {
val result = "ABCDEF".let {
it.substring(2) // it 代表 "ABCDEF"
}
Log.d(TAG, result) // CDEF
}

// T.also()
fun alsoDemo() {
val result = "ABCDEF".also {
it.substring(2) // it 代表 "ABCDEF"
}
Log.d(TAG, result) // ABCDEF
}

// T.apply()
fun applyDemo() {
val result = "ABCDEF".apply {
this.substring(2) // this 代表 "ABCDEF"
}
Log.d(TAG, result) // ABCDEF
}
}

通常,T.run()、T.let()、T.also() 和 T.apply() 四个用的比较多,使用时可以通过简单的规则作出⼀些判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(需要返回自身){
if(作用域中使用 this 作为参数){
选择 apply
}
if(作用域中使用 it 作为参数){
选择 also
}
}
if(不需要返回自身){
if(作用域中使用 this 作为参数){
选择 run
}
if(作用域中使用 it 作为参数){
选择 let
}
}

实化类型参数

泛型类型擦除:JVM 中的泛型一般是通过类型擦除实现的,也就是说泛型类实例的类型实参在编译时被擦除,在运行时是不会被保留的。基于这样实现的做法是有历史原因的,最大的原因之一是为了兼容 JDK1.5 之前的版本,当然泛型类型擦除也是有好处的,在运行时丢弃了一些类型实参的信息,对于内存占用也会减少很多。

正因为泛型类型擦除原因在业界 Java 的泛型又称伪泛型。因为编译后所有泛型的类型实参类型都会被替换成 Object 类型或者泛型类型形参指定上界约束类的类型。例如: List、List、List 在 JVM 运行时 Float、String、Student 都被替换成 Object 类型,如果泛型定义是 List 那么运行时 T 被替换成 Student 类型。

Kotlin 和 Java 都存在泛型类型擦除的问题,但 Kotlin 可以通过 inline 函数保证使得泛型类的类型实参在运行时能够保留,这样的操作 Kotlin 中把它称为实化,对应需要使用 reified 关键字。因此,我们可以通过配合 inline + reified 达到「真泛型」的效果。

1、满足实化类型参数函数的必要条件

  • 必须是 inline 内联函数,使用 inline 关键字修饰。
  • 泛型类定义泛型形参时必须使用 reified 关键字修饰。

2、带实化类型参数的函数基本定义

1
2
3
4
5
// 定义
inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T

val obj = User()
val instanceOf = isInstanceOf<User>(obj) // 调用

对于以上例子,我们可以说类型形参 T 是泛型函数 isInstanceOf 的实化类型参数。

3、原理描述

编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。 总之一句话很简单,就是带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引用了具体的类型,而不是类型参数所以不会存在擦除问题。

4、实化类型参数函数不能在 Java 中调用

Kotlin 的实化类型参数函数主要得益于 inline 函数的内联功能,虽然 Java 可以调用普通的内联函数但是失去了内联功能,失去内联功能也就意味实化操作也就化为泡影。

5、实化类型参数函数的使用限制

  • 不能使用非实化类型形参作为类型实参调用带实化类型参数的函数。
  • 不能使用实化类型参数创建该类型参数的实例对象。
  • 不能调用实化类型参数的伴生对象方法。
  • reified 关键字只能标记实化类型参数的内联函数,不能作用于类和属性。

6、实化类型参数函数使用例子

1
2
3
4
5
6
7
8
9
inline fun <reified T> create(): T {
return RETROFIT.create(T::class.java)
}

val api = create<API>() // 调用

inline fun <reified T> Gson.fromJson(json: String) = fromJson(json, T::class.java)

val user = Gson().fromJson<User>(json) // 调用

类与继承

Kotlin 中也使用 class 关键字定义类,所有类都继承于 Any 类,类似于 Java 中 Object 类的概念。类实例化的形式也与 Java 一样,但是去掉了 new 关键字。

构造器

类的构造器分为主构造器(primary constructor)和次级构造器(secondary constructor),前者只能有一个,而后者可以有多个。如果两者都未指定,则默认为无参数的主构造器。

主构造器是属于类头的一部分,用 constructor 关键字定义,如果没有被「可见性修饰符」或者「注解」标注,constructor 可省略。由于主构造器不能包含任何代码,初始化代码需要单独写在 init 代码块中,主构造器的参数只能在 init 代码块和变量初始化时使用。

次级构造器也是用 constructor 关键字定义,必须要直接或间接代理主构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
class User(name: String) { // 主构造器
var id: Int = 0
var name: String = ""

init {
// ...
}

constructor(id: Int, name: String) : this(name) { // 次级构造器
this.id = id
this.name = name
}
}

主构造器中的参数加上 val 或者 var 修饰,那么参数就变成类的成员变量,如果参数和类原来的成员变量一样,那么对应原来的成员变量就要去掉。因此,以上代码一般可以简化为以下代码。

1
2
3
4
5
class User(val id: Int, val name: String) {
init {
// ...
}
}

继承

类继承使用符号 “:” 表示,接口实现也一样,不做原本 Java 中的 extends 和 implement 关键字区分。Kotlin 中取消了 final 关键字,所有类默认都是被 final 修饰,不能被继承。Kotlin 中新增了 open 关键字,被 open 或者 abstract 修饰的类才可以被继承。

单例与伴随对象

Kotlin 使用关键词 object 定义单例类。这里需要注意,是全小写。单例类访问直接使用类名,无构造函数。

1
2
3
4
5
6
7
object LogUtil {
fun d(msg: String) {
if (BuildConfig.DEBUG) {
Log.d("tag", msg)
}
}
}

可通过 AS 工具栏 Tools –> Kotlin –> Show Kotlin Bytecode, 点击右侧的 Decompile 来把当前 Kotlin 代码转换为 Java 代码,来验证以上 object 对象是否转换成 Java 中的单例类。

Java 中使用 static 标识一个类里的静态属性或方法。Kotlin 中没有 static 关键字,改为使用伴随对象,用 companion 修饰单例类 object, 来实现静态属性或方法功能。

1
2
3
4
5
6
7
8
9
10
11
12
class LoginCache {
companion object {
const val USER_NAME = "user_name"
const val PASSWORD = "password"

fun saveLoginInfo(userName: String, password: String) {

}
}
}

LoginCache.saveLoginInfo("ZhangSan","123456") // 调用

内部类

在 Kotlin 中,内部类默认是静态内部类,通过 inner 关键字声明为嵌套内部类。

匿名内部类

形式:

1
2
object : OnItemCLickListener {
}

应用:

1
2
3
4
5
6
7
8
9
10
btnLogin.setOnClickListener(object :View.OnClickListener{
override fun onClick(v: View?) {

}
})

// 以上匿名内部类转换成的 lambda 表达式如下:
btnLogin.setOnClickListener {

}

const

  • const 必须修饰 val。
  • const 只允许在 top-level 级别和 object 中声明。
1
2
3
4
object LogUtil {
const val TAG: String = "LogUtil"
val msg: String = "msg"
}

以上 Kotlin 代码转换成的 Java 代码如下:

1
2
3
4
5
6
7
8
9
10
// 省略了部分不大相关代码
public final class LogUtil {
public static final String TAG = "LogUtil";
private static final String msg;

public final String getMsg() {
return msg;
}
// ...
}

可以看出,const val 可见性为 public static final, 可以直接访问。val 可见性为 private static final, 并且 val 会生成对应属性的 getter 方法,通过方法调用访问。当定义常量时,出于效率考虑,应该使用 const val 方式,避免频繁函数调用。const 修饰的静态变量又称为编译器常量

声明抽象类/接口/枚举/注解

1
2
3
4
5
6
7
8
// 声明抽象类
abstract class
// 声明接⼝
interface
// 声明注解
annotation class
// 声明枚举
enmu class

受检异常

Kotlin 不需要使用 try-catch 强制捕获异常。

获取 Class 对象

1
2
3
4
使用 [类名::class] 获取的是 Kotlin 的类型 KClass
使用 [类名::class.java] 获取的是 Java 的类型

startActivity(Intent(this, MainActivity::class.java))

setter/getter

在 Kotlin 声明属性的时候(没有使用 private 修饰),会自动生成一个私有属性和一对公开的 setter/getter 方法。在写 setter/getter 的时候使⽤ field 来代替内部的私有属性(防⽌递归栈溢出)。

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
var name: String = ""
get() = field // 默认实现方式,可省略
set(value) { // 默认实现方式,可省略
field = value
}
var age: Int = 0
get() = 18 // 获取的值一直都是 18,不会改变
set(value) {
field = if (value < 0) 1 else value
}
}

JvmXxx 相关注解

JvmField

通过 @JvmField 注解可以让编译器只生成一个 public 的成员属性,不生成对应的 setter/getter 方法。

JvmName

顶层函数在文件中定义函数和属性,会直接生成静态的成员,在 Java 中通过「文件名Kt」来访问,同时可以通过 @file:JvmName 注解来修改这个类名。注解要写在包名前面才会起作用。

1
2
3
4
5
6
7
8
@file:JvmName("AppCache")

package com.zch.kotlin.biz1

var mToken: String? = ""
fun saveToken(token: String?) {
mToken = token
}
1
2
3
4
5
// java 中访问方式
public void test() {
AppCacheKt.saveToken("abc123"); // 原来的访问方式
AppCache.saveToken("abc123"); // 加了 @file:JvmName 注解后的访问方式
}

JvmStatic

如果将命名对象或伴生对象中定义的函数注解为 @JvmStatic ,Kotlin 会为这些函数生成静态方法。

命名对象中的 @JvmStatic

1
2
3
4
5
6
7
object SizeUtil {
@JvmStatic
fun dp2px() {
}

fun sp2px() {}
}
1
2
3
4
5
6
7
8
9
// java 方法
public void testSizeUtil() {
// 之前的访问方式
SizeUtil.INSTANCE.dp2px();
SizeUtil.INSTANCE.sp2px();

// 加了 @JvmStatic 注解后,dp2px 变成了静态方法
SizeUtil.dp2px();
}

伴生对象中的 @JvmStatic

1
2
3
4
5
6
7
8
9
10
class Util {
companion object {
@JvmStatic
fun sayYes() {
}

fun sayNo() {
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// java 方法
public void testUtil(){
// 之前的访问方式
Util.Companion.sayYes();
Util.Companion.sayNo();

/**
* 加了 @JvmStatic 注解后,Util 生成了静态方法 sayYes,
* 该静态方法直接访问 Companion 的 sayYes()
*/
Util.sayYes();
}

@JvmStatic 注解也可以应用于对象或伴生对象的属性,使其 getter 和 setter 方法成为该对象或包含伴生对象的类的静态成员。

JvmOverloads

在 Kotlin 中调用默认参数值的方法或者构造函数是完全没问题的,Java 中调用相应 Kotlin 方法时,是必须输入所有参数的值的,Kotlin 中默认参数我们无法使用。而当加上 @JvmOverloads, Kotlin 编译器生成的字节码中有对应的重载方法,我们就可以通过 Java 的重载方式来使用 Kotlin 的代码了,不必要输入所有的参数。

1
2
3
class ParamView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {

}

以上 Kotlin 的自定义控件加上 @JvmOverloads 后,相当于 Java 中的以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ParamView extends LinearLayout {

public ParamView(Context context) {
this(context, null);
}

public ParamView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ParamView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}

Kotlin 中构造函数、顶级函数、类中方法,静态方法(@Jvmstatic 修饰)均可采用 @JvmOverloads 生成对应重载方法。

注解使用处目标

当某个元素可能会包含多种内容(例如构造属性,成员属性),使用注解时可以通过「注解使⽤处⽬标」,让注解对⽬标发⽣作⽤,例如 @file: 、 @get: 、@set: 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
class App : Application() {
companion object {
@JvmStatic
@get:JvmName("instance")
lateinit var instance: Application
private set
}

override fun onCreate() {
super.onCreate()
instance = this
}
}
1
2
App.Companion.getInstance(); // java 中原来调用方式
App.instance(); // java 中现在调用方式

Elvis 操作符

可通过 ?: 的操作来简化 if null 操作。

1
2
3
4
5
6
// lesson.date 为空时使⽤默认值
val date = lesson.date?: "⽇期待定"
// lesson.state 为空时提前返回函数
val state = lesson.state?: return
// lesson.content 为空时抛出异常
val content = lesson.content ?: throw IllegalArgumentException("content expected")

空指针安全

Kotlin 中,当我们定义一个变量时,其默认就是非空类型。如果你直接尝试给他赋值为 null, 编译器会直接报错。Kotlin 中将符号 “?” 定义为安全调用操作符。变量类型后面跟 ? 号定义,表明这是一个可空类型。同样的,在调用子属性和函数时,也可以用字符 ? 进行安全调用。Kotlin 的编译器会在写代码时就检查非空情况。

Kotlin 还提供 “!!” 双感叹号操作符来强制调用对象的属性和方法,无视其是否非空。这是一个挺危险的操作符,除非有特殊需求,否则为了远离 NPE, 还是少用为妙。

1
2
3
4
5
var tvContent: TextView // 非空类型
var tvContent: TextView? // 可空类型

!! // 强行调用符
? // 安全调用符
1
2
3
4
5
6
7
8
9
10
11
12
13
var s1: String = "abc"
s1 = null // 这里编译器会报错

var s2: String? = "abc"
s2 = null // 编译器不会报错

var l1 = s1.length // 可正常编译
var l2 = s2.length // 没有做非空判断,编译器检查报错

if (s2 != null) s2.length // Java 式的判空方案
s2?.length // Kotlin 的安全调用操作符 ?。当 s2 为 null 时,s2?.length 也为 null

s2!!.length // 可能会导致 NPE

lateinit 关键字

1
2
3
4
5
6
7
8
/**
* 1、lateinit 只能修饰 var 可读可写变量。
* 2、lateinit 关键字声明的变量类型必须是不可空类型。
* 3、lateinit 声明的变量不能有初始值。
* 4、lateinit 声明的变量不能是基本数据类型。
* 5、在构造器中初始化的属性不需要 lateinit 关键字。
*/
private lateinit var tvContent: TextView

data class

数据类通常都是由多个属性和对应的 getter、setter 组成。当有大量多属性时,不仅这些类会因为大量的 getter 和 setter 方法而行数爆炸,也使整个工程方法数骤增。

Kotlin 中做了这层特性优化,提供了数据类的简单实现。不用再写 getter、setter 方法,这些都由编译器背后去做,你得到的是一个清爽干净的数据类。数据类用 data class 声明。

1
2
3
4
5
data class Student(
var id: Int,
var name: String,
var age: Int
)

把这个数据类反编译成 Java 代码可知,数据类除了为我们生成了 getter、setter(val 声明的变量不生成 setter 方法)、构造函数外,还有以下方法。

1
2
3
4
equals() / hashCode()
toString()
componentN()...
copy()

copy 函数

1
2
3
public final Student copy(int id, String name, int age) {
return new Student(id, name, age);
}

当要复制一个对象,只改变一些属性,但其余不变,copy() 就是为此而生。

componentN 函数-解构声明(Destructuring Declarations)

1
2
3
4
5
6
7
8
9
10
11
public final int component1() {
return this.id;
}

public final String component2() {
return this.name;
}

public final int component3() {
return this.age;
}

编译器为数据类(data class)自动声明 componentN() 函数,可直接用解构声明。

1
2
3
val student = Student(101, "ZhangSan", 30)
val (id, name, age) = student // 自动赋值给 id, name, age
println("id=$id, name=$name, age=$age")

相等性

两个等号==和三个等号===

两个等号 ==:比较的是对象的内容是否相同,相当于 Java 的 equals()。== 的否定形式为 !=

三个等号 ===:比较的是对象的地址是否相同(即判断是否为同一对象),相当于 Java 的 ==。=== 的否定形式为 !==

1
2
3
4
5
6
7
8
val student1 = Student(101, "ZhangSan", 30)
val student2 = Student(101, "ZhangSan", 30)
if (student1 == student2) { // true
// ...
}
if (student1 === student2) { // false
// ...
}

operator

通过 operator 修饰「特定函数名」的函数,例如 plus 、get, 可以达到重载运算符的效果。

表达式 翻译为
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)

委托

委托,也就是委托模式,它是 23 种经典设计模式中的一种,又名代理模式,在委托模式中,有 2 个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。委托模式中,有三个角色:约束、委托对象和被委托对象。Kotlin 直接支持委托模式,更加优雅,简洁。Kotlin 通过关键字 by 实现委托。

  • 约束:约束是接口或者抽象类,它定义了通用的业务类型,也就是需要被代理的业务。
  • 被委托对象:具体的业务逻辑执行者。
  • 委托对象:负责对真正角色的应用,将约束类定义的业务委托给具体的委托对象。

类委托

类的委托即一个类中定义的方法实际是调用另一个类的对象的方法来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 约束类,我们约定的业务
interface IGamePlayer {
fun play() // 打游戏
}

// 被委托对象,实现了我们约定的业务
class RealGamePlayer(private val name: String) : IGamePlayer {
override fun play() {
println("${name}开始打游戏")
}
}

// 委托对象,通过关键字 by 建立委托类
class DelegateGamePlayer(private val player: IGamePlayer) : IGamePlayer by player

fun main(args: Array<String>) {
val realGamePlayer = RealGamePlayer("张三")
val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
delegateGamePlayer.play() // 输出:张三开始打游戏
}

在 DelegateGamePlayer 声明中,by 子句表示,将 player 保存在 DelegateGamePlayer 的对象实例内部,而且编译器将会生成继承自 IGamePlayer 接口的所有方法,并将调用转发给 player。

可以通过类委托的模式来减少继承。

属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。

属性委托语法格式:

1
val/var <属性名>: <类型> by <表达式>
  • var/val:属性类型(可变/只读)。
  • 属性名:属性名称。
  • 类型:属性的数据类型。
  • 表达式:委托代理类。

by 关键字之后的表达式就是委托,属性的 get() 方法(以及 set() 方法)将被委托给这个对象的 getValue() 和 setValue() 方法。属性委托不必实现任何接口,但必须提供 getValue() 函数(对于 var 属性,还需要 setValue() 函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 被委托类(委托代理类)
* 该类需要包含 getValue() 方法和 setValue() 方法,且参数 thisRef 为进行委托的类的对象,property 为进行委托的属性的对象。
*/
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 这里委托了 ${property.name} 属性"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef${property.name} 属性赋值为 $value")
}
}

fun main(args: Array<String>) {
var token: String by Delegate()
println(token) // 访问该属性,调用 getValue() 函数

token = "xxx" // 调用 setValue() 函数
println(token)
}

其中的参数解释如下:

  • thisRef —— 必须与属性所有者类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。
  • value —— 必须与属性同类型或者是它的子类型。

属性委托的另一种实现方式

Kotlin 标准库中声明了 2 个含所需 operator 方法的 ReadOnlyProperty / ReadWriteProperty 接口。

1
2
3
4
5
6
7
8
interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

被委托类 实现这两个接口其中之一就可以了,val 属性实现 ReadOnlyProperty, var 属性实现 ReadWriteProperty。 

1
2
3
4
5
6
7
8
9
10
class Delegate2 : ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return ""
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
}

var token2: String by Delegate2()

标准库中提供的几个委托

  • 延迟属性(lazy): 其值只在首次访问时计算。
  • 可观察属性(observable): 监听器会收到有关此属性变更的通知。
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

1、延迟属性 lazy

lazy() 是一个函数,接受一个 Lambda 表达式作为参数,返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy() 的 lamda 表达式并记录结果,后续调用 get() 只是返回记录的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val lazyValue: String by lazy {
println("computed!") // 第一次调用输出,第二次调用不执行
"Hello"
}

fun main(args: Array<String>) {
println(lazyValue) // 第一次执行,执行两次输出表达式
println(lazyValue) // 第二次执行,只输出返回值
}

// 执行输出结果
computed!
Hello
Hello

2、可观察属性 Observable

observable 可以用于实现观察者模式。

Delegates.observable() 函数接受两个参数:第一个是初始化值,第二个是属性值被修改时的回调处理器 onChange。回调处理器有三个参数:被赋值的属性、旧值和新值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import kotlin.properties.Delegates

class User {
var name: String by Delegates.observable("A") { property, oldValue, newValue ->
println("旧值:$oldValue -> 新值:$newValue")
}
}

fun main(args: Array<String>) {
val user = User()
user.name = "B"
user.name = "C"
}

// 执行输出结果
旧值:A -> 新值:B
旧值:B -> 新值:C

vetoable 函数vetoableobservable 一样,可以观察属性值的变化,不同的是,vetoable 可以通过处理器函数来决定属性值是否生效

1
2
3
4
5
// 声明一个 Int 类型的属性 vetoableProp, 如果新的值比旧值大,则生效,否则不生效。
var vetoableProp: Int by Delegates.vetoable(0){ _, oldValue, newValue ->
// 如果新的值大于旧值,则生效
newValue > oldValue
}

3、把属性储存在映射中

一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

fun main(args: Array<String>) {
// 构造函数接受一个映射参数
val user = User(mapOf(
"name" to "张三",
"age" to 18
))

// 读取映射值
println(user.name)
println(user.age)
}

集合中函数式 API 操作符

筛选过滤类

slice 系列

操作符可以取集合中一部分元素或者某个元素,最后组合成一个新元素。

  • **slice(indices: IntRange)**:指定切片的起始位置和终止位置,将范围内的元素切出加入到新集合。

    1
    2
    3
    4
    5
    val list = listOf(1, 2, 3, 4, 5).slice(IntRange(2, 4))
    println(list)

    // 打印结果:
    [3, 4, 5]
  • **slice(indices: Iterable<Int>)**:指定下标分别切出对应的元素,放入新集合中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val list = listOf(1, 2, 3, 4, 5).slice(IntRange(2, 4))
    println(list)

    val list2 = listOf(1, 2, 3, 4, 5).slice(listOf(1, 3))
    println(list2)

    // 打印结果:
    [3, 4, 5]
    [2, 4]
filter 系列
  • **filter(predicate: (T) -> Boolean)**:从一个集合筛选出符合条件的元素,并以一个新集合返回。

    1
    2
    3
    4
    5
    val list = listOf(1, 2, 3, 4, 5).filter { it % 2 == 0 }
    println(list)

    // 打印结果:
    [2, 4]
  • **filterTo(destination: C, predicate: (T) -> Boolean)**:从多个集合筛选出符合条件的元素,并最终用一个集合进行收集从每个集合筛选出的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    val numberList1 = listOf(3, 6, 9, 12, 15)
    val numberList2 = listOf(5, 10, 15, 20, 25)
    val numberList3 = listOf(10, 20, 30, 40, 50)

    // filterTo 的 destination 是一个可变集合类型,所以这里使用 mutableListOf 初始化
    val newNumberList = mutableListOf<Int>().apply {
    numberList1.filterTo(this) {
    it % 5 == 0
    }
    numberList2.filterTo(this) {
    it % 5 == 0
    }
    numberList3.filterTo(this) {
    it % 20 == 0
    }
    }
    println(newNumberList)

    // 打印结果:
    [15, 5, 10, 15, 20, 25, 20, 40]
  • **filterIndexed(predicate: (index: Int, T) -> Boolean)**:需要集合元素 index 参与筛选条件。

    1
    2
    3
    4
    5
    6
    7
    val result = listOf(3, 6, 9, 12, 15).filterIndexed { index, item ->
    index > 2 && item % 2 == 0
    }
    println(result)

    // 打印结果:
    [12]
  • **filterIndexedTo(destination: C, predicate: (index: Int, T) -> Boolean)**:从多个集合筛选出符合条件的元素,筛选条件需要 index,并最终用一个集合进行收集从每个集合筛选出的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    val result = mutableListOf<Int>()
    listOf(0, 1, 2, 12, 4).filterIndexedTo(result) { index, item ->
    index == item
    }
    println(result)

    // 打印结果:
    [0, 1, 2, 4]
  • **filterIsInstance()**:一个抽象类集合中含有多种子类型的元素,可以很方便筛选对应子类型的元素,并组成一个集合返回。

    1
    2
    3
    4
    5
    6
    val list = mutableListOf(1, "2", 3, "4", 5f, 6.0, "7")
    val result = list.filterIsInstance<Int>()
    println(result)

    // 打印结果:
    [1, 3]
  • **filterIsInstanceTo(destination: C)**:适用于筛选多个集合的情况。

    1
    2
    3
    4
    5
    6
    7
    val list = mutableListOf(1, "2", 3, "4", 5f, 6.0, "7")
    val result = mutableListOf<String>()
    list.filterIsInstanceTo(result)
    println(result)

    // 打印结果:
    [2, 4, 7]
  • **filterNot(predicate: (T) -> Boolean)filterNotTo(destination: C, predicate: (T) -> Boolean)**:从一个集合筛选出符合条件之外的元素,并以一个新集合返回。它是 filter 操作符取反操作。

  • **filterNotNull()filterNotNullTo(destination: C)**:filterNotNull 操作符可以过滤集合中为 null 的元素。同理 filterNotNullTo 才是真正过滤操作,但是需要从外部传入一个可变集合。

drop 系列
  • **drop(n: Int)**:把集合元素去除一部分,drop 是顺序地删除,n 则表示顺序删除几个元素,最后返回剩余元素集合。

    1
    2
    3
    4
    println(listOf(1, 2, 3, 4, 5).drop(3))

    // 打印结果:
    [4, 5]
  • **dropLast(n: Int)**:根据传入数值 n,表示从右到左倒序地删除 n 个集合中的元素,并返回集合中剩余的元素。

    1
    2
    3
    4
    println(listOf(1, 2, 3, 4, 5).dropLast(3))

    // 打印结果:
    [1, 2]
  • **dropWhile(predicate: (T) -> Boolean)**:从集合的第一项开始去掉满足条件元素,这样操作一直持续到出现第一个不满足条件元素出现为止,返回剩余元素(可能剩余元素有满足条件的元素)。

    1
    2
    3
    4
    println(listOf(1, 2, 32, 40, 5).dropWhile { it < 20 })

    // 打印结果:
    [32, 40, 5]
  • **dropLastWhile(predicate: (T) -> Boolean)**:从集合的最后一项开始去掉满足条件元素,这样操作一直持续到出现第一个不满足条件元素出现为止,返回剩余元素(可能剩余元素有满足条件的元素)。

    1
    2
    3
    4
     println(listOf(1, 2, 32, 40, 5).dropLastWhile { it < 20 })

    // 打印结果:
    [1, 2, 32, 40]
take 系列
  • **take(n: Int)**:从原集合的第一项开始顺序取集合的元素,取 n 个元素,最后返回取出这些元素的集合。换句话说就是取集合前 n 个元素组成新的集合返回。

    1
    2
    3
    4
    println(listOf(1, 2, 32, 40, 5).take(3))

    // 打印结果:
    [1, 2, 32]
  • **takeLast(n: Int)**:从原集合的最后一项开始倒序取集合的元素,取 n 个元素,最后返回取出这些元素的集合。

    1
    2
    3
    4
    println(listOf(1, 2, 32, 40, 5).takeLast(3))

    // 打印结果:
    [32, 40, 5]
  • **takeLastWhile(predicate: (T) -> Boolean)**:从集合的最后一项开始取出满足条件元素,这样操作一直持续到出现第一个不满足条件元素出现为止,暂停取元素,返回取出元素的集合。

    1
    2
    3
    4
    println(listOf(1, 2, 32, 40, 5).takeLastWhile { it < 20 })

    // 打印结果:
    [5]
  • **takeWhile(predicate: (T) -> Boolean)**:从集合的第一项开始取出满足条件元素,这样操作一直持续到出现第一个不满足条件元素出现为止,暂停取元素,返回取出元素的集合。

    1
    2
    3
    4
    println(listOf(1, 2, 32, 40, 5).takeWhile { it < 20 })

    // 打印结果:
    [1, 2]
distinct 系列
  • distinct:去除集合中的重复元素。

    1
    2
    3
    4
    println(listOf(1, 2, 2, 1, 3).distinct())

    // 打印结果:
    [1, 2, 3]
  • **distinctBy(selector: (T) -> K)**:根据操作元素后的结果去除重复元素。

    1
    2
    3
    4
    println(listOf(1, 2, 4, 3, 6).distinctBy { it % 2 })

    // 打印结果:
    [1, 2]

并集类操作符

any、all、count、none 系列
  • **any()**:判断是不是一个集合,若是,则再判断集合是否为空。若为空则返回 false,反之返回 true。若不是集合,则返回 hasNext。

    1
    2
    3
    4
    println(listOf(1, 2).any())

    // 打印结果:
    true
  • **any(predicate: (T) -> Boolean)**:判断集合中是否存在满足条件的元素。若存在则返回 true,反之返回 false。

    1
    2
    3
    4
    println(listOf(1, 2, 32, 40, 5).any { it > 30 })

    // 打印结果:
    true
  • **all(predicate: (T) -> Boolean)**:判断集合中的所有元素是否都满足条件。若是则返回 true,反之则返回 false。

    1
    2
    3
    4
    println(listOf(0, 2, 32, 40, 50).all { it % 2 == 0 })

    // 打印结果:
    true
  • **count()count(predicate: (T) -> Boolean)**:返回集合中的元素个数或查询集合中满足条件的元素个数。

    1
    2
    3
    4
    5
    6
    println(listOf(0, 2, 32, 40, 50).count())
    println(listOf(0, 2, 32, 40, 50).count { it > 10 })

    // 打印结果:
    5
    3
  • **none()none(predicate: (T) -> Boolean)**:如果一个集合是空集合,返回 true 或者集合中没有满足条件的元素,则返回 true。

    1
    2
    3
    4
    5
    6
    println(listOf(0, 2, 32, 40, 50).none()) 
    println(listOf(0, 2, 32, 40, 50).none { it > 50 })

    // 打印结果:
    false
    true
fold 系列
  • **fold(initial: R, operation: (acc: R, T) -> R)**:在一个初始值的基础上,从第一项到最后一项通过一个函数累计所有的元素。

    1
    2
    3
    4
    5
    6
    println(listOf(0, 2, 4, 6).fold(10) { result, element ->
    result + element
    })

    // 打印结果:
    22
  • **foldIndexed(initial: R, operation: (index: Int, acc: R, T) -> R)**:在一个初始值的基础上,从第一项到最后一项通过一个函数累计所有的元素,该函数的参数可以包含元素索引。

    1
    2
    3
    4
    5
    6
    println(listOf("h", "e", "l", "l", "o").foldIndexed("Say") { index, result, element ->
    "$result $element$index"
    })

    // 打印结果:
    Say h0 e1 l2 l3 o4
  • **foldRight(initial: R, operation: (T, acc: R) -> R)**:在一个初始值的基础上,从最后项到第一项通过一个函数累计所有的元素,与 fold 类似,不过是从最后一项开始累计。

    1
    2
    3
    4
    5
    6
    println(listOf("1", "2", "3").foldRight("Say") { element, result ->
    "$result $element"
    })

    // 打印结果:
    Say 3 2 1
  • **foldRightIndexed(initial: R, operation: (index: Int, T, acc: R) -> R)**:在一个初始值的基础上,从最后一项到第一项通过一个函数累计所有的元素,该函数的参数可以包含元素索引。

    1
    2
    3
    4
    5
    6
    println(listOf("1", "2", "3").foldRightIndexed("Say") { index, element, result ->
    "$result $element-$index"
    })

    // 打印结果:
    Say 3-2 2-1 1-0
forEach 系列
  • **forEach(action: (T) -> Unit)**:集合元素的遍历操作符。

    1
    2
    3
    4
    5
    6
    listOf("1", "2", "3").forEach {
    print(it)
    }

    // 打印结果:
    123
  • **forEachIndexed(action: (index: Int, T) -> Unit)**:集合中带元素下标的遍历操作。

    1
    2
    3
    4
    5
    6
    listOf("1", "2", "3").forEachIndexed { index, s ->
    print("$index-$s ")
    }

    // 打印结果:
    0-1 1-2 2-3
max、min 系列
  • **maxOrNull()**:获取集合中最大的元素,若为空元素集合,则返回 null。

    1
    2
    3
    4
    println(listOf("B", "C", "A").maxOrNull())

    // 打印结果:
    C
  • **maxByOrNull(selector: (T) -> R)**:根据给定的函数返回最大的一项,如果没有则返回 null。

    1
    2
    3
    4
    5
    6
    println(listOf(-2, 1, 0).maxByOrNull {
    it * -3
    })

    // 打印结果:
    -2
  • **maxWithOrNull(comparator: Comparator<in T>)**:接受一个 Comparator 对象并且根据此 Comparator 对象返回最大元素。

    1
    2
    3
    4
    5
    6
    println(listOf("-20", "-102", "0").maxWithOrNull(compareBy {
    it.length
    }))

    // 打印结果:
    -102

min 操作符作用与 max 相反,包括 minBy 和 minWith。

reduce 系列
  • **reduce(operation: (acc: S, T) -> S)**:从集合中的第一项到最后一项的累计操作,与 fold 操作符的区别是没有初始值。

    1
    2
    3
    4
    5
    6
    println(listOf("Nice", "to", "meet", "you").reduce { result, element ->
    "$result $element"
    })

    // 打印结果:
    Nice to meet you

    其中 reduceIndexed,reduceRight,reduceRightIndexed 操作符都与前述的 fold 相关操作符类似,只是没有初始值。

  • **reduceOrNull(operation: (acc: S, T) -> S)**:从集合中的第一项到最后一项的累计操作,如果集合为空,返回 null。

    reduceRightOrNull 操作符作用等价于 reduceRight,不同的是当集合为空,返回 null。

sum 系列
  • **sum()**:计算集合中所有元素累加的结果。

    1
    2
    3
    4
    println(listOf(1, 2, 3).sum())

    // 打印结果:
    6
  • **sumOf(selector: (T) -> Int)**:计算集合所有元素通过某个函数转换后数据之和。

    1
    2
    3
    4
    println(listOf(1, 2, 3).sumOf { it * 2 })

    // 打印结果:
    12

映射类操作符

flatMap 系列
  • **flatMap(transform: (T) -> Iterable<R>)**:根据条件合并两个集合,组成一个新的集合。

    1
    2
    3
    4
    5
    val strList = listOf("A", "B", "C")
    println(strList.flatMap { listOf(it.plus("1")) })

    // 打印结果:
    [A1, B1, C1]
  • **flatMapTo(destination: C, transform: (T) -> Iterable<R>)**:多个集合的条件合并。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    val strList = listOf("A", "B", "C")
    val strList2 = listOf("a", "b", "c")
    val resultList = mutableListOf<String>().apply {
    strList.flatMapTo(this) {
    listOf(it.plus("1"))
    }
    strList2.flatMapTo(this) {
    listOf(it.plus("2"))
    }
    }
    println(resultList)

    // 打印结果:
    [A1, B1, C1, a2, b2, c2]
group 系列
  • **groupBy(keySelector: (T) -> K)**:分组操作符,根据条件将集合拆分为一个 Map 类型集合。

    1
    2
    3
    4
    5
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.groupBy { if (it.startsWith("Java")) "MyJava" else "MyKotlin" })

    // 打印结果:
    {MyJava=[Java, JavaScript], MyKotlin=[Kotlin, C, C++]}
  • **groupByTo(destination: M, keySelector: (T) -> K)**:分组操作,适用于多个集合的分组操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    val strList2 = listOf("15", "223", "45", "520", "18")
    val mutableMap = mutableMapOf<String, MutableList<String>>().apply {
    strList.groupByTo(this) {
    if (it.startsWith("Java")) "Java" else "Not Java"
    }
    strList2.groupByTo(this) {
    if (it.contains("2")) "2" else "Not 2"
    }
    }
    println(mutableMap)

    // 打印结果:
    {Java=[Java, JavaScript], Not Java=[Kotlin, C, C++], Not 2=[15, 45, 18], 2=[223, 520]}
  • **groupingBy(crossinline keySelector: (T) -> K)**:对元素进行分组,然后一次将操作应用于所有分组。适用于对集合复杂分组的情况。

    1
    2
    3
    4
    5
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.groupingBy { it.startsWith("Java") }.eachCount())

    // 打印结果:
    {true=2, false=3}

    Grouping 支持以下操作:

    • eachCount() 计算每个组中的元素。
    • fold() 与 reduce() 对每个组分别执行 fold 与 reduce 操作,作为一个单独的集合并返回结果。
    • aggregate() 随后将给定操作应用于每个组中的所有元素并返回结果。 这是对 Grouping 执行任何操作的通用方法。当折叠或缩小不够时,可使用它来实现自定义操作。
map 系列
  • **map(transform: (T) -> R)**:集合变换,遍历每个元素并执行给定表达式,最终形成新的集合。

    1
    2
    3
    4
    5
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.map { it.plus("-1") })

    // 打印结果:
    [Java-1, Kotlin-1, C-1, C++-1, JavaScript-1]
  • **mapTo(destination: C, transform: (T) -> R)**:多个集合的元素转换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val strList = listOf("A", "B", "C")
    val strList2 = listOf("a", "b", "c")
    println(mutableListOf<String>().apply {
    strList.mapTo(this) { it.plus("-1") }
    strList2.mapTo(this) { it.plus("-2") }
    })

    // 打印结果:
    [A-1, B-1, C-1, a-2, b-2, c-2]
  • **mapIndexed(transform: (index: Int, T) -> R)**:带有元素下标的集合转换。

    1
    2
    3
    4
    5
    val strList = listOf("A", "B", "C")
    println(strList.mapIndexed { index, s -> "$index-$s" })

    // 打印结果:
    [0-A, 1-B, 2-C]
  • **mapIndexedTo(destination: C, transform: (index: Int, T) -> R)**:带有元素下标的多个集合转换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val strList = listOf("A", "B", "C")
    val strList2 = listOf("a", "b", "c")
    println(mutableListOf<String>().apply {
    strList.mapIndexedTo(this) { index, s -> "$index-$s" }
    strList2.mapIndexedTo(this) { index, s -> "$index-$s" }
    })

    // 打印结果:
    [0-A, 1-B, 2-C, 0-a, 1-b, 2-c]
  • **mapNotNull(transform: (T) -> R?)**:同 map 函数的作用相同,不过其过滤了集合转换后为 null 的元素。

    1
    2
    3
    4
    5
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.mapNotNull { if (it.startsWith("Java")) null else it })

    // 打印结果:
    [Kotlin, C, C++]
  • **mapNotNullTo(destination: C, transform: (T) -> R?)**:同 mapNotNull 函数的作用相同,它适用于多集合操作场景。

  • **mapIndexedNotNullmapIndexedNotNullTo**:同理。

元素类操作符

element 系列
  • **elementAt(index: Int)**:获取集合指定下标的元素。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.elementAt(1)) // 打印:Kotlin
  • **elementAtOrElse(index: Int, defaultValue: (Int) -> T)**:获取对应下标的集合元素。若下标越界,返回默认值。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.elementAtOrElse(10) { "unknown" }) // 打印:unknown
  • **elementAtOrNull(index: Int)**:获取对应下标的集合元素。若下标越界,返回 null。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.elementAtOrNull(10)) // 打印:null
first、last 系列
  • **first()**:获取集合第一个元素,若集合为空集合,这会抛出 NoSuchElementException 异常。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.first()) // 打印:Java
  • **first(predicate: (T) -> Boolean)**:获取集合中指定条件的第一个元素。若不满足条件,抛异常。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.first { it.length > 5 }) // 打印:Kotlin
  • **firstOrNull()**:获取集合第一个元素,若集合为空集合,返回 null。

  • **firstOrNull{}**:获取集合满足条件的首个元素,若无则返回 null。

first 等操作符对应的是 last 等相关操作符,即取集合最后一个元素等。

find 系列
  • **find(predicate: (T) -> Boolean)**:获取集合满足条件的首个元素,若无则返回 null,其实就是 firstOrNull{}。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.find { it.length > 5 }) // 打印:Kotlin
  • **findLast(predicate: (T) -> Boolean)**:获取集合满足条件的最后一个元素,若无则返回 null,其实就是 lastOrNull{}。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.findLast { it.length > 5 }) // 打印:JavaScript
single 系列
  • **single()**:当集合中只有一个元素时,返回该元素,否则抛异常。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.single()) // 打印:java.lang.IllegalArgumentException: List has more than one element.
  • **single(predicate: (T) -> Boolean)**:找到集合中满足条件的元素,若只有单个元素满足条件,则返回该元素,否则抛异常。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.single { it.startsWith("K") }) // 打印:Kotlin

singleOrNullsingleOrNull{} 操作符 只是将前述的抛异常改为返回 null。

component 系列
  • **component1()component5()**:用于获取第 1 到第 5 个元素。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.component3()) // 打印:C
indexOf 系列
  • **indexOf(element: T)**:返回指定元素的下标,若不存在,则返回 -1。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.indexOf("C")) // 打印:2
  • **indexOfFirst(predicate: (T) -> Boolean)**:返回满足条件的第一个元素的下标,若不存在,则返回 -1。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.indexOfFirst { it.length > 5 }) // 打印:1
  • **indexOfLast(predicate: (T) -> Boolean)**:返回满足条件的最后一个元素的下标,若不存在,则返回 -1。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.indexOfLast { it.length > 5 }) // 打印:4

排序类操作符

reverse 系列
  • **reversed()**:反转集合元素。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.reversed()) // 打印:[JavaScript, C++, C, Kotlin, Java]
sort 系列
  • **sorted()**:对集合中的元素自然升序排序。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.sorted()) // 打印:[C, C++, Java, JavaScript, Kotlin]
  • **sortedDescending()**:与 sorted 操作符相反,为倒序。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.sortedDescending()) // 打印:[Kotlin, JavaScript, Java, C++, C]
  • **sortedBy(crossinline selector: (T) -> R?)**:根据条件升序,即把不满足条件的放在前面,满足条件的放在后面。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.sortedBy { it.length }) // 打印:[C, C++, Java, Kotlin, JavaScript]
  • **sortedByDescending(crossinline selector: (T) -> R?)**:与 sortedBy 操作符相反,为倒序。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.sortedByDescending { it.length }) // 打印:[JavaScript, Kotlin, Java, C++, C]

生成类操作符

partition 系列
  • **partition(predicate: (T) -> Boolean)**:将一个集合按条件拆分为两个 pair 组成的新集合。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.partition { it.startsWith("Java") }) // 打印:([Java, JavaScript], [Kotlin, C, C++])
plus 系列
  • **plus(element: T)**:合并两个集合中的元素,组成一个新的集合。也可以使用符号 +

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.plus(listOf(1, 2, 3))) // 打印:[Java, Kotlin, C, C++, JavaScript, 1, 2, 3]
  • **plusElement(element: T)**:往集合中添加一个元素。

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.plusElement("CSS")) // 打印:[Java, Kotlin, C, C++, JavaScript, CSS]
zip 系列
  • **zip(other: Array<out R>)**:由两个集合按照相同的下标组成一个新集合。该新集合的类型是:List<Pair>

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.zip(listOf(1, 2, 3))) // 打印:[(Java, 1), (Kotlin, 2), (C, 3)]
  • **zipWithNext(transform: (a: T, b: T) -> R)**:集合中相邻元素组成 pairs

    1
    2
    val strList = listOf("Java", "Kotlin", "C", "C++", "JavaScript")
    println(strList.zipWithNext()) // 打印:[(Java, Kotlin), (Kotlin, C), (C, C++), (C++, JavaScript)]

本笔记整理自大地老师的 Dart 笔记,同时加入了自己写的一些测试例子。

环境搭建

要在我们本地开发 Dart 程序的话首先需要安装 Dart SDK

官方文档:https://dart.dev/get-dart

1
2
3
4
5
6
7
8
9
10
11
windows(推荐):

http://www.gekorm.com/dart-windows/

mac:

如果 mac 电脑没有安装 brew 这个工具首先第一步需要安装它: https://brew.sh/

brew tap dart-lang/dart

brew install dart

开发工具

1
2
3
4
5
6
7
Dart 的开发工具有很多: IntelliJ IDEA  、 WebStorm、 Atom、Vscode 等

这里我们主要给大家讲解的是如果在 vscode 中配置 Dart。

1、找到 vscode 插件安装 Dart

2、找到 vscode 插件安装 code runner, Code Runner 可以运行我们的文件

变量

Dart 中定义变量可以使用具体的类型(int、double、String、bool 等)申明 ,也可以通过 var 关键字来申明变量。

常量

final 和 const 修饰符都可以表示常量。

  • const 值不变,一开始就得赋值。
  • final 可以开始不赋值,只能赋一次。而 final 不仅有 const 的编译时常量的特性,最重要的它是运行时常量,并且 final 是惰性初始化,即在运行时第一次使用前才初始化。
  • 注意:永远不改变的量,请使用 final 或 const 修饰它,而不是使用 var 或其他变量类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    main() {
    const PI = 3.14159;
    // PI = 3.14; // 报错
    print(PI);

    // const x = new DateTime.now(); // 报错

    final a = 1;
    // a = 2; // 报错
    print(a);

    final time = new DateTime.now();
    print(time);
    }

常用数据类型

Dart 中支持以下常用数据类型:

1
2
3
4
5
6
7
8
9
10
11
Numbers(数值)
int
double
Strings(字符串)
String
Booleans(布尔)
bool
List(数组)
在 Dart 中,数组是列表对象,所以大多数人只是称它们为列表
Maps(字典)
通常来说,Map 是一个键值对相关的对象。键和值可以是任何类型的对象。每个键只出现一次,而一个值则可以出现多次

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
main() {
/**
* 1、字符串类型(String)
*/
var str1 = "Hello";
String str2 = "World";
print("$str1 $str2"); // 字符串拼接
print(str1 + " " + str2); // 字符串拼接

// 三个单引号或三个双引号,让字符串原样输出
var str3 = '''Hello
World''';
print(str3);

/**
* 2、数值类型(int、double)
*/
int x = 1; // 必须为整型
x = 20;
print(x);

double d = 99.9; // 整型或浮点型
d = 100;
print(d);

/**
* 3、布尔类型(bool)
*/
bool flag = true;
if (flag) {
print('真');
} else {
print('假');
}

/**
* 4、List(数组/集合)
*/
// 第一种定义方式
var list1 = ['a', 'b', 'c'];
print(list1);

// 第二种定义方式
var list2 = new List();
list2.add('a');
list2.add('b');
list2.add('c');
print(list2);

// 定义 List 时指定类型
var list3 = new List<String>();
// list3.add(1); // 错误
list3.add("Hello");
print(list3);

/**
* 5、Maps(字典)
*/
// 第一种定义方式
var person1 = {
"name": "张三",
"age": 20,
"work": ["程序员", "送外卖"]
};
print(person1);
print(person1["name"]);
print(person1["work"]);

// 第二种定义方式
var person2 = new Map();
person2["name"] = "李四";
person2["age"] = 21;
person2["work"] = ["程序员", "送外卖"];
print(person2);
}

使用 is 关键词来判断类型

1
2
3
4
5
6
7
8
9
10
main() {
var x = 123;
if (x is String) {
print('是 String 类型');
} else if (x is int) {
print('是 int 类型'); // 执行这里
} else {
print('是其他类型');
}
}

运算符

Dart 运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
算术运算符

+ - * / ~/ (取整) %(取余)

关系运算符

== != > < >= <=

逻辑运算符

! && ||

赋值运算符

基础赋值运算符 = ??=
复合赋值运算符 += -= *= /= %= ~/=

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
main() {
/**
* 1、算术运算符
*/
var x = 10;
var y = 3;
print(x / y); // 3.3333333333333335
print(x ~/ y); // 3
print(x % y); // 1

/**
* 2、关系运算符
*/
var a = 123;
var b = 123;
var c = "123";
var d = 123.0;
if (a == b) {
print('a 等于 b'); // 执行
}
if (a == c) {
print('a 等于 c'); // 不执行
}
if (a == d) {
print('a 等于 d'); // 执行
}

/**
* 3、逻辑运算符
*/
bool flag = false;
print(!flag); // true(取反)

/**
* 4、基础赋值运算符(= ??=)
*/
int aa = 10;
int bb;
aa ??= 100;
bb ??= 200; // 因为 b 为空,所以重新赋值
print(aa); // 10
print(bb); // 200

/**
* 5、复合赋值运算符 += -= *= /= %= ~/=
*/
int xx = 10;
xx += 10;
print(xx); // 20
}

条件表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
main() {
/**
* 1、if else / switch case
*/
bool flag = true;
if (flag) {
print("true");
} else {
print("false");
}

String sex = "女";
switch (sex) {
case "男":
print("男");
break;
case "女":
print("女");
break;
default:
print("不男不女");
break;
}

/**
* 2、三目运算符
*/
String s = flag ? "我是 true" : "我是 false";
print(s);

/**
* 3、?? 运算符
*/
var a = 20;
var b = a ?? 30;
print(b); // 20
}

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
main() {
/**
* 1、Number 与 String 类型之间的转换
*/
int num = 20;
double d = 30.0;
String str = "50";
print(num.toString()); // 20
print(d.toString()); // 30.0
print(int.parse(str)); // 50
print(double.parse(str)); // 50.0
print(int.tryParse("100a")); // null
// print(int.parse("100a")); // 报错
try {
var myNum = double.parse("");
print(myNum);
} catch (err) {
print(err); // FormatException: Invalid double
}

/**
* 2、其他类型转换成 Booleans 类型
*/
var myStr = '';
if (myStr.isEmpty) {
print('myStr 为空'); // 执行
} else {
print('myStr 不为空');
}

var myStr2;
if (myStr2 == null) {
print('myStr2 为空'); // 执行
} else {
print('myStr2 不为空');
}

var x = 0 / 0;
print(x); // NaN
if (x.isNaN) {
print("x is NaN"); // 执行
}
}

循环语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
main() {
/**
* 1、++ -- 表示自增 自减 1
* 在赋值运算里面,如果 ++ -- 写在前面,先运算后赋值,如果 ++ -- 写在后面,先赋值后运算
*/
var a = 10;
var b = a--;
print(a); // 9
print(b); // 10

var aa = 100;
var bb = --aa;
print(aa); // 99
print(bb); // 99

/**
* 2、for 语句
*/
var list = ['新闻0', '新闻1', '新闻2', '新闻3', '新闻4'];
for (var i = 0; i < list.length; i++) {
if (i % 2 == 0) {
print(list[i]);
}
}

/**
* 3、while / do while
*/
var i = 1;
var sum = 0;
while (i <= 100) {
sum += i;
i++;
}
print(sum); // 5050

i = 1;
sum = 0;
do {
sum += i;
i++;
} while (i <= 100);
print(sum); // 5050

/**
* 4、break / continue
*/
sum = 0;
for (var i = 0; i < 5; i++) {
if (i > 3) {
break; // 结束当前循环体
}
sum += i;
}
print(sum); // 1 + 2 + 3 = 6

sum = 0;
for (var i = 0; i < 5; i++) {
if (i % 2 == 0) {
continue; // 本次循环跳过下面代码
}
sum += i;
}
print(sum); // 1 + 3 = 4
}

集合类型

List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
main() {
/*
List 里面常用的属性和方法:

常用属性:
length 长度
reversed 翻转
isEmpty 是否为空
isNotEmpty 是否不为空
常用方法:
add 增加
addAll 拼接数组
indexOf 查找 传入具体值
remove 删除 传入具体值
removeAt 删除 传入索引值
fillRange 修改
insert(index,value); 指定位置插入
insertAll(index,list) 指定位置插入 List
toList() 其他类型转换成 List
join() List 转换成字符串
split() 字符串转化成 List
forEach
map
where
any
every
*/
List myList = ['AA', 'BB', 'CC'];
myList.add('DD');
var newList = myList.reversed.toList();
print(newList); // [DD, CC, BB, AA]

// myList.fillRange(1, 3, 'aaa');
// print(myList); // [AA, aaa, aaa, DD]

var str = myList.join('-');
print(str); // AA-BB-CC-DD
}

Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
main() {
/**
* Set
* 1、用它最主要的功能就是去除数组重复内容;
* 2、Set 是没有顺序且不能重复的集合,所以不能通过索引去获取值;
*/
var set = new Set();
set.add('AA');
set.add('BB');
set.add('AA');
print(set); // {AA, BB}
print(set.toList()); // [AA, BB]

var myList = ['AA', 'BB', 'AA', 'CC', 'AA', 'BB'];
var mySet = new Set();
mySet.addAll(myList);
print(mySet); // {AA, BB, CC}
print(mySet.toList()); // [AA, BB, CC]
}

Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
main() {
/*
映射(Maps)是无序的键值对:

常用属性:
keys 获取所有的 key 值
values 获取所有的 value 值
isEmpty 是否为空
isNotEmpty 是否不为空
常用方法:
remove(key) 删除指定 key 的数据
addAll({...}) 合并映射 给映射内增加属性
containsValue 查看映射内的值 返回 true/false
forEach
map
where
any
every
*/
Map zhangSan = {"name": "张三", "age": 20};
var liSi = new Map();
liSi["name"] = "李四";
liSi["age"] = 21;

print(zhangSan.keys.toList()); // [name, age]
print(zhangSan.values.toList()); // [张三, 20]

zhangSan.addAll({
"sex": "男",
"work": ['敲代码', '送外卖']
});
print(zhangSan); // {name: 张三, age: 20, sex: 男, work: [敲代码, 送外卖]}

zhangSan.remove("sex");
print(zhangSan); // {name: 张三, age: 20, work: [敲代码, 送外卖]}
}

集合的 forEach、map、where、any、every 方法使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
main() {
/**
* List
*/
var list = ['AA', 'BB', 'CC'];
for (var i = 0; i < list.length; i++) {
print(list[i]);
}

for (var item in list) {
print(item);
}

list.forEach((element) {
print(element);
});

var myList = [1, 3, 4];
var newList = myList.map((e) {
return e * 2;
});
print(newList.toList()); // [2, 6, 8]

var newList2 = myList.where((element) {
return element > 1;
});
print(newList2.toList()); // [3, 4]

var f = myList.any((element) {
// 只要集合里面有满足条件的就返回 true
return element > 3;
});
print(f); // true

var g = myList.every((element) {
// 每一个都满足条件返回 true, 否则返回 false
return element > 1;
});
print(g); // false

/**
* Set
*/
var set = new Set();
set.addAll(['1', '2', '3']);
set.forEach((element) => print(element));

/**
* Map
*/
var zhangSan = {"name": "张三", "age": 20};
zhangSan.forEach((key, value) {
print("$key---$value");
});
}

函数

函数定义、作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
main() {
/*
内置方法/函数:

print();

自定义方法:
自定义方法的基本格式:

返回类型 方法名称(参数1,参数2,...){
方法体
return 返回值;
}
*/
printInfo("Hello World");
int sum = getSum(1, 2);
print(sum);

// 演示方法的作用域
void exec() {
void sayHello() {
print("Hello");
}

sayHello();
}

exec(); // 调用方法
// sayHello(); // 错误写法
}

void printInfo(String info) {
print(info);
}

int getSum(int a, int b) {
return a + b;
}

函数传参、默认参数、可选参数、命名参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
main() {
/**
* 1、基本传参
*/
var sum = getSum(10);
print(sum); // 55

/**
* 2、可选参数
*/
printUserInfo("张三", 20);
printUserInfo("李四");

/**
* 3、默认参数
*/
printUserInfo2("张三", "女", 20);
printUserInfo2("李四");

/**
* 4、命名参数
*/
printUserInfo3("王五", sex: "未知", age: 22);

/**
* 5、把方法当参数的方法
*/
fun2(fun1);
}

int getSum(int n) {
var sum = 0;
for (var i = 1; i <= n; i++) {
sum += i;
}
return sum;
}

/**
* 带可选参数
*/
void printUserInfo(String name, [int age]) {
if (age == null) {
print("姓名:$name---年龄保密");
} else {
print("姓名:$name---年龄:$age");
}
}

/**
* 带默认参数
*/
void printUserInfo2(String name, [String sex = '男', int age]) {
if (age == null) {
print("姓名:$name---性别:$sex---年龄保密");
} else {
print("姓名:$name---性别:$sex---年龄:$age");
}
}

/**
* 带命名参数
*/
void printUserInfo3(String name, {String sex = '男', int age}) {
if (age == null) {
print("姓名:$name---性别:$sex---年龄保密");
} else {
print("姓名:$name---性别:$sex---年龄:$age");
}
}

fun1() {
print("fun1");
}

fun2(fn) {
fn();
}

箭头函数、匿名函数、自执行方法、方法递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
main() {
/**
* 1、箭头函数
* 只能写一行,不能多行。
*/
List myList = [1, 3, 4];
myList.forEach((element) => print(element));

var newList = myList.map((e) => e > 2 ? e * 2 : e);
print(newList.toList()); // [1, 6, 8]

/**
* 2、匿名函数
*/
var myNum = () {
return 10;
};
print(myNum()); // 10

/**
* 3、自执行方法
*/
((int n) {
print("我是自执行方法");
print(n * 10); // 180
})(18);

/**
* 4、方法递归
*/
var sum = fn(100);
print(sum); // 5050
}

int fn(int n) {
if (n == 1) {
return 1;
}
return n + fn(n - 1);
}

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main() {
/**
* 全局变量特点:全局变量常驻内存、全局变量污染全局;
* 局部变量的特点:不常驻内存会被垃圾机制回收、不会污染全局;
*
* 闭包:
* 1、可以常驻内存,不污染全局;
* 2、函数嵌套函数,内部函数会调用外部函数的变量或参数,变量或参数不会被系统回收(不会释放内存);
* 3、写法:函数嵌套函数,并 return 里面的函数,这样就形成了闭包;
*/
var f = fn();
f(); // 2
f(); // 3
f(); // 4
}

fn() {
var x = 1;
return () {
x++;
print(x);
};
}

类与对象

面向对象的介绍

面向对象编程(OOP)的三个基本特征是:封装、继承、多态

  • 封装:封装是对象和类概念的主要特性。封装,把客观事物封装成抽象的类,并且把自己的部分属性和方法提供给其他对象调用, 而一部分属性和方法则隐藏。
  • 继承:面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承是指这样一种能力:它可以使用现有类的功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
  • 多态:允许将子类类型的指针赋值给父类类型的指针, 同一个函数调用会有不同的执行效果 。

Dart 所有的东西都是对象,所有的对象都继承自 Object 类。

Dart 是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是 Object 的子类

一个类通常由属性和方法组成。

Dart 和其他面向对象语言不一样,Dart 中没有 public、private、protected 这些访问修饰符,但是我们可以使用 _ 把一个属性或者方法定义成私有。

类与对象的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person {
String _name; // 加了下划线表示私有属性
int age;

// 默认构造函数的简写
Person(this._name, this.age);

Person.now() {
print('我是命名构造函数');
}

Person.setInfo(String name, int age) {
this._name = name;
this.age = age;
}

void printInfo() {
print("${this._name}---${this.age}");
}

void _sayHello() {
print("_sayHello 是一个私有方法");
}

execRun() {
this._sayHello();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Rect {
int width;
int height;

Rect(this.width, this.height);

getArea() {
// 通过方法获取面积
return this.width * this.height;
}

get area {
// 通过属性获取面积
return this.width * this.height;
}

set areaHeight(height) {
this.height = height;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'Person.dart';
import 'Rect.dart';

main() {
Person p = new Person("张三", 20);
p.printInfo();
p.execRun();

Person p2 = Person.now();

Person p3 = Person.setInfo("李四", 21);
p3.printInfo();

var r = Rect(10, 5);
print(r.getArea()); // 50
print(r.area); // 50

r.areaHeight = 10;
print(r.area); // 100
}

类中的初始化列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rect {
int width;
int height;

// 在构造函数体运行之前初始化实例变量
Rect()
: width = 3,
height = 2 {
print("${this.width}---${this.height}");
}

get area {
return this.width * this.height;
}
}
1
2
3
4
5
6
import 'Rect.dart';

main() {
var r = Rect();
print(r.area); // 6
}

类的静态变量与静态函数

Dart 中的静态成员:

1、使用 static 关键字来实现类级别的变量和函数;

2、静态方法不能访问非静态成员,非静态方法可以访问静态成员;

1
2
3
4
5
6
class Person {
static String name = '张三';
static void showName() {
print(name);
}
}
1
2
3
4
5
6
import 'Person.dart';

main() {
print(Person.name);
Person.showName();
}

对象操作符

Dart 中的对象操作符:

  • ? 条件运算符
  • as 类型转换
  • is 类型判断
  • .. 级联操作(连缀)
1
2
3
4
5
6
7
8
9
class Person {
String name;
num age;
Person(this.name, this.age);

void printInfo() {
print("${this.name}---${this.age}");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'Person.dart';

main() {
Person p = Person("张三", 20);
p?.printInfo(); // 张三---20

Person p2;
p2?.printInfo(); // 无打印结果

if (p is Person) {
p.name = '李四';
}
p.printInfo(); // 李四---20

print(p is Object); // true

var p3;
p3 = '';
p3 = new Person("王五", 22);
p3.printInfo(); // 王五---22
(p3 as Person).printInfo(); // 王五---22

Person p4 = new Person("AA", 10);
p4.printInfo(); // AA---10

p4
..name = "BB"
..age = 11
..printInfo(); // BB---11
}

类的继承

面向对象的三大特性:封装 、继承、多态

Dart 中的类的继承:

  • 子类使用 extends 关键词来继承父类;
  • 子类会继承父类里面可见的属性和方法,但是不会继承构造函数;
  • 子类能复写父类的方法;
1
2
3
4
5
6
7
8
9
10
class Person {
String name;
num age;
Person(this.name, this.age);
Person.xxx(this.name, this.age);

void printInfo() {
print("${this.name}---${this.age}");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'Person.dart';

class Teacher extends Person {
String sex;
Teacher(String name, num age, String sex) : super(name, age) {
this.sex = sex;
}
// Teacher(String name, num age, String sex) : super.xxx(name, age) {
// this.sex = sex;
// }

@override
void printInfo() {
// super.printInfo(); //自类调用父类的方法
print("${this.name}---${this.age}---${this.sex}");
}

run() {
print("${this.name}***${this.age}***${this.sex}");
}
}
1
2
3
4
5
6
7
import 'Teacher.dart';

main() {
Teacher t = new Teacher('张三', 20, '男');
t.printInfo(); // 张三---20---男
t.run(); // 张三***20***男
}

抽象类

Dart 抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口。

  • 抽象类通过 abstract 关键字来定义;
  • 抽象方法不能用 abstract 声明,Dart 中没有方法体的方法我们称为抽象方法;
  • 如果子类继承抽象类必须得实现里面的抽象方法;
  • 如果把抽象类当做接口实现的话,必须得实现抽象类里面定义的所有属性和方法;
  • 抽象类不能被实例化,只有继承它的子类可以;

extends 抽象类和 implements 的区别:

  • 如果要复用抽象类里面的方法,并且要用抽象方法约束自类的话,我们就用 extends 继承抽象类;
  • 如果只是把抽象类当做标准的话,我们就用 implements 实现抽象类;
1
2
3
4
5
6
7
8
abstract class Animal {
eat(); // 抽象方法
run(); // 抽象方法

printInfo() {
print('我是一个抽象类里面的普通方法');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import 'Animal.dart';

class Dog extends Animal {
@override
eat() {
print("Dog eat ...");
}

@override
run() {
print("Dog run ...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import 'Animal.dart';

class Cat extends Animal {
@override
eat() {
print("Cat eat ...");
}

@override
run() {
print("Cat run ...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
import 'Cat.dart';
import 'Dog.dart';

main() {
Dog d = new Dog();
d.eat(); // Dog eat ...
d.printInfo(); // 我是一个抽象类里面的普通方法

Cat c = new Cat();
c.eat(); // Cat eat ...
c.printInfo(); // 我是一个抽象类里面的普通方法
}

多态

  • 允许将子类类型的指针赋值给父类类型的指针,同一个函数调用会有不同的执行效果;
  • 子类的实例赋值给父类的引用;
  • 多态就是父类定义一个方法不去实现,让继承他的子类去实现,每个子类有不同的表现;
1
2
3
4
5
6
7
8
9
10
11
import 'Animal.dart';
import 'Cat.dart';
import 'Dog.dart';

main() {
Animal d = new Dog();
d.eat(); // Dog eat ...

Animal c = new Cat();
c.eat(); // Cat eat ...
}

接口

和 Java 一样,Dart 也有接口,但是和 Java 还是有区别的:

  • Dart 的接口没有 interface 关键字定义接口,而是普通类或抽象类都可以作为接口被实现;
  • 同样使用 implements 关键字进行实现;
  • Dart 的接口有点奇怪,如果实现的类是普通类,需要将普通类中抽象的属性和方法全部覆写一遍;
  • 因为抽象类可以定义抽象方法,普通类不可以,所以一般如果要实现像 Java 接口那样的方式,一般会使用抽象类;
  • 建议使用抽象类定义接口;

一个类可以实现多个接口。

1
2
3
4
5
6
abstract class DB {
// 当做接口 接口:就是约定 、规范
String uri;
insert(String data);
delete(String id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'DB.dart';

class MySQL implements DB {
@override
String uri;

@override
insert(String data) {
print("MySQL insert");
}

@override
delete(String id) {
print("MySQL delete");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'DB.dart';

class MsSQL implements DB {
@override
String uri;

@override
insert(String data) {
print("MsSQL insert");
}

@override
delete(String id) {
print("MsSQL delete");
}
}
1
2
3
4
5
6
7
import 'DB.dart';
import 'MySQL.dart';

main() {
DB db = new MySQL();
db.insert('123456');
}

mixins

mixins 的中文意思是混入,就是在类中混入其他功能。

在 Dart 中可以使用 mixins 实现类似多继承的功能。

因为 mixins 使用的条件,随着 Dart 版本一直在变,这里讲的是 Dart 2.x 中使 用 mixins 的条件:

  • 作为 mixins 的类只能继承自 Object, 不能继承其他类;
  • 作为 mixins 的类不能有构造函数;
  • 一个类可以 mixins 多个 mixins 类;
  • mixins 绝不是继承,也不是接口,而是一种全新的特性;

mixins 的实例类型就是其超类的子类型。mixins 使用 with 关键字实现其功能。

1
2
3
4
5
6
class A {
String info = "this is A";
printA() {
print("A");
}
}
1
2
3
4
5
class B {
printB() {
print("B");
}
}
1
2
3
4
5
6
7
import 'A.dart';
import 'B.dart';
import 'Person.dart';

class C extends Person with A, B {
C(String name, num age) : super(name, age);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'A.dart';
import 'B.dart';
import 'C.dart';

main() {
var c = new C("张三", 20);
c.printA(); // A
c.printB(); // B
print(c.info); // this is A
c.printInfo(); // 张三---20

print(c is C); // true
print(c is A); // true
print(c is B); // true
}

泛型

通俗理解:泛型就是解决类、接口、方法的复用性、以及对不特定数据类型的支持(类型校验)。

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
main() {
print(getData(1)); // A
print(getData2(2)); // 2
}

getData<T>(T value) {
return "A";
}

T getData2<T>(T value) {
// return "A"; // 错误,只能返回 T 类型
return value;
}

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
main() {
PrintClass p = new PrintClass<int>();
p.add(1);
p.add(2);
p.add(3);
// p.add("A"); // 报错
p.printInfo();
}

class PrintClass<T> {
List list = new List<T>();
void add(T value) {
this.list.add(value);
}

void printInfo() {
for (var i = 0; i < this.list.length; i++) {
print(this.list[i]);
}
}
}

泛型接口

需求:实现数据缓存的功能。有文件缓存和内存缓存,内存缓存和文件缓存按照接口约束实现。

  1. 定义一个泛型接口,约束实现它的子类必须有 getByKey(key) 和 setByKey(key,value);
  2. 要求 setByKey 的时候的 value 的类型和实例化子类的时候指定的类型一致;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
main() {
MemoryCache m = MemoryCache<Map>();
m.setByKey("index", {"name": "张三", "age": 20}); // 我是内存缓存,把 key = index value = {name: 张三, age: 20} 写入到了内存中
}

abstract class Cache<T> {
getByKey(String key);
void setByKey(String key, T value);
}

class MemoryCache<T> implements Cache<T> {
@override
getByKey(String key) {
return null;
}

@override
void setByKey(String key, T value) {
print("我是内存缓存,把 key = ${key} value = ${value} 写入到了内存中");
}
}

class FileCache<T> implements Cache<T> {
@override
getByKey(String key) {
return null;
}

@override
void setByKey(String key, T value) {
print("我是文件缓存,把 key = ${key} value = ${value} 写入到了文件中");
}
}

在 Dart 中,库的使用是通过 import 关键字引入的。library 指令可以创建一个库,每个 Dart 文件都是一个库,即使没有使用 library 指令来指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dart 中的库主要有三种:

1、我们自定义的库
import 'lib/xxx.dart';
2、系统内置库
import 'dart:math';
import 'dart:io';
import 'dart:convert';
3、pub 包管理系统中的库
https://pub.dev/packages
https://pub.flutter-io.cn/packages
https://pub.dartlang.org/flutter/

1、需要在自己项目根目录新建一个 pubspec.yaml
2、在 pubspec.yaml 文件配置名称、描述、依赖等信息
3、然后运行 pub get 获取包下载到本地
4、项目中引入库 import 'package:http/http.dart' as http; 看文档使用

导入自己本地库

1
2
3
4
5
6
import 'Person.dart';

main() {
var p = new Person("张三", 20);
p.printInfo(); // 张三---20
}

导入系统内置库

1
2
3
4
5
6
import 'dart:math';

main() {
print(max(10, 20)); // 20
print(min(-1, -2)); // -2
}

导入系统内置库实现请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'dart:convert';
import 'dart:io';

main() async {
var result = await getDataFromZhihuAPI();
print(result);
}

// api 接口: http://news-at.zhihu.com/api/3/stories/latest
getDataFromZhihuAPI() async {
// 1、创建 HttpClient 对象
var httpClient = new HttpClient();
// 2、创建 Uri 对象
var uri = new Uri.http('news-at.zhihu.com', '/api/3/stories/latest');
// 3、发起请求,等待请求
var request = await httpClient.getUrl(uri);
// 4、关闭请求,等待响应
var response = await request.close();
// 5、解码响应的内容
return await response.transform(utf8.decoder).join();
}

async 和 await

这两个关键字的使用只需要记住两点:①、只有 async 方法才能使用 await 关键字调用方法;②、如果调用别的 async 方法必须使用 await 关键字;

async 是让方法变成异步,await 是等待异步方法执行完成。

1
2
3
4
5
6
7
8
main() async {
var result = await testAsync();
print(result); // testAsync
}

testAsync() async {
return "testAsync";
}

导入 pub 包管理系统中的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub 包管理系统:

1、从下面网址找到要用的库
https://pub.dev/packages
https://pub.flutter-io.cn/packages
https://pub.dartlang.org/flutter/

2、创建一个 pubspec.yaml 文件,内容如下

name: xxx
description: A new flutter module project.
dependencies:
date_format: ^1.0.9

3、配置 dependencies

4、运行 pub get 获取远程库

5、看文档引入库使用
1
2
3
4
5
import 'package:date_format/date_format.dart';

main() {
print(formatDate(DateTime(1989, 02, 21), [yyyy, '-', mm, '-', dd])); // 1989-02-21
}

库重命名与冲突解决

1
2
3
4
5
6
7
8
9
当引入两个库中有相同名称标识符的时候,如果是 Java, 通常我们通过写上完整的包名路径来指定使用的具体标识符,甚至不用 import 都可以,
但是 Dart 里面是必须 import 的。当冲突的时候,可以使用 as 关键字来指定库的前缀。如下例子所示:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;


Element element1 = new Element(); // Uses Element from lib1.
lib2.Element element2 = new lib2.Element(); // Uses Element from lib2.

部分导入

1
2
3
4
5
6
7
8
9
如果只需要导入库的一部分,有两种模式:

模式一:只导入需要的部分,使用 show 关键字,如下例子所示:

import 'package:lib1/lib1.dart' show foo;

模式二:隐藏不需要的部分,使用 hide 关键字,如下例子所示:

import 'package:lib2/lib2.dart' hide foo;

MyMath.dart

1
2
3
4
5
6
7
getOne() {
return "one";
}

getTwo() {
return "two";
}
1
2
3
4
5
6
import 'MyMath.dart' show getOne;

main() {
print(getOne());
// print(getTwo()); // 报错
}

延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
延迟加载也称为懒加载,可以在需要的时候再进行加载。
懒加载的最大好处是可以减少 APP 的启动时间。

懒加载使用 deferred as 关键字来指定,如下例子所示:

import 'package:deferred/hello.dart' deferred as hello;

当需要使用的时候,需要使用 loadLibrary() 方法来加载:

greet() async {
await hello.loadLibrary();
hello.printGreeting();
}

2.13 之后的一些新特性

Null safety

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
Null safety 翻译成中文的意思是空安全。

null safety 可以帮助开发者避免一些日常开发中很难被发现的错误,并且额外的好处是可以改善性能。

Flutter2.2.0(2021 年 5 月 19 日发布)之后的版本都要求使用 null safety。

1、? 可空类型
2、! 类型断言
*/

String? getData(apiUrl) {
if (apiUrl != null) {
return "this is server data";
}
return null;
}

void printLength(String? str) {
try {
print(str!.length);
} catch (e) {
print("str is null");
}
}

void main() {
// 1、? 可空类型
String? str = "abc"; // String? 表示 str 是一个可空类型
str = null; // 允许设置为 null
print(str);

print(getData("http://www.baidu.com"));
print(getData(null));

// 2、! 类型断言
String? str2 = "this is str";
// str2 = null;
// 如果 str2 不等于 null 会打印 str2 的长度,如果等于 null 会抛出异常
print(str2!.length);

printLength("str");
printLength(null);
}

late 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* late 关键字主要用于延迟初始化。
*/
class Person {
late String name;
late int age;
void setInfo(String name, int age) {
this.name = name;
this.age = age;
}

String getInfo() {
return "${this.name}---${this.age}";
}
}

void main(args) {
Person p = new Person();
p.setInfo("张三", 20);
print(p.getInfo());
}

required 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* required 翻译成中文的意思是需要、依赖
*
* 最开始 @required 是注解,现在它已经作为内置修饰符,主要用于允许根据需要标记任何命名参数(函数或类),
* 使得它们不为空。因为可选参数中必须有个 required 参数或者该参数有个默认值。
*/
String printInfo(String username, {int age = 10, String sex = "男"}) {
return "姓名:$username---性别:$sex--年龄:$age";
}

String printInfo2(String username, {required int age, required String sex}) {
return "姓名:$username---性别:$sex--年龄:$age";
}

void main(args) {
print(printInfo('张三'));

print(printInfo('张三', age: 20, sex: "女"));

// age 和 sex 必须传入
print(printInfo2('张三', age: 22, sex: "女"));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* name 可以传入也可以不传入,age 必须传入
*/
class Person {
String? name; // 可空属性
int age;
Person({this.name, required this.age}); // age 必须传入

String getInfo() {
return "${this.name}---${this.age}";
}
}

void main(args) {
Person p = new Person(name: "张三", age: 20);
print(p.getInfo()); // 张三---20

Person p1 = new Person(age: 20);
print(p1.getInfo()); // null---20
}

性能优化

回顾 Dart 常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
Dart 常量: final 和 const 修饰符。
1、const 声明的常量是在编译时确定的,永远不会改变;
2、final 声明的常量允许声明后再赋值,赋值后不可改变,final 声明的变量是在运行时确定的;
3、final 不仅有 const 的编译时常量的特性,最重要的它是运行时常量,并且 final 是惰性初始化,即在运行时第一次使用前才初始化。
*/

void main() {
// const 常量
// const PI = 3.14;
// PI = 3.14159; // const 定义的常量没法改变
// print(PI);

// final 常量
// final PI = 3.14;
// print(PI);

final a;
a = 13;
// a = 14;
print(a);

final d = new DateTime.now();
}

const、identical 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
dart:core 库中 identical 函数的用法介绍如下:

用法:
bool identical(
Object? a,
Object? b
)
检查两个引用是否指向同一个对象。
*/

void main() {
// var o1 = new Object();
// var o2 = new Object();
// print(identical(o1, o2)); // false,不共享存储空间
// print(identical(o1, o1)); // true,共享存储空间

// var o1 = Object();
// var o2 = Object();
// print(identical(o1, o2)); // false
// print(identical(o1, o1)); // true

// 表示实例化常量构造函数
// o1 和 o2 共享了存储空间
// var o1 = const Object();
// var o2 = const Object();
// print(identical(o1, o2)); // true,共享存储空间
// print(identical(o1, o1)); // true,共享存储空间

// print(identical([2], [2])); // false

// var a = [2];
// var b = [2];
// print(identical(a, b)); // false,不共享存储空间

const c = [2];
const d = [3];
print(identical(c, d)); // false,不共享存储空间

// 发现:const 关键词在多个地方创建相同的对象的时候,内存中只保留了一个对象。
// 共享存储空间条件:1、常量 2、值相等。
}

普通构造函数

1
2
3
4
5
6
7
8
9
10
11
class Container {
int width;
int height;
Container({required this.width, required this.height});
}

void main() {
var c1 = new Container(width: 100, height: 100);
var c2 = new Container(width: 100, height: 100);
print(identical(c1, c2)); // false,c1 和 c2 在内存中存储了 2 份
}

常量构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
常量构造函数总结如下几点:
1、常量构造函数需以 const 关键字修饰;
2、const 构造函数必须用于成员变量都是 final 的类;
3、如果实例化时不加 const 修饰符,即使调用的是常量构造函数,实例化的对象也不是常量实例;
4、实例化常量构造函数的时候,多个地方创建这个对象,如果传入的值相同,只会保留一个对象;
5、Flutter 中 const 修饰不仅仅是节省组件构建时的内存开销,Flutter 在需要重新构建组件的时候,由于这个组件是不应该改变的,重新构建没有任何意义,因此 Flutter 不会重新构建 const 组件。
*/

// 常量构造函数
class Container {
final int width;
final int height;
const Container({required this.width, required this.height});
}

void main() {
var c1 = Container(width: 100, height: 100);
var c2 = Container(width: 100, height: 100);
print(identical(c1, c2)); // false

var c3 = const Container(width: 100, height: 100);
var c4 = const Container(width: 100, height: 100);
print(identical(c3, c4)); // true

var c5 = const Container(width: 100, height: 110);
var c6 = const Container(width: 120, height: 100);
print(identical(c5, c6)); // false
}
// 实例化常量构造函数的时候,多个地方创建这个对象,如果传入的值相同,只会保留一个对象。

概述

Android 在处理后台任务上,根据不同的需求给我们提供了 Service、JobScheduler、Loader 等。然而,大量的后台任务势必会过度消耗设备的电量。为了在设备电量和用户体验之间达到一个比较好的平衡,谷歌推出了 WorkManager。

WorkManager 是一个 Android 库,它在工作的触发器(如适当的网络状态和电池条件)满足时, 优雅地运行可推迟的后台工作。WorkManager 尽可能使用框架 JobScheduler , 以帮助优化电池寿命和批处理作业。在 Android 6.0(API 级 23)下面的设备上, 如果 WorkManager 已经包含了应用程序的依赖项,则尝试使用 Firebase JobDispatcher。否则,WorkManager 返回到自定义 AlarmManager 实现,以优雅地处理您的后台工作。

主要功能

  • 最高向后兼容到 API 14
    • 在运行 API 23 及以上级别的设备上使用 JobScheduler
    • 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager
  • 添加网络可用性或充电状态等工作约束
  • 调度一次性或周期性异步任务
  • 监控和管理计划任务
  • 将任务链接起来
  • 确保任务执行,即使应用或设备重启也同样执行任务
  • 遵循低电耗模式等省电功能

重要特点

  • 针对不需要立即执行的任务:比如向后端服务发送日志或分析数据,定期将应用数据与服务器同步等。从业务角度看,这些任务不需要立即执行。
  • 保证任务一定会被执行:即使应用程序当前不在运行中,哪怕彻底退出,或者设备重新启动,任务仍然会在适当的时候执行。这是因 WorkManager 有自己的数据库,关于任务的所有信息和数据都保存在这个数据库中。

WorkManager 不适用于应用进程结束时能够安全终止的运行中后台工作,也不适用于需要立即执行的任务。请查看后台处理指南,了解哪种解决方案符合您的需求。

使用

添加相关依赖

使用 Java 或 Kotlin 语言将 WorkManager 依赖项添加到您的 Android 项目中。依赖链接

1
2
3
4
5
6
7
8
9
dependencies {
def work_version = "2.3.1"

// (Java only)
implementation "androidx.work:work-runtime:$work_version"

// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
}

使用 Worker 定义任务

Worker 是一个抽象类,用来指定需要执行的具体任务。我们需要继承 Worker 类,并实现它的 doWork 方法,所有需要在任务中执行的代码都在该方法中编写。

1
2
3
4
5
6
7
8
9
10
11
12
class UploadLogWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

override fun doWork(): Result {
LogUtil.d("UploadLogWorker", "doWork()")
return Result.success()
}

override fun onStopped() {
super.onStopped()
// 当任务结束时会回调这里
}
}

doWork 方法最后返回一个 Result,这个 Result 是一个枚举,它有几个固定的值:

  • Result.success() 任务成功。
  • Result.Failure() 任务失败。
  • Result.Retry() 遇到暂时性失败,此时可使用 WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) 来重试。

使用 WorkRequest 配置任务

通过 WorkRequest 配置我们的任务何时运行以及如何运行

  • 设置任务触发条件 。比如,我们可以设置设备处于充电中,网络已连接,并且电量充足的情况下才执行我们的任务(完整的触发条件列表,请参阅 Constraints.Builder 参考文档)。

    1
    2
    3
    4
    5
    val constraints = Constraints.Builder()
    .setRequiresCharging(true) // 充电中
    .setRequiredNetworkType(NetworkType.CONNECTED) // 网络已连接
    .setRequiresBatteryNotLow(true) // 电量充足
    .build()
  • 将 constraints 设置到 WorkRequest 中。WorkRequest 是抽象类,它有两个子类,OneTimeWorkRequest 和 PeriodicWorkRequest,分别对应一次性任务和周期性任务。

    1
    2
    3
    val oneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadLogWorker::class.java)
    .setConstraints(constraints) // 设置触发条件
    .build()
  • 设置延迟执行任务。如果任务没有设置触发条件,或者所有触发条件都符合了,系统可能立刻执行任务,如果你希望再延后执行,则可以使用 setInitialDelay 方法。以下示例设置符合触发条件后,至少经过 5 分钟后再执行。

    1
    2
    3
    4
    val oneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadLogWorker::class.java)
    .setConstraints(constraints)
    .setInitialDelay(5, TimeUnit.MINUTES) // 符合触发条件后,至少经过 5 分钟后再执行
    .build()

    任务执行的确切时间还取决于 WorkRequest 中使用的触发条件和系统优化。WorkManager 经过设计,能够在满足这些触发条件的情况下提供可能的最佳行为。

  • 设置指数退避策略。如果需要 WorkManager 重新尝试执行任务,可以让 Worker 的 doWork 方法返回 Result.retry()。系统有默认的指数退避策略来帮助我们重新执行任务,我们也可以使用 setBackoffCriteria 方法来自定义指数退避策略。

    1
    2
    3
    val oneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadLogWorker::class.java)
    .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
    .build()

    比如 Worker 线程的执行出现异常,比如服务器宕机,那么我们可能就希望过一段时间再重新执行任务。

  • 任务的输入/输出。输入和输出值以键值对的形式存储在 Data 对象中。

    在 WorkRequest 中设置输入数据。

    1
    2
    3
    4
    val inputData = workDataOf("name" to "张三", "id" to 112134)
    val uploadLogRequest = OneTimeWorkRequest.Builder(UploadLogWorker::class.java)
    .setInputData(inputData)
    .build()

    在 Worker 的 doWork 方法中取出输入数据。类似地,Data 类可用于输出返回值。要返回 Data 对象,请将它包含到 Result 的 Result.success() 或 Result.failure() 中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class UploadLogWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    override fun doWork(): Result {
    val name = inputData.getString("name")
    val id = inputData.getInt("id", 0)
    LogUtil.d("name-->$name, id--->$id")

    val outputData = workDataOf("name" to name, "id" to id)
    return Result.success(outputData)
    }
    }

    按照设计,Data 对象应该很小,值可以是字符串、基元类型或数组变体。如果需要将更多数据传入和传出工作器,应该将数据放在其它位置,例如 Room 数据库。Data 对象的大小上限为 10KB。

  • 为任务设置标签

    设置了 Tag 后,可以通过 WorkManager.cancelAllWorkByTag(String) 取消使用特定 Tag 的所有任务,通过 WorkManager.getWorkInfosByTagLiveData(String) 返回 LiveData 和具有该 Tag 的所有任务的状态列表。

    1
    2
    3
    4
    5
    6
    val oneTimeWorkRequest1 = OneTimeWorkRequest.Builder(UploadLogWorker::class.java)
    .addTag("A")
    .build()
    val oneTimeWorkRequest2 = OneTimeWorkRequest.Builder(UploadLogWorker::class.java)
    .addTag("A")
    .build()

    以上两个任务的 Tag 都设置为 A , 使这两个任务成为了一个组:A 组,这样的好处是以后可以操作整个组。

将 WorkRequest 提交给系统

1
WorkManager.getInstance(applicationContext).enqueue(uploadLogRequest)

观察任务的状态

将任务提交给系统后,可通过 WorkInfo 获知任务的状态,WorkInfo 包含了任务的 id、tag、Worker 对象传递过来的 outputData,以及当前的状态。以下三种方式可以得到 WorkInfo 对象。

1
2
3
WorkManager.getInstance(applicationContext).getWorkInfosByTag()
WorkManager.getInstance(applicationContext).getWorkInfoById()
WorkManager.getInstance(applicationContext).getWorkInfosForUniqueWork()

如果希望实时获知任务的状态,上面三个方法还有对于的 LiveData 方法。

1
2
3
WorkManager.getInstance(applicationContext).getWorkInfosByTagLiveData()
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData()
WorkManager.getInstance(applicationContext).getWorkInfosForUniqueWorkLiveData()

通过 LiveData,我们便可在任务状态发生变化的时候收到通知。

1
2
3
4
5
6
7
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(uploadLogRequest.id).observe(this, Observer { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val name = workInfo.outputData.getString("name")
val id = workInfo.outputData.getInt("id", 0)
LogUtil.d("name-->$name, id-->$id")
}
})

取消任务

可根据 tag、id 取消任务,也可以取消全部任务。

1
2
3
4
WorkManager.getInstance(applicationContext).cancelAllWorkByTag()
WorkManager.getInstance(applicationContext).cancelWorkById()
WorkManager.getInstance(applicationContext).cancelUniqueWork()
WorkManager.getInstance(applicationContext).cancelAllWork()

周期任务 PeriodicWorkRequest

WorkRequest 有两种实现,OneTimeWorkRequest(一次性任务)和 PeriodicWorkRequest(周期性任务)。OneTimeWorkRequest 在任务成功完成后就结束了,而 PeriodicWorkRequest 会按设定的时间周期执行。二者使用起来无太大差别。

1
2
3
val uploadWorkRequest = PeriodicWorkRequest.Builder(UploadLogWorker::class.java, 15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()

周期性任务的时间间隔不能少于 15 分钟

任务链

  • 并发任务。使用 **WorkManager.getInstance().beginWith(…).enqueue()**。

    1
    2
    // request1,request2 同时执行
    WorkManager.getInstance(applicationContext).beginWith(request1,request2).enqueue()
  • 串发任务。使用 **WorkManager.getInstance().beginWith().then().then()…enqueue()**。

    1
    2
    // 先执行 request1,然后执行 request2
    WorkManager.getInstance(applicationContext).beginWith(request1).then(request2).enqueue()
  • 组合任务。使用 WorkContinuation.combine() 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // AB 串发,CD 串发,这两个串之间并发执行后,把汇总结果给到 E
    val chuan1 = WorkManager.getInstance(applicationContext)
    .beginWith(A)
    .then(B)
    val chuan2 = WorkManager.getInstance(applicationContext)
    .beginWith(C)
    .then(D)
    WorkContinuation.combine(listOf(chuan1, chuan2))
    .then(E)
    .enqueue()


注意点

  • WorkManager 是根据系统版本决定用 JobScheduler 或者 BroadcastReceiver + AlarmManager 的组合,如果某个系统不允许 AlarmManager 自动唤起,那么 WorkManager 就可能无法正常工作。
  • 实际操作中,周期任务的执行与所设定的时间可能有差别,执行时间可能也没有太明显的规律。

文中 Demo GitHub 地址

参考资料:

  • Android Developers
  • WorkManager的基本使用

二进制的一些概念

在二进制数里,最高位 0 表示正数,1 表示负数。

原码

一个正数,按照绝对值大小转换成的二进制数;一个负数按照绝对值大小转换成的二进制数,然后最高位补 1,称为原码。

1
2
 5 的原码是:00000000 00000000 00000000 00000101
-5 的原码是:10000000 00000000 00000000 00000101

反码

正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反(即 0 变 1,1 变 0)。

1
2
正数 00000000 00000000 00000000 00000101 的反码还是 00000000 00000000 00000000 00000101
负数 10000000 00000000 00000000 00000101 的反码却是 11111111 11111111 11111111 11111010

补码

正数的补码与原码相同,负数的补码为该数的反码加 1。

负数 10000000 00000000 00000000 00000101 的反码是 11111111 11111111 11111111 11111010,那么补码为:

1
11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011

位运算基础

基本的位操作符有与、或、异或、取反、左移、右移这 6 种,它们的运算规则如下所示:

符号 描述 运算规则
& 两个位都为 1 时,结果才为 1
| 两个位只要有一位为 1,结果都为 1
^ 异或 两个位相同为 0,不同为 1
~ 取反 0 变 1,1 变 0
<< 左移 各二进位全部左移若干位,高位丢弃,低位补 0
>> 右移 各二进位全部右移若干位,对无符号数,高位补 0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补 0(逻辑右移)

注意

  1. 在这 6 种操作符,只有 ~ 取反是单目操作符,其它 5 种都是双目操作符。

  2. 位操作只能用于整型数据,对 float 和 double 类型进行位操作会被编译器报错。

  3. 对于移位操作,在微软的 VC6.0 和 VS2008 编译器都是采取算术称位即算术移位操作,算术移位是相对于逻辑移位,它们在左移操作中都一样,低位补 0 即可,但在右移中逻辑移位的高位补 0 而算术移位的高位是补符号位。如下面代码会输出 -4 和 3。

    1
    2
    System.out.println((15) >> 2); // 3
    System.out.println((-15) >> 2); // -4

    15 = 00000000 00000000 00000000 00001111(二进制),右移二位,高位补 0,得到

    00000000 00000000 00000000 00000011 即 3。

    -15 = 11111111 11111111 11111111 11110001(二进制),右移二位,最高位由符号位填充,得到

    11111111 11111111 11111111 11111100 即 -4。

  4. 位操作符的运算优先级比较低,因此应尽量使用括号来确保运算顺序。

  5. 位操作还有一些复合操作符,如 &=、|=、 ^=、<<=、>>=。

常用的位运算技巧

判断奇偶数

一个二进制数 x 的末位为 0 则该数为偶数,为 1 则为奇数,因此可以使用 (x & 1) 的结果来判断 x 的奇偶性,结果为 0,则 x 为偶数,结果为 1,则 x 为奇数。

如要求输出 0 到 10 之间的所有偶数:

1
2
3
4
5
for (int i = 0; i < 10; i++) {
if ((i & 1) == 0) {
System.out.println(i);
}
}

交换两数

1
2
3
4
5
6
7
int a = 10;
int b = 20;
a ^= b;
b ^= a;
a ^= b;
System.out.println("a=" + a); // a=20
System.out.println("b=" + b); // b=10

分析:

第一步,a = a ^ b ①;

第二步,b = b ^ a,把 ① 代入得,b = b ^ (a ^ b),由于 ^ 满足交换律,所以 b = b ^ b ^ a,根据「一个数和自己异或为 0,而 0 和任何数异或结果还是保持不变」的原理得,b = a ②;

第三步,a = a ^ b,将 ①、② 代入得,a = (a ^ b) ^ a 即 a = b ③。

从 ②、③ 得知,a 和 b 的值已经得到了交换。

变换符号

一个数 x 取反加 1 后就会变成 -x,即正数变为负数,负数变为正数。

1
2
3
4
5
6
int a = -5;
int b = 10;
a = ~a + 1;
b = ~b + 1;
System.out.println("a=" + a); // a=5
System.out.println("b=" + b); // b=-10

分析:

-5 = 11111111 11111111 11111111 11111011(二进制),取反再加 1 后变为:

00000000 00000000 00000000 00000101 = 5

注意:这里负数的取反是包括符号位的,不要和负数的反码混淆。

求绝对值

对于正数,绝对值就是它本身,对于负数,直接取反加 1 就得到正数了,所以先判断一个整数的符号再做处理。对于整数 a,它的最高位为 0 代表正数,为 1 代表负数,我们对 a 右移 31 位得到一个整数 i(i = a >> 31),i 值为 0 代表 a 为正数,为 -1 代表 a 为负数。

1
2
3
4
private int abs(int a) {
int i = a >> 31;
return i == 0 ? a : (~a + 1);
}

进一步分析,对于任意整数 a,和 0(32 个 0)异或都保持不变,和 -1(32 个 1)异或相当于取反,所以上面的返回值可以转换为:

1
return i == 0 ? (a ^ i) : ((a ^ i) + 1);

上面返回值再变换下得:

1
return i == 0 ? ((a ^ i) - 0) : ((a ^ i) + 1);

由于 i 的值非 0 即 -1,因此上面返回值可以精简为:

1
return (a ^ i) - i;

通过上面的分析,我们得出求一个整数的绝对值的精简方式,这种方式不需任何判断。

1
2
3
4
private int abs(int a) {
int i = a >> 31;
return (a ^ i) - i;
}

位操作与空间压缩

当我们要标记一个布尔型数组的状态为 true|false 时,我们通常的做法是这样的:

1
boolean[] flag = new boolean[100];

由于数组在内存上也是连续分配的一段空间,我们可以「认为」它是一个很长的整数,因此我们仅需用一个长度为 4(100 / 32 + 1)的整型数组即可完成上面的状态标记。

1
int[] b = new int[4]; // 每个 int 值有 32 位,各个位上为 0 代表 false,为 1 代表 true

由于 boolean 占 1 个字节,int 占 4 个字节,因此,用第二种方式所使用的空间仅为第一种的 1/6 左右。

以下是用筛素数法计算 100 以内的素数的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void printPrime() {
int max = 100;
boolean[] flag = new boolean[max];
int[] primes = new int[max / 3 + 1];
int index = 0;
for (int i = 2; i < max; i++) {
if (!flag[i]) {
primes[index++] = i;
for (int j = i; j < max; j += i) { // 素数的倍数必然不是素数
flag[j] = true;
}
}
}

// 输出 100 以内所有素数
for (int i = 0; i < index; i++) {
System.out.print(primes[i] + " ");
}
}
输出:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

如果是用长度为 4 的整型数组 b 来替代 flag 布尔型数组怎么做?两个关键点,第一,如何将一个整数的指定位上置为 1?第二,如何判断一个整数指定位上是 0 还是 1?

将整数 j 指定位上置为 1:

将 1 向左移位后和其相或来达到在指定位上置 1 的效果

1
2
3
4
5
6
7
private void setOne() {
System.out.println();
int j = 0;
j |= (1 << 10);
System.out.println(Integer.toBinaryString(j));
}
// 输出:10000000000

判断整数 j 指定位上是否为 1:

将 1 向左移位后和原数相与来判断指定位上是 0 还是 1(也可以将原数右移若干位再和 1 相与)

1
2
3
4
5
6
7
8
9
private void isOne() {
int j = 1 << 10;
if ((j & (1 << 10)) != 0) {
System.out.println("指定位上为 1");
} else {
System.out.println("指定位上为 0");
}
}
// 输出:指定位上为 1

再把这种思路扩展到一个整型数组上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void setOne2() {
int max = 40;
int[] b = new int[max / 32 + 1];
for (int i = 0; i < max; i += 3) {
b[i / 32] |= (1 << (i % 32)); // 每 3 个位设置为 1
}

for (int i = 0; i < max; i++) {
if (((b[i / 32] >> i) & 1) == 1) { // 判断是否为 1
System.out.print("1");
} else {
System.out.print("0");
}
}
}
// 输出:1001001001001001001001001001001001001001

现在可以将上面的筛素数法改成使用位操作压缩后的筛素数法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void printPrime2() {
int max = 100;
int[] b = new int[max / 32 + 1];
int[] primes = new int[max / 3 + 1];
int index = 0;
for (int i = 2; i < max; i++) {
int x = b[i / 32] >> (i % 32); // 通过右移,逐位判断是 0 还是 1
if ((x & 1) == 0) {
primes[index++] = i;
for (int j = i; j < max; j += i) {
b[j / 32] |= (1 << (j % 32)); // 将指定位上设置为 1
}
}
}
// 输出 100 以内所有素数
for (int i = 0; i < index; i++) {
System.out.print(primes[i] + " ");
}
}
输出:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

当我们在 Kotlin 中定义泛型时,我们会发现它需要使用到 inout 两个关键字来定义。从形式上来讲,这是一种定义「逆变」和「协变」的方式。

那啥叫逆变?啥叫协变?可以参考下维基百科的定义:协变与逆变

in & out 怎么记?

out(协变)

如果泛型类只将泛型类型作为函数的返回(输出),那么使用 out:

1
2
3
interface Production<out T> {
fun produce(): T
}

可以称之为生产类/接口,因为它主要是用来生产(produce)指定的泛型对象。因此,我们可以简单地这样记忆:

produce = output = out

in(逆变)

如果泛型类只将泛型类型作为函数的入参(输入),那么使用 in:

1
2
3
interface Consumer<in T> {
fun consume(item: T)
}

可以称之为消费者类/接口,因为它主要是用来消费(consume)指定的泛型对象。因此我们可以简单地这样记忆:

consume = input = in

invariant(不变)

如果泛型类既将泛型类型作为函数参数,又将泛型类型作为函数的输出,那么既不用 out 也不用 in:

1
2
3
4
interface ProductionConsumer<T> {
fun produce(): T
fun consume(item: T)
}

为啥要使用 in & out ?

举个例子,我们定义下汉堡类对象,它是一种快餐,也是一种食物。

1
2
3
open class Food
open class FastFood : Food()
class Burger : FastFood()

汉堡生产者

根据上面定义的生产(Production)接口,我们可以进一步扩展它们来生产食物、快餐和汉堡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FoodStore : Production<Food> {
override fun produce(): Food {
println("Produce food")
return Food()
}
}

class FastFoodStore : Production<FastFood> {
override fun produce(): FastFood {
println("Produce fast food")
return FastFood()
}
}

class InOutBurger : Production<Burger> {
override fun produce(): Burger {
println("Produce burger")
return Burger()
}
}

现在,我们可以这样赋值:

1
2
3
val production1 : Production<Food> = FoodStore()
val production2 : Production<Food> = FastFoodStore()
val production3 : Production<Food> = InOutBurger()

显然,汉堡商店属于快餐商店,也属于食物商店。

因此,对于 out 类型,我们能够将使用子类泛型的对象赋值给使用父类泛型的对象。

如果我们修改如下,那么就会出错了,因为食物或快餐商店是可以生产汉堡,但不一定仅仅生产汉堡:

1
2
3
val production1 : Production<Burger> = FoodStore()  // Error
val production2 : Production<Burger> = FastFoodStore() // Error
val production3 : Production<Burger> = InOutBurger()

汉堡消费者

根据上面定义的消费(Consumer)接口,我们可以进一步扩展它们来消费食物、快餐和汉堡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Everybody : Consumer<Food> {
override fun consume(item: Food) {
println("Eat food")
}
}

class ModernPeople : Consumer<FastFood> {
override fun consume(item: FastFood) {
println("Eat fast food")
}
}

class American : Consumer<Burger> {
override fun consume(item: Burger) {
println("Eat burger")
}
}

我们可以将人类、现代人、美国人指定为汉堡消费者,所以可以这样赋值:

1
2
3
val consumer1 : Consumer<Burger> = Everybody()
val consumer2 : Consumer<Burger> = ModernPeople()
val consumer3 : Consumer<Burger> = American()

不难理解,汉堡的消费者可以是美国人,也可以是现代人,更可以是人类。

因此,对于 in 泛型,我们能够将使用父类泛型的对象赋值给使用子类泛型的对象。

反之,如果我们修改如下,就会出现错误,因为汉堡的消费者不仅仅是美国人或现代人。

1
2
3
val consumer1 : Consumer<Food> = Everybody()
val consumer2 : Consumer<Food> = ModernPeople() // Error
val consumer3 : Consumer<Food> = American() // Error

记住 in & out 的另一种方式

  • 父类泛型对象可以赋值给子类泛型对象,用 in;
  • 子类泛型对象可以赋值给父类泛型对象,用 out。

参考资料:

In and out type variant of Kotlin

Kotlin 泛型中的 in 和 out

介绍

Pair 的字面意思是“一对”、“一双”,瞄一眼它的源码,果不其然,里面只有两个字段 firstsecond .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Pair<F, S> {
public final F first;
public final S second;

public Pair(F first, S second) {
this.first = first;
this.second = second;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Pair)) {
return false;
}
Pair<?, ?> p = (Pair<?, ?>) o;
return Objects.equals(p.first, first) && Objects.equals(p.second, second);
}

// ...

public static <A, B> Pair <A, B> create(A a, B b) {
return new Pair<A, B>(a, b);
}
}

用法

它的使用也是非常简单的:

1
2
3
4
5
6
7
8
9
10
11
12
// 两种方式都可以创建 Pair 实例,而第二种方式内部实际上也是使用第一种方式创建
Pair pair1 = new Pair<Integer, String>(1, "111"); // 第一种方式创建
Pair pair2 = Pair.create(1, 111); // 第二种方式创建
Pair pair3 = Pair.create(1, 111);

Log.e(TAG, pair1.first.toString()); // 1
Log.e(TAG, pair1.second.toString()); // 111
Log.e(TAG, pair1.second.equals("111") + ""); // true
Log.e(TAG, pair1.second.equals(111) + ""); // false

Log.e(TAG, pair1.equals(pair2) + ""); // false
Log.e(TAG, pair2.equals(pair3) + ""); // true

从以上示例可知:

  • Pair 的 first 获取的是第一个位置的数据,second 获取的是第二个位置的数据;
  • Pair 的 equals 比较的是 first 与 second 值是否同时 equals .

说到 equals , 上面的源码只是 android.util 包下 Pair 类的 equals 方法,由于 android.support.v4.util 包下也有 Pair 类,通过比较,两个包下的 Pair 类只有 equals 方法有所不同,其它方法无异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// android.util 包下
public boolean equals(Object o) {
if (!(o instanceof Pair)) {
return false;
}
Pair<?, ?> p = (Pair<?, ?>) o;
return Objects.equals(p.first, first) && Objects.equals(p.second, second);
}

// android.support.v4.util 包下
public boolean equals(Object o) {
if (!(o instanceof Pair)) {
return false;
} else {
Pair<?, ?> p = (Pair)o;
return ObjectsCompat.equals(p.first, this.first) && ObjectsCompat.equals(p.second, this.second);
}
}

ObjectsCompat 类里面的 equals 方法:

1
2
3
4
5
6
7
public static boolean equals(@Nullable Object a, @Nullable Object b) {
if (VERSION.SDK_INT >= 19) {
return Objects.equals(a, b);
} else {
return a == b || a != null && a.equals(b);
}
}

Objects 是 Java7 以后才有的类,而 Android 是从 4.4 开始支持 JDK7 编译的,因此为了兼容 4.4 之前的版本,在 v4 中加入了一个不依赖 JDK7 的 Pair 类。

使用场景

既要以键值对的方式存储数据列表,同时在输出时保持顺序的情况下,我们可以使用 Pair 搭配 ArrayList 实现。

场景一:

假如我们需要生成 n 个按钮,而每个按钮都有 code 值、展示文本内容的 content 值,当我们点击其中一个按钮后就根据 code 值去做指定的事情(如网络请求)。

1
ArrayList<Pair<String,String>> dataList = new ArrayList();

场景二:

记录推送过来的消息,我们可以用 Pair 的 first 记录消息到达的时间戳,second 记录消息体。

1
ArrayList<Pair<Long,Message>> dataList = new ArrayList();

概述

  • LiveData 是一个持有数据的类,它持有的数据是可以被观察者订阅的,当数据被修改时就会通知观察者。观察者可以是 Activity、Fragment、Service 等。
  • LiveData 能够感知观察者的生命周期,只有当观察者处于激活状态(STARTED、RESUMED)才会接收到数据更新的通知,在未激活时会自动解注册观察者,以减少内存泄漏。
  • 使用 LiveData 保存数据时,由于数据和组件是分离的,当组件重建时可以保证数据不会丢失。

优点

  • 确保 UI 界面始终和数据状态保持一致。
  • 没有内存泄漏,观察者绑定到 Lifecycle 对象并在其相关生命周期 destroyed 后自行解除绑定。
  • 不会因为 Activity 停止了而奔溃,如 Activity finish 了,它就不会收到任何 LiveData 事件了。
  • UI 组件只需观察相关数据,不需要停止或恢复观察,LiveData 会自动管理这些操作,因为 LiveData 可以感知生命周期状态的更改。
  • 在生命周期从非激活状态变为激活状态,始终保持最新数据,如后台 Activity 在返回到前台后可以立即收到最新数据。
  • 当配置发生更改(如屏幕旋转)而重建 Activity / Fragment,它会立即收到最新的可用数据。
  • LiveData 很适合用于组件(Activity / Fragment)之间的通信。

使用

添加相关依赖

LiveData 有两种使用方式,结合 ViewModel 使用以及直接继承 LiveData 类。

结合 ViewModel 使用

以下代码场景:点击按钮提示一个名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyViewModel : ViewModel() {

// 创建一个 String 类型的 LiveData
// MutableLiveData 是抽象类 LiveData 的子类,我们一般使用的是 MutableLiveData
private lateinit var name: MutableLiveData<String>

fun getName(): MutableLiveData<String> {
if (!::name.isInitialized) {
name = MutableLiveData()
}
return name
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LiveDataActivity : AppCompatActivity() {

private lateinit var myViewModel: MyViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_data)

// 创建并注册观察者
myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
myViewModel.getName().observe(this, Observer {
// LiveData 数据更新回调,it 代表被观察对象的数据,此处为 name
Toast.makeText(baseContext, it, Toast.LENGTH_SHORT).show()
})

btnSetName.setOnClickListener {
// 使用 setValue 的方式更新 LiveData 数据
myViewModel.getName().value = "张三"
}
}
}

让数据(name)和组件(LiveDataActivity)分离,当 Activity 重建时,数据(name)不会丢失。

直接继承 LiveData 类

以下代码场景:在 Activity 中监听 Wifi 信号强度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class WifiLiveData private constructor(context: Context) : LiveData<Int>() {

private var mContext: WeakReference<Context> = WeakReference(context)

companion object {

private var instance: WifiLiveData? = null

fun getInstance(context: Context): WifiLiveData {
if (instance == null) {
instance = WifiLiveData(context)
}
return instance!!
}
}

override fun onActive() {
super.onActive()
registerReceiver()
}

override fun onInactive() {
super.onInactive()
unregisterReceiver()
}

/**
* 注册广播,监听 Wifi 信号强度
*/
private fun registerReceiver() {
val intentFilter = IntentFilter()
intentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION)
mContext.get()!!.registerReceiver(mReceiver, intentFilter)
}

/**
* 注销广播
*/
private fun unregisterReceiver() {
mContext.get()!!.unregisterReceiver(mReceiver)
}

private val mReceiver = object : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent) {
when (intent.action) {
WifiManager.RSSI_CHANGED_ACTION -> getWifiLevel()
}
}
}

private fun getWifiLevel() {
val wifiManager = mContext.get()!!.applicationContext.getSystemService(android.content.Context.WIFI_SERVICE) as WifiManager
val wifiInfo = wifiManager.connectionInfo
val level = wifiInfo.rssi

instance!!.value = level // 发送 Wifi 的信号强度给观察者
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LiveDataActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_data)

withExtendsLiveDataTest()
}

/**
* 直接继承 LiveData 类
*/
private fun withExtendsLiveDataTest() {
WifiLiveData.getInstance(this).observe(this, Observer {
Log.e("LiveDataActivity", it.toString()) // 观察者收到数据更新的通知,打印 Wifi 信号强度
})
}
}

当组件(Activity)处于激活状态(onActive)时注册广播,处于非激活状态(onInactive)时注销广播。

源码解析

observe 注册流程

LiveData 通过 observe() 方法将被观察者 LifecycleOwner (Activity / Fragment) 和观察者 Observer 关联起来。

1
LiveData.observe(LifecycleOwner owner , Observer<T> observer)

进入 LiveData 的 observe() 方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// 若 LifecycleOwner 处于 DESTROYED 状态,则返回
return;
}

// LifecycleBoundObserver 把 LifecycleOwner 对象和 Observer 对象包装在一起
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);

// mObservers(类似 Map 的容器)的 putIfAbsent() 方法用于判断容器中的 observer(key)
// 是否已有 wrapper(value)与之关联
// 若已关联则直接返回关联值,否则关联后再返回 wrapper
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);

if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}

// 由于 LifecycleBoundObserver 实现了 GenericLifecycleObserver 接口,而 GenericLifecycleObserver 又
// 继承了 LifecycleObserver,所以 LifecycleBoundObserver 本质是一个 LifecycleObserver
// 此处属于注册过程, Lifecycle 添加观察者 LifecycleObserver
owner.getLifecycle().addObserver(wrapper);
}

从上面的代码可知,observe() 方法最终是会调用:

1
LifecycleOwner.getLifecycle().addObserver(LifecycleObserver)

因此 LiveData 是能够感知观察者的生命周期变化的。

感知生命周期变化

通过以上的分析,我们知道 LifecycleBoundObserver(LiveData 的内部类)是观察者,以下具体分析 LifecycleBoundObserver 的实现过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
@NonNull final LifecycleOwner mOwner;

LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<T> observer) {
super(observer); // 保存 Observer
mOwner = owner; // 保存 LifecycleOwner
}

@Override
boolean shouldBeActive() {
// 判断是否处于激活状态
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}


@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
// 若 Lifecycle 处于 DESTROYED 状态,则移除 Observer 对象
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
// 移除观察者,在这个方法中会移除生命周期监听并且回调 activeStateChanged() 方法
removeObserver(mObserver);
return;
}
// 若处于激活状态,则调用 activeStateChanged() 方法
activeStateChanged(shouldBeActive());
}

@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}

@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}

当组件(Activity / Fragment)的生命周期发生改变时,onStateChanged() 方法将会被调用。若当前处于 DESTROYED 状态,则会移除观察者;若当前处于激活状态,则会调用 activeStateChanged() 方法。activeStateChanged() 方法位于父类 ObserverWrapper 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void activeStateChanged(boolean newActive) {
// 若新旧状态一致,则返回
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) { // 激活状态的 observer 个数从 0 到 1
onActive(); // 空实现,一般让子类去重写
}
if (LiveData.this.mActiveCount == 0 && !mActive) { // 激活状态的 observer 个数从 1 到 0
onInactive(); // 空实现,一般让子类去重写
}
if (mActive) { // 激活状态,向观察者发送 LiveData 的值
dispatchingValue(this);
}
}

再看看最终调用的 dispatchingValue() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void dispatchingValue(@Nullable ObserverWrapper initiator) {
// ...
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
// 循环遍历 mObservers 这个 map , 向每一个观察者都发送新的数据
for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
// ...
}

可以看到 dispatchingValue() 方法里面再通过 considerNotify() 方法将消息通知下去。

1
2
3
4
private void considerNotify(ObserverWrapper observer) {
// ...
observer.mObserver.onChanged((T) mData);
}

上面的 mObserver 正是我们调用 observe() 方法时传入的观察者。

总结上面的分析就是:调用 LiveData.observe(LifecycleOwner owner , Observer observer) 进行注册后,当 LiveData 数据发生变化后,最终就会调用 Observer 对象的 onChanged() 方法,并把变化的数据作为参数回传。

通知观察者更新数据的方式

LiveData 为我们提供了两种改变数据后,通知观察者更新数据的方式,一个是 setValue() 方法(必须在主线程调用),另一个是 postValue() 方法(必须在子线程调用)。

setValue() 方法

1
2
3
4
5
6
7
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}

dispatchingValue() 方法会跑我们上面分析的流程,最终把改变的数据 value(对应上面的 mData)作为 onChanged() 方法的参数传给观察者。

postValue() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET;
}
//noinspection unchecked
setValue((T) newValue);
}
};

可以看出 postValue() 方法最终也会在主线程中调用 setValue() 方法。

文中 Demo GitHub 地址

参考资料:

  • Android开发——架构组件LiveData源码解析
  • Android Developers

前言

在日常的开发中,我们通常需要在 Activity / Fragment 的生命周期方法中进行一些繁重的操作,这样使代码看起来十分臃肿。Lifecycle 的引入主要是用来管理和响应 Activity / Fragment 的生命周期的变化,帮助我们编写出更易于组织且通常更加轻量级的代码,让代码变得更易于维护。

Lifecycle 是一个类,它持有 Activity / Fragment 生命周期状态的信息,并允许其它对象观察此状态。

Lifecycle 使用

添加相关依赖

场景:让 MVP 中的 Presenter 观察 Activity 的 onCreate 和 onDestroy 状态。

  • Presenter 继承 LifecycleObserver 接口
1
2
3
4
5
6
7
8
9
10
11
interface IPresenter : LifecycleObserver {

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(owner: LifecycleOwner)

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(owner: LifecycleOwner)

@OnLifecycleEvent(Lifecycle.Event.ON_ANY) // ON_ANY 注解能观察到其它所有的生命周期方法
fun onLifecycleChanged(owner: LifecycleOwner, event: Lifecycle.Event)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyPresenter : IPresenter {

override fun onCreate(owner: LifecycleOwner) {
Log.e(javaClass.simpleName, "onCreate")
}

override fun onDestroy(owner: LifecycleOwner) {
Log.e(javaClass.simpleName, "onDestroy")
}

override fun onLifecycleChanged(owner: LifecycleOwner, event: Lifecycle.Event) {
// Log.e(javaClass.simpleName, "onLifecycleChanged")
}
}
  • 在 Activity 中添加 LifecycleObserver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyLifecycleActivity : AppCompatActivity() {

private lateinit var myPresenter: MyPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_lifecycle)

Log.e(javaClass.simpleName, "onCreate")

myPresenter = MyPresenter()
lifecycle.addObserver(myPresenter) // 添加 LifecycleObserver
}

override fun onDestroy() {
Log.e(javaClass.simpleName, "onDestroy")
super.onDestroy()
}
}

启动 Activity 会打印:

1
2
MyLifecycleActivity: onCreate
MyPresenter: onCreate

finish Activity 会打印:

1
2
MyPresenter: onDestroy
MyLifecycleActivity: onDestroy

以上 Presenter 对象只观察了 Activity 的 onCreate 方法和 onDestroy 方法,我们还可以观察其它的生命周期方法。在 Lifecycle 内部有个枚举类 Event , 它包含了 LifecycleObserver 能够观察到的所有生命周期方法,只需要添加上相应的注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
enum class Event {
/**
* Constant for onCreate event of the [LifecycleOwner].
*/
ON_CREATE,
/**
* Constant for onStart event of the [LifecycleOwner].
*/
ON_START,
/**
* Constant for onResume event of the [LifecycleOwner].
*/
ON_RESUME,
/**
* Constant for onPause event of the [LifecycleOwner].
*/
ON_PAUSE,
/**
* Constant for onStop event of the [LifecycleOwner].
*/
ON_STOP,
/**
* Constant for onDestroy event of the [LifecycleOwner].
*/
ON_DESTROY,
/**
* An [Event] constant that can be used to match all events.
*/
ON_ANY
}

Lifecycle 内部还有代表了各个生命周期所处状态的枚举类 State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
enum class State {

/**
* Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
* any more events. For instance, for an [android.app.Activity], this state is reached
* before Activity's [onDestroy] call.
*/
DESTROYED,

/**
* Initialized state for a LifecycleOwner. For an [android.app.Activity], this is
* the state when it is constructed but has not received
* [onCreate] yet.
*/
INITIALIZED,

/**
* Created state for a LifecycleOwner. For an [android.app.Activity], this state
* is reached in two cases:
*
* after [onCreate] call;
* before [onStop] call.
*/
CREATED,

/**
* Started state for a LifecycleOwner. For an [android.app.Activity], this state
* is reached in two cases:
*
* after [onStart] call;
* before [onPause] call.
*/
STARTED,

/**
* Resumed state for a LifecycleOwner. For an [android.app.Activity], this state
* is reached after [onResume] is called.
*/
RESUMED;

/**
* Compares if this State is greater or equal to the given `state`.
*
* @param state State to compare with
* @return true if this State is greater or equal to the given `state`
*/
fun isAtLeast(state: State): Boolean {
return compareTo(state) >= 0
}
}

在一般开发中,当 Activity 拥有多个 Presenter 并需要在各个生命周期做一些特殊逻辑时,代码可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun onStop() {
presenter1.onStop()
presenter2.onStop()
presenter3.onStop()
super.onStop()
}

override fun onDestroy() {
presenter1.onDestroy()
presenter2.onDestroy()
presenter3.onDestroy()
super.onDestroy()
}

这样会使 Activity 的代码变得很臃肿。

如果用 Lifecycle , 只需将持有 Lifecycle 对象的 Activity 的生命周期的响应分发到各个 LifecycleObserver 观察者中即可。

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_lifecycle)

lifecycle.addObserver(presenter1) // 添加 LifecycleObserver
lifecycle.addObserver(presenter2) // 添加 LifecycleObserver
lifecycle.addObserver(presenter3) // 添加 LifecycleObserver
}

基本原理

几个概念

  • LifecycleObserver 接口

    Lifecycle 观察者。实现了该接口的类,被 LifecycleOwner 类的 addObserver 方法注册后,通过注解的方式即可观察到 LifecycleOwner 的生命周期方法。

  • LifecycleOwner 接口

    Lifecycle 持有者。实现了该接口的类持有生命周期(Lifecycle 对象),该接口生命周期(Lifecycle 对象)的改变会被其注册的观察者 LifecycleObserver 观察到并触发其对应的事件。

  • Lifecycle 类

    生命周期。和 LifecycleOwner 不同,LifecycleOwner 通过 getLifecycle() 方法获取到内部的 Lifecycle 对象。

  • State

    当前生命周期所处状态。Lifecycle 将 Activity 的生命周期函数对应成 State .

  • Event

    当前生命周期改变对应的事件。State 变化将触发 Event 事件,从而被已注册的 LifecycleObserver 接收。

实现原理

LifecycleOwner

AppCompatActivity 的父类 SupportActivityFragment 一样,实现了 LifecycleOwner 接口,因此它们都拥有 Lifecycle 对象。

1
2
3
4
5
6
7
8
9
10
11
12
public class SupportActivity extends Activity implements LifecycleOwner, Component {

// ...

private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

public Lifecycle getLifecycle() {
return this.mLifecycleRegistry;
}

// ...
}
1
2
3
4
5
6
7
8
9
public interface LifecycleOwner {
/**
* Returns the Lifecycle of the provider.
*
* @return The lifecycle of the provider.
*/
@NonNull
Lifecycle getLifecycle();
}

从源码可知 getLifecycle() 方法返回的是 LifecycleRegistry 对象,而 LifecycleRegistry 是 Lifecycle 的子类,所有对 LifecycleObserver 的操作都是由 LifecycleRegistry 完成的。

LifecycleRegistry

生命周期登记处。作为 Lifecycle 的子类,它的作用是添加观察者、响应生命周期事件和分发生命周期事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class LifecycleRegistry extends Lifecycle {

// LifecycleObserver Map , 每一个 Observer 都有一个 State
private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
new FastSafeIterableMap<>();

// 当前的状态
private State mState;

// Lifecycle 持有者,如继承了 LifecycleOwner 的 SupportActivity
private final WeakReference<LifecycleOwner> mLifecycleOwner;

public LifecycleRegistry(@NonNull LifecycleOwner provider) {
mLifecycleOwner = new WeakReference<>(provider);
mState = INITIALIZED;
}

/**
* 添加 LifecycleObserver 观察者,并将之前的状态分发给这个 Observer , 例如我们在 onResume 之后注册这个 Observer ,
* 该 Observer 依然能收到 ON_CREATE 事件
*/
@Override
public void addObserver(@NonNull LifecycleObserver observer) {
// ...
// 例如:Observer 初始状态是 INITIALIZED , 当前状态是 RESUMED , 需要将 INITIALIZED 到 RESUMED 之间的
// 所有事件分发给 Observer
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}
// ...
}

/**
* 处理生命周期事件
*/
public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
State next = getStateAfter(event);
moveToState(next);
}

/**
* 改变状态
*/
private void moveToState(State next) {
if (mState == next) {
return;
}
mState = next;
// ...
sync();
// ...
}

/**
* 同步 Observer 状态,并分发事件
*/
private void sync() {
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
Log.w(LOG_TAG, "LifecycleOwner is garbage collected, you shouldn't try dispatch "
+ "new events from it.");
return;
}
while (!isSynced()) {
mNewEventOccurred = false;
// State 中,状态值是从 DESTROYED - INITIALIZED - CREATED - STARTED - RESUMED 增大
// 如果当前状态值 < Observer 状态值,需要通知 Observer 减小状态值,直到等于当前状态值
if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
backwardPass(lifecycleOwner);
}
Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
// 如果当前状态值 > Observer 状态值,需要通知 Observer 增大状态值,直到等于当前状态值
if (!mNewEventOccurred && newest != null
&& mState.compareTo(newest.getValue().mState) > 0) {
forwardPass(lifecycleOwner);
}
}
mNewEventOccurred = false;
}

/**
* 向前传递事件。
* 增加 Observer 的状态值,直到状态值等于当前状态值
*/
private void forwardPass(LifecycleOwner lifecycleOwner) {
Iterator<Entry<LifecycleObserver, ObserverWithState>> ascendingIterator =
mObserverMap.iteratorWithAdditions();
while (ascendingIterator.hasNext() && !mNewEventOccurred) {
Entry<LifecycleObserver, ObserverWithState> entry = ascendingIterator.next();
ObserverWithState observer = entry.getValue();
while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred
&& mObserverMap.contains(entry.getKey()))) {
pushParentState(observer.mState);
// 分发状态改变事件
observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState));
popParentState();
}
}
}

/**
* 向后传递事件。
* 减小 Observer 的状态值,直到状态值等于当前状态值
*/
private void backwardPass(LifecycleOwner lifecycleOwner) {
Iterator<Entry<LifecycleObserver, ObserverWithState>> descendingIterator =
mObserverMap.descendingIterator();
while (descendingIterator.hasNext() && !mNewEventOccurred) {
Entry<LifecycleObserver, ObserverWithState> entry = descendingIterator.next();
ObserverWithState observer = entry.getValue();
while ((observer.mState.compareTo(mState) > 0 && !mNewEventOccurred
&& mObserverMap.contains(entry.getKey()))) {
Event event = downEvent(observer.mState);
pushParentState(getStateAfter(event));
observer.dispatchEvent(lifecycleOwner, event);
popParentState();
}
}
}
}

根据上面的分析,我们知道 LifecycleRegistry 才是真正替 Lifecycle 去埋头干粗活的类!

接下来继续来看看实现了 LifecycleOwner 接口的 SupportActivity 类是如何将事件分发给 LifecycleRegistry 的。

1
2
3
4
5
6
7
public class SupportActivity extends Activity implements LifecycleOwner, Component {

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ReportFragment.injectIfNeededIn(this);
}
}

注意到 SupportActivity 的 onCreate() 方法里面有行 ReportFragment.injectIfNeededIn(this) 代码,再进入 ReportFragment 类分析。

ReportFragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class ReportFragment extends Fragment {

public static void injectIfNeededIn(Activity activity) {
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
manager.executePendingTransactions();
}
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
dispatchCreate(mProcessListener);
dispatch(Lifecycle.Event.ON_CREATE);
}

@Override
public void onStart() {
super.onStart();
dispatchStart(mProcessListener);
dispatch(Lifecycle.Event.ON_START);
}

@Override
public void onResume() {
super.onResume();
dispatchResume(mProcessListener);
dispatch(Lifecycle.Event.ON_RESUME);
}

@Override
public void onPause() {
super.onPause();
dispatch(Lifecycle.Event.ON_PAUSE);
}

@Override
public void onStop() {
super.onStop();
dispatch(Lifecycle.Event.ON_STOP);
}

@Override
public void onDestroy() {
super.onDestroy();
dispatch(Lifecycle.Event.ON_DESTROY);
// just want to be sure that we won't leak reference to an activity
mProcessListener = null;
}

/**
* 分发事件
*/
private void dispatch(Lifecycle.Event event) {
Activity activity = getActivity();
if (activity instanceof LifecycleRegistryOwner) {
((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
return;
}

if (activity instanceof LifecycleOwner) {
Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
}
}
}
}

不难看出这是一个没有 UI 的后台 Fragment , 一般可以为 Activity 提供一些后台行为。在 ReportFragment 的各个生命周期中都调用了 LifecycleRegistry.handleLifecycleEvent() 方法来分发生命周期事件。

为什么不直接在 SupportActivity 的生命周期函数中给 Lifecycle 分发生命周期事件,而是要加一个 Fragment 呢?

在 ReportFragment 的 injectIfNeededIn() 方法中找到答案:

1
2
3
4
5
6
7
8
9
10
public static void injectIfNeededIn(Activity activity) {
// ProcessLifecycleOwner should always correctly work and some activities may not extend
// FragmentActivity from support lib, so we use framework fragments for activities
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
// Hopefully, we are the first to make a transaction.
manager.executePendingTransactions();
}
}

有两个原因:为了能让 ProcessLifecycleOwner 正确地工作;②、并非所有的 Activity 都是继承来自 support 包的 FragmentActivity 类的。因此封装一个同样具有生命周期的后台 Fragment 来给 Lifecycle 分发生命周期事件。

另一方面,假如我们不继承自 SupportActivity , 那 Lifecycle 是如何通过 ReportFragment 分发生命周期事件呢?

鼠标停在 ReportFragment 类,同时按下 Ctrl + Shift + Alt + F7 在 Project and Libraries 的范围下搜索 ReportFragment 被引用的地方。我们发现还有 LifecycleDispatcher 和 ProcessLifecycleOwner 两个类有使用到 ReportFragment .

LifecycleDispatcher

生命周期分发者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class LifecycleDispatcher {

// ...

static void init(Context context) {
if (sInitialized.getAndSet(true)) {
return;
}
((Application) context.getApplicationContext())
.registerActivityLifecycleCallbacks(new DispatcherActivityCallback());
}

// 通过注册 Application.registerActivityLifecycleCallbacks 来获取 Activity 的生命周期回调
static class DispatcherActivityCallback extends EmptyActivityLifecycleCallbacks {
private final FragmentCallback mFragmentCallback;

DispatcherActivityCallback() {
mFragmentCallback = new FragmentCallback();
}

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (activity instanceof FragmentActivity) {
((FragmentActivity) activity).getSupportFragmentManager()
.registerFragmentLifecycleCallbacks(mFragmentCallback, true);
}
// 给每个 Activity 添加 ReportFragment
ReportFragment.injectIfNeededIn(activity);
}

@Override
public void onActivityStopped(Activity activity) {
if (activity instanceof FragmentActivity) {
markState((FragmentActivity) activity, CREATED);
}
}

@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
if (activity instanceof FragmentActivity) {
markState((FragmentActivity) activity, CREATED);
}
}
}

/**
* 通过递归形式给所有子 Fragment 设置 State
*/
private static void markState(FragmentManager manager, State state) {
Collection<Fragment> fragments = manager.getFragments();
if (fragments == null) {
return;
}
for (Fragment fragment : fragments) {
if (fragment == null) {
continue;
}
markStateIn(fragment, state);
if (fragment.isAdded()) {
// 递归
markState(fragment.getChildFragmentManager(), state);
}
}
}

private static void markStateIn(Object object, State state) {
if (object instanceof LifecycleRegistryOwner) {
LifecycleRegistry registry = ((LifecycleRegistryOwner) object).getLifecycle();
registry.markState(state);
}
}

/**
* 将某 Activity 及其所有子 Fragment 的 State 设置为某状态
*/
private static void markState(FragmentActivity activity, State state) {
markStateIn(activity, state);
markState(activity.getSupportFragmentManager(), state);
}

// ...
}

从源码可知,LifecycleDispatcher 是通过注册 Application.registerActivityLifecycleCallbacks 来监听 Activity 的生命周期回调的。

  • 在 onActivityCreated 中添加 ReportFragment , 将 Activity 的生命周期交给 ReportFragment 去分发给 LifecycleRegistry ;
  • 在 onActivityStopped() 以及 onActivitySaveInstanceState() 中,将 Activity 及其所有子 Fragment 的 State 置为 CREATED .

ProcessLifecycleOwner

为整个 App 进程提供生命周期的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ProcessLifecycleOwner implements LifecycleOwner {

static final long TIMEOUT_MS = 700; //mls

// ...

static void init(Context context) {
sInstance.attach(context);
}

private ActivityInitializationListener mInitializationListener =
new ActivityInitializationListener() {
@Override
public void onCreate() {
}

@Override
public void onStart() {
activityStarted();
}

@Override
public void onResume() {
activityResumed();
}
};

void activityStarted() {
mStartedCounter++;
if (mStartedCounter == 1 && mStopSent) {
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
mStopSent = false;
}
}

void activityResumed() {
mResumedCounter++;
if (mResumedCounter == 1) {
if (mPauseSent) {
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
mPauseSent = false;
} else {
mHandler.removeCallbacks(mDelayedPauseRunnable);
}
}
}

void activityPaused() {
mResumedCounter--;
if (mResumedCounter == 0) {
mHandler.postDelayed(mDelayedPauseRunnable, TIMEOUT_MS);
}
}

void activityStopped() {
mStartedCounter--;
dispatchStopIfNeeded();
}

void attach(Context context) {
mHandler = new Handler();
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
Application app = (Application) context.getApplicationContext();
app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
ReportFragment.get(activity).setProcessListener(mInitializationListener);
}

@Override
public void onActivityPaused(Activity activity) {
activityPaused();
}

@Override
public void onActivityStopped(Activity activity) {
activityStopped();
}
});
}
}

从源码可知:

  • ProcessLifecycleOwner 是用来监听 Application 生命周期的,它只会分发一次 ON_CREATE 事件,并不会分发 ON_DESTROY 事件;
  • ProcessLifecycleOwner 在 Activity 的 onResume 中调用 Handle.postDelayed() , 在 onPause 中调用了 mHandler.removeCallbacks(mDelayedPauseRunnable) , 是为了处理 Activity 重建时比如横竖屏幕切换时,不会发送事件;
  • ProcessLifecycleOwner 一般用来判断应用是在前台还是后台,但由于使用了 Handle.postDelayed() , TIMEOUT_MS = 700,因此这个判断不是即时的,有 700ms 的延迟;
  • ProcessLifecycleOwner 与 LifecycleDispatcher 一样,都是通过注册 Application.registerActivityLifecycleCallbacks 来监听 Activity 的生命周期回调,来给每个 Activity 添加 ReportFragment 的。

最后,通过点击 init() 方法,我们发现 LifecycleDispatcher 和 ProcessLifecycleOwner 都是在 ProcessLifecycleOwnerInitializer 类下完成初始化的,而 ProcessLifecycleOwnerInitializer 是一个 ContentProvider .

1
2
3
4
5
6
7
8
9
10
11
public class ProcessLifecycleOwnerInitializer extends ContentProvider {

@Override
public boolean onCreate() {
LifecycleDispatcher.init(getContext());
ProcessLifecycleOwner.init(getContext());
return true;
}

// ...
}

Lifecycle 会自动在我们的 AndroidManifest.xml 中添加以下代码用于初始化 ProcessLifecycleOwner 与 LifecycleDispatcher , 这样就不需要我们在 Application 中写代码来初始化了。

1
2
3
4
5
6
7
8
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
// ...
<provider
android:name="android.arch.lifecycle.ProcessLifecycleOwnerInitializer"
android:authorities="me.baron.achitecturelearning.lifecycle-trojan"
android:exported="false"
android:multiprocess="true" />
</manifest>

Lifecycle 的最佳实践

  • 保持 Activity / Fragment 尽可能的精简,它们不应该试图去获取它们所需的数据,要用 ViewModel 来获取,并观察 LiveData 对象将数据变化反映到视图中;
  • 尝试编写数据驱动(data-driven)的 UI , 即 UI 控制器的责任是在数据改变时更新视图或者将用户的操作通知给 ViewModel ;
  • 将数据逻辑放到 ViewModel 类中,ViewModel 应该作为 UI 控制器和应用程序其它部分的连接服务。注意:不是由 ViewModel 负责获取数据(例如:从网络获取)。相反,ViewModel 调用相应的组件获取数据,然后将数据获取结果提供给 UI 控制器;
  • 使用 Data Binding 来保持视图和 UI 控制器之间的接口干净。这样可以让视图更具声明性,并且尽可能减少在 Activity 和 Fragment 中编写更新代码。如果你喜欢在 Java 中执行该操作,请使用像 Butter Knife 这样的库来避免使用样板代码并进行更好的抽象化;
  • 如果 UI 很复杂,可以考虑创建一个 Presenter 类来处理 UI 的修改。虽然通常这样做不是必要的,但可能会让 UI 更容易测试;
  • 不要在 ViewModel 中引用 View 或者 Activity 的 context . 因为如果 ViewModel 存活的比 Activity 时间长(在配置更改的情况下),Activity 将会被泄漏并且无法被正确的回收。

文中 Demo GitHub 地址

参考资料:

  • Android-Lifecycle超能解析-生命周期的那些事儿
  • Android官方架构组件:Lifecycle详解&原理分析
  • Android Developers
0%