白筱汐

想都是问题,做都是答案

0%

flutter学习指南

初识 dart

ps: Flutter开发环境搭建的步骤可以查找我之前的文章。

学习flutter之前我们必须先学会dart,可以通过查看dart官网快速了解这门语言。如果你之前使用过typescript或者java语言,那么你将很快就能上手这门语言。

以下举例一些 dart 语言的重要特征:

异步

Future 类似于 javascript 中的 Promise, async await 语法则几乎于 javascript 中一致。

1
2
3
4
Future<void> printWithDelay(String message) async {
await Future.delayed(Duration(seconds: 1)); // 等待1s
print(message);
}

等同于

1
2
3
4
5
Future<void> printWithDelay(String message) async {
Future.delayed(Duration(seconds: 1)).then((_){
print(message);
})
}

异常处理

通常我们可以使用 async 和 await 来控制异步流程,使用 try 语句配合 on 或 catch (两者也可同时使用)关键字来捕获一个异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
try {
// 可能会抛出异常的代码
int result = 10 ~/ 0; // ~/ 除法,结果返回整数,故意除以0,会抛出除零异常
print("Result: $result");
} catch (e) {
// 捕获并处理异常
print("Exception caught: $e");
} finally {
// 无论是否发生异常,都会执行该块中的代码
print("Finally block executed");
}
}

空安全(null-safety)以及一些语法糖

变量声明的时候默认是非空的,如果你想让变量可以为 null,只需要在类型声明后加上 ?。

1
2
int x = 1; // 默认不为空,必须在声明的时候初始化
int? a = null

如果不知道可空类型的表达式是否等于 null,可以使用条件属性访问,操作符 ?.。

1
fn?.action(); // 如果 fn 为 null,则返回 null,否则调用 action()

避空运算符,??= 赋值运算符,仅当该变量为空值时才为其赋值

1
2
3
int? a; 
a ??= 3;
print(a); // 3

级联,要对同一对象执行一系列操作,请使用级联(..)。

1
myObject..someMethod()

虽然它仍然在 myObject 上调用了 someMethod,但表达式的结果却不是该方法返回值,而是是 myObject 对象的引用!

1
2
3
4
5
querySelector('#confirm')
?..text = 'Confirm'
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();

在第一个级联位置,使用 空判断 级联操作符 (?..),它可以确保级联操作均在实例不为 null 时执行

非空断言,(non-null assertion)是一种用于确保一个表达式或变量不为null的机制。非空断言运算符是一个后缀感叹号 !,用于标记一个表达式或变量为非空

1
2
int? number = 5;
print(number!); // 使用非空断言,告诉编译器number不会为null

请注意,使用非空断言运算符时需要谨慎,因为如果实际上该表达式或变量为null,会导致运行时抛出NullPointerException。因此,在使用非空断言时,请确保你确定该表达式或变量不会为空,以避免潜在的运行时错误。

Stream 数据流

Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。

1、可以使用异步 for 循环 await for ,来替代 Stream API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Future<void> readFileAwaitFor() async {
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();
// 使用 transform() 方法生成具有不同类型数据的流
// 将转换后的数据变换成一个 LineSplitter 执行
var lines = inputStream.transform(utf8.decoder).transform(const LineSplitter());
try {
await for (final line in lines) {
print('Got ${line.length} characters from stream');
}
print('file is now closed');
} catch (e) {
print(e);
}
}

异步 for 循环,通过 try-catch 来处理错误。代码位于异步 for 循环之后,会在 stream 被关闭后执行。

2、使用 listen 方法来订阅一个Stream并监听其中的事件,使用 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
import 'dart:async';

void main() {
Stream<int> stream = countStream(5);

stream.listen(
(event) {
print('Received event: $event');
},
onError: (error) {
print('Error occurred: $error');
},
);
}

Stream<int> countStream(int count) async* {
for (int i = 1; i <= count; i++) {
if (i == 3) {
throw Exception('An error occurred');
}
yield i;
await Future.delayed(Duration(seconds: 1));
}
}

初识 widget

总结起来就一句话,Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行事件处理的,Flutter 中万物皆为Widget。

写构建 Flutter 应用的时候,我们经常会看到 StatelessWidget 和 StatefulWidget。

StatelessWidget

StatelessWidget 一般用于不需要维护状态的场景(类比 React 中无状态组件的概念)。它通常在build方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget。build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。

1
2
3
4
5
6
7
8
class MyApp extends StatelessWidget {
const MyAppBar({super.key});

@override
Widget build(BuildContext context) {
return Text('文字');
}
}

StatefulWidget

StatefulWidget 会对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态。在 Widget 构建的时候可以读取到 state 里面的信息,当我们调用 setState 方法时可以更新 state,并且通知 Flutter 重新调用 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
43
44
45
46
47
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

这段代码是使用 flutter create project 命令生成的官方基础代码。State 中有两个常用属性:

  1. context,功能前文以后说过了,同 StatelessWidget 中的 BuildContext。
  2. widget,它表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。我们可以看到示例代码中的用法 “widget.title”, 在 state 类里面,我们访问了与它对应的 widget 的属性。

State 生命周期

对应web开发中组件的生命周期,深刻理解这些函数的调用时机对于我们优化代码至关重要。

  • initState():当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。

  • didChangeDependencies():当State对象的依赖发生变化时会被调用。

  • build():主要是用于构建 widget 子树。

    1. 在调用initState()之后。
    2. 在调用didUpdateWidget()之后。
    3. 在调用setState()之后。
    4. 在调用didChangeDependencies()之后。
    5. 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
  • reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。

  • didUpdateWidget ():在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调。正如之前所述,widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()就会被调用。

  • deactivate():当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。

  • dispose():当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

通常我们会在 initState 中初始化状态,调用 setState 改变状态并更新UI,在 dispose 中我们销毁一些控制器等来释放资源,这些内容在后面我们会经常看到。