一文带你全盘掌握Dart基本语法
变量声明
关键字 var
通过 var 声明一个变量,如果这个变量在声明时赋值了,此时类型就已经确定了,再次赋值时不能更改其他类型。
var str = "hello dart";
// 因为声明时已经赋值为String,所以不能更改类型
str = 3; // 编译器会报错
str = "nihao"; //再次赋值只能为String类型
因为 Dart 本身是一个强类型语言,任何变量都是有确定类型的,在 Dart 中,当用var声明一个变量后,Dart 在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定。
关键字 dynamic和Object
Object是Dart所有对象的基类,就是说在 Dart 中所有类型都是Object的子类(包括Function和Null),所以任何类型的数据都可以赋值给Object声明的对象。 dynamic与Object声明的变量都可以赋值任意对象,且后期可以改变赋值的类型,这和 var 是不同的,如:
dynamic str = "hi dart!";
str = 4;
Object src = 34.32;
src = "nihao";
这样赋值时没有任何问题的,这时候有人会问,既然用 dynamic 和 object 都可以让变量赋值任何类型,那么这两个有什么区别呢? dynamic 与 Object 不同的是 dynamic 声明的对象编译器会提供所有可能的组合,而 Object 声明的对象只能使用 Object 的属性与方法, 否则编译器会报错,如:
dynamic a = "hi dart!";
Object b = "nihao";
// 正常打印 值为8
print(a.length);
// 报错
print(b.length);
用Object修饰的变量b,不能使用length属性,因为Object对象并没有这个属性,而用dynamic修饰的变量a,赋予了String类型的属性和方法,所以可以使用length属性。
但是这里有个点需要注意: dynamic修饰的变量a,使用a.length
没有报错,而且能正常打印值,是因为 a 被赋值为String 类型,而String类型有length这个属性,但是如果我们把length改成其他String类型没有的属性,比如随便写一个不存在的属性:
print(a.ccccc); // a是字符串,没有"ccccc"属性,编译时不会报错,运行时会报错
这样写在编译期不会报错,但运行时会报错,这一点要注意。
关键字 final和const
final 和const 都是修饰一个不可变的变量,一个final变量只能被设置一次,两者区别在于:const 变量是一个编译时常量(编译时直接替换为常量值),final变量在第一次使用时被初始化。被final或者const修饰的变量,变量类型可以省略,如:
//可以省略String这个类型声明
final str = "hi dart";
//final String str = "hi dart";
const str1 = "hi dart";
//const String str1 = "hi dart";
空安全(null-safety)
Dart 中一切都是对象,这意味着如果我们定义一个数字,在初始化它之前如果我们使用了它,假如没有某种检查机制,则不会报错,比如:
test() {
int i;
print(i*8);
}
在 Dart 引入空安全之前,上面代码在执行前不会报错,但会触发一个运行时错误,原因是 i 的值为 null 。但现在有了空安全,则定义变量时我们可以指定变量是可空还是不可空。
由于空安全机制,将运行时的错误提前到编译期,这样在我们对这个变量操作时,提前发现这个变量是否为空,从而避免运行时崩溃。
int i = 8; // 默认不为空,使用前必须初始化
int? j; // 定义为可空类型,对于可空变量,我们在使用前必须判空。
// 如果我们预期变量不能为空,但在定义时不能确定其初始值,则可以加上late关键字,
// 表示会稍后初始化,但是在正式使用它之前必须得保证初始化过了,否则会报错
late int k;
k = 9;
如果一个变量我们定义为可空类型,在某些情况下即使我们给它赋值过了,但是预处理器仍然有可能识别不出,这时我们就要显式(通过在变量后面加一个“!”符号)告诉预处理器它已经不是null了,比如:
class Test {
int? i;
Function? fun;
say() {
if (i != null) {
print(i! * 8); // 因为已经判过空,所以能走到这 i 必不为null,如果没有显式申明,则 IDE 会报错
}
if (fun != null) {
fun!(); // 同上
}
}
}
上面中如果函数变量可空时,调用的时候可以用语法糖:
fun!.call(); // fun不为空时,调用call
内置类型
num
我们都知道,在Dart中,没有基本数据类型,所有的数据类型都是class,num就是其中一种表示数值的class,我们所使用的int和double都继承自num,在平时使用过程中,只要注意不要超过int的最大字节数就可以了。int的最大字节数和和编译环境有关,32位机器上运行时,int最大是4个字节,64位机器上运行时,int最大是8个字节。
int j = 5;
print(j.bitLength); // 返回存储此整数所需的最小位数。5 -> 101 最小是3位
String
- 单引号声明
声明一个String类型字符串,既可以用双引号(""
),也可以用单引号(''
)。
- 格式化
在java中,我们使用String做字符串拼接时有三种方式:+ 、 StringBuilder 、String.format()。使用最多的基本就是前两种了,但是在dart中,我们拼接两个字符串时可以使用"$
"符号,后面跟上变量名,如果变量名后面还有英文,我们可以使用{}
包含变量名,和英文分割开。
String a = "hello";
String b = 'dart';
print("$a $b"); // hello dart
print("${a}nihao$b"); // hellonihaodart
- 双引号转义
String c = "\"test\""; // "test"
- 三引号(
"""
)
如果进行多行字符串声明时,可以使用"""
。
String desc = """
原生应用程序是指某一个移动平台(比如iOS或安卓)所特有的应用,
使用相应平台支持的开发工具和语言,并直接调用系统提供的SDK API。
比如Android原生应用就是指使用Java或Kotlin语言直接调用Android SDK开发的应用程序;
而iOS原生应用就是指通过Objective-C或Swift语言直接调用iOS SDK开发的应用程序。
""";
List 和 Map
在Dart 中数组等于列表,声明一个集合和map可以使用:
var list = [];
var map = {};
以上声明相当于java中的:List list = new List()
和 Map map = new Map()
list中的元素和map中的key-value类型都是dynamic。因为Dart中没有基本数据类型,所以在list中,可以给元素赋值任意类型,而他们基类都是Object,map同理。
如果想指定list中的元素类型,可以使用:
List<String> list = []
、Map<int, String> map = {}
如果想取集合里面的元素,通过list[0]
来取下标为0的元素。
- 遍历数组 (
for-i
模板和for-each
模板)
iter: for (var o in list) {}
itar: for (var i = 0; i < list.length; ++i) {}
- 遍历map:迭代器的遍历
var keys = map.keys;
for (var element in keys) {
}
操作符
类型判定操作符
- 类型转换
使用as
关键字,后面跟上要转换的类型
num j = 1;
int a = j as int;
- 类型判断
判断一个变量是否属于这个类型,使用is
关键字,相当于java中的instantof
,判断不属于该类型,使用is!
Object i = 2;
if(i is int) {
print("$i 属于 int"); // 2 属于 int
}
Object j = "2";
if(j is! int) {
print("$j 不属于 int"); // 2 不属于 int
}
赋值操作符
前面已经讲过Dart中的空安全机制了,dart中默认变量不为空,如果声明时不赋初值,则使用String? s;
来说明该变量可空。
??
如果左边表达式的值不为null,则直接返回自己的值,否则返回右边表达式的值。
k = null;
var v = k ?? "789";
print(v); // k为null,返回789,如果k不为null,例如:k = 123,则返回123
??=
左边值不为null时,直接返回自己的值,否则返回右边表达式的值。
String? k = "xx";
String? k1 = null;
k ??= "123"; // k 不为null,这句表达式无效
k1 ??= "456";
print(k); // k = xx;
print(k1); // k1 = 456;
级联操作符..
void main() {
Builder()..a()..b();
}
class Builder {
void a() {}
void b() {}
}
安全操作符?.
String? str;
print(str?.length);
函数
Dart是一种真正的面向对象的语言,所以即使是函数也是对象并且有一个类型Function。这意味着函数可以赋值给变量或者作为参数传递给其他函数,这就是函数式编程的典型特征。
函数声明
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理,注意,函数返回值没有类型推断:
对于只包含一个表达式的函数,可以使用简写语法:
bool isTrue() => true;
函数作为变量
var say = (str) {
print(str); // 打印:hi dart!
};
say("hi dart!");
函数作为参数传递
void execute(var callback) {
callback();
}
execute(() => print("xxx"))
上面的例子,就是把一个() => print("xxx")
函数当作参数传递给callback,然后execute里执行callback()函数,也就是执行了传进去的那个函数,打印:"xxx"。
1. 可选的位置参数
包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
下面是一个不带可选参数调用这个函数的例子:
say('Bob', 'Howdy'); //结果是: Bob says Howdy
下面是用第三个参数调用这个函数的例子:
say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal
2. 可选的命名参数
定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。例如:
//设置[bold]和[hidden]标志
void enableFlags({bool bold, bool hidden}) {
// ...
}
调用函数时,可以使用指定命名参数。例如:paramName: value
enableFlags(bold: true, hidden: false);
可选命名参数在Flutter中使用非常多。注意,不能同时使用可选的位置参数和可选的命名参数。
类
类的私有化
使用_
+类名/变量名,表示该类或者类的成员属性是私有的,只能在当前文件访问,外部无法访问,相当于java中的private
class _Point {
int? _x;
int? _y;
String? _name;
}
类的构造方法
- 命名构造方法
在dart中,没有像java中的构造方法重载,但是在我们实际开发的场景中,确实会遇到重载的需求,dart提供了命名构造方法,解决了没有构造方法重载的问题。
class _Point {
int? _x;
int? _y;
String? _name;
_Point(this._x, this._y); //构造方法
// 命名构造方法
_Point.setY(int _y) {
this._y = _y;
}
// 命名构造方法
_Point.setName(String _name) {
this._name = _name;
print(_name);
}
}
- 参数初始化列表
从一个外部传递的参数,来初始化类的属性。
_Point.fromMap(Map map) : _x = map["x"], _y = map["y"];
这句代码的意思很直白,从map中取出k
为"x"
的值赋值给_x
,取出k
为"y"
的值赋值给_y
。
- 重定向构造方法
class View {
View(int context, int attr);
// 重定向构造方法
View.a(int context):this(context, 0);
}
- 常量构造方法
在构造方法前面加上const
关键字,就说明该构造方法是常量构造方法。
这样写编译器会报错:不允许在
const
的构造方法里传入非final
的参数。我们需要在参数前面加上final
class ImmutabelPoint {
final int x;
final int y;
const ImmutabelPoint(this.x, this.y);
}
如果使用const来定义了构造方法,在使用构造方法创建对象时,可以这样来用:
void main() {
var p1 = const ImmutabelPoint(1, 1);
var p2 = const ImmutabelPoint(1, 1);
print(p1.hashCode == p2.hashCode); // true
print(p1 == p2); //true
}
我们知道,在java中,通过new的方式来创建对象,每次创建会开辟一块新的内存,在dart中,同样通过new的方式创建对象的话,和普通对象没有区别。而通过const来创建多个对象,并且传递参数也相同,这几个对象其实是同一个对象,又叫编译期常量对象。
在上面的示例中,p1和p2是同一个对象。
- 工厂构造方法
使用factory
声明的构造方法,叫做工厂构造方法。工厂构造方法,必须返回一个实例对象。
class Manager {
factory Manager.get() {
return Child(); // 返回子类实例对象
// return Manager(); // 返回自身实例对象
}
Manager(); // 必须提供默认构造方法,否则编译器报错
}
class Child extends Manager {
}
void main() {
Manager.get();
}
class Manager {
static Manager? _instance;
factory Manager.getInstance() {
return _instance ??= Manager._newInstance();
}
//私有的
Manager._newInstance();
}
上面就是一个单例模式,在外部文件中只能通过Manager.getInstance()
来获取Manager
实例。
操作符重载
class Point {
int? _x;
int? _y;
Point operator +(Point other) {
var point = Point();
point._x = _x! + other._x!;
return point;
}
}
void main() {
var p1 = Point();
var p2 = Point();
p1._x = 10;
p2._x = 40;
var p3 = p1 + p2;
print(p3._x); // 结果:50
}
抽象类
和java的区别在于:方法前面不需要加abstract,而且可以写方法体。
abstract class Test {
void test(); // 抽象方法,不需要在方法前面声明abstract
void test1() {
}
}
接口
和java不同,dart中没有interface
关键字,Dart中每个类都隐式的定义了一个包含所有实例成员的接口,并且这个类实现了这个接口,如果你想创建类A
来支持类B
的方法,而不想继承B
的实现,则类A
应该实现B
的接口。
class Listener {
void onSuccess(){}
void onFailure(){}
}
class MyListener implements Listener {
@override
void onFailure() {
// TODO: implement onFailure
}
@override
void onSuccess() {
// TODO: implement onSuccess
}
}
call
在dart中,还有一种骚操作,对象当函数调用。如果Dart类实现了call()
方法,则对象可以当作方法来调用。
class Test2 {
void call() {
print("调用了call方法");
}
}
void main() {
var test2 = Test2();
test2(); // 这句代码就调用了Test2类中的call方法,打印:调用了call方法
}
混合mixin
Dart不支持多继承,但是他支持mixin,简单来讲 mixin 可以 “组合” 多个类,我们通过一个例子来理解。 定义一个 Person 类,实现吃饭、说话、走路和写代码功能,同时定义一个 Dog 类,实现吃饭、和走路功能:
class Person {
say() {
print('say');
}
}
mixin Eat {
eat() {
print('eat');
}
}
mixin Walk {
walk() {
print('walk');
}
}
mixin Code {
code() {
print('key');
}
}
class Dog with Eat, Walk{}
class Man extends Person with Eat, Walk, Code{}
我们定义了几个 mixin,然后通过 with 关键字将它们组合成不同的类。有一点需要注意:如果多个mixin 中有同名方法,with 时,会默认使用最后面的 mixin 的,mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。
Dart异步编程
isolate机制
Dart是基于单线程模型的语言,但是在开发当中我们经常会进行耗时操作比如网络请求,这种耗时操作会阻塞我们的代码,所以在Dart也有并发机制,叫:isolate。 APP的启动入口main
函数就是一个类似Android主线程的一个主isolate
,和java的Thread不同的是,Dart中的isolate
无法共享内存。
- isolate的双向通信机制
void main() {
var receivePort = ReceivePort();
Isolate.spawn(entryPoint, receivePort.sendPort);
receivePort.listen((message) {
if (message is SendPort) {
message.send("主 ------> 子"); // 主isolate发送消息到子isolate
} else {
print(message); // 这里接收子isolate发送的消息:子 ------> 主
}
});
}
void entryPoint(SendPort sendPort) {
var receivePort = ReceivePort();
var sendPort2 = receivePort.sendPort;
sendPort.send(sendPort2);
sendPort.send("子 ------> 主"); // 子isolate发送消息到主isolate
receivePort.listen((message) {
print(message); // 这里接收主isolate发送的消息:主 ------> 子
});
}
解析一下上面的流程:
- 在dart中,isolate虽然不像java中的线程一样,共享同一块内存,但又不能划在进程级别,因为进程量级是很重的,其实更像是dart中的线程,只不过是内存隔离罢了。
- 既然是线程,那么就会有主线程和子线程,线程之间消息是可以相互通信的,注意:这里不涉及线程同步问题(因为内存隔离), 上面的代码中,
main
函数所处的线程就是主线程,entryPoint
函数所处的线程就是子线程。 - 在主线程中,创建一个消息接收器
receivePort
,然后将消息接收器当中的发送器receivePort.sendPort
发给子线程,在子线程中,使用主线程发过来的sendPort
进行send发送,就可以在主线程进行listen
监听消息,接收到子线程的消息并打印。 - 在子线程中,同样也可以创建一个消息接收器
receivePort
,然后将消息接收器当中的发送器sendPort.send(sendPort2)
发送给主线程,同样可以在 主线程监听,判断message
如果是SendPort
,就可以在主线程发送消息给子线程了,子线程监听到消息后就会打印。
任务队列
void main() {
var receivePort = ReceivePort();
receivePort.listen((message) {
print(message);
});
// 在微任务队列中提交一个任务
Future.microtask(() {
print("微任务执行1");
});
receivePort.sendPort.send("发送消息给消息接收器1");
Future.microtask(() {
print("微任务执行2");
});
receivePort.sendPort.send("发送消息给消息接收器2");
Future.microtask(() {
print("微任务执行3");
});
receivePort.sendPort.send("发送消息给消息接收器3");
}
上面的输出结果是:
微任务执行1
微任务执行2
微任务执行3
发送消息给消息接收器1
发送消息给消息接收器2
发送消息给消息接收器3
这就证明微任务优先级是高于普通任务的,无论是先提交微任务还是普通任务,微任务优先执行。
那么我们会有一个疑问:微任务会插队吗?看下面的代码:
void main() {
var receivePort = ReceivePort();
receivePort.listen((message) {
print(message);
Future.microtask(() {
print("开启微任务");
});
});
receivePort.sendPort.send("发送消息给消息接收器1");
receivePort.sendPort.send("发送消息给消息接收器2");
receivePort.sendPort.send("发送消息给消息接收器3");
}
首先提交三个普通任务,在接收到普通任务后,插入微任务,此时微任务会在这里插队执行吗?答案是:会插队。
发送消息给消息接收器1
开启微任务
发送消息给消息接收器2
开启微任务
发送消息给消息接收器3
开启微任务
看到这样的打印,就明白了微任务是会插队执行的。
假设:我现在在main方面最后加入sleep休眠,会不会影响listen回调的时机?
是会影响的,所有的任务都要等sleep休眠结束,再执行,这就再次证实了Dart是单线程的结论。
Future
Future
与JavaScript中的Promise
非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future
只会对应一个结果,要么成功,要么失败。 由于本身功能较多,这里我们只介绍其常用的API及特性。还有,请记住,Future
的所有API的返回值仍然是一个Future
对象,所以可以很方便的进行链式调用。
1)Future.then
为了方便示例,在本例中我们使用Future.delayed
创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即2秒后返回结果字符串"hi world!",然后我们在then
中接收异步结果并打印结果,代码如下:
Future.delayed(Duration(seconds: 2), () {
return "hi world";
}).then((value) => print(value));
2)Future.catchError
如果异步任务发生错误,我们可以在catchError中捕获错误,我们将上面示例改为:
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print("success");
}).catchError((e){
//执行失败会走到这里
print(e);
});
在本示例中,我们在异步任务中抛出了一个异常,then
的回调函数将不会被执行,取而代之的是 catchError
回调函数将被调用;但是,并不是只有 catchError
回调才能捕获错误,then
方法还有一个可选参数onError
,我们也可以用它来捕获异常:
Future.delayed(Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
3)Future.whenComplete
有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如:在网络请求前弹出加载对话框,在请求结束后关闭对话框,这种场景,有两种方法:
- 分别在
then
或catch
中关闭一下对话框 - 使用
Future
的whenComplete
回调
我们将上面示例改一下:
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});
4)Future.wait
有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到UI界面上,应该怎么做?答案是Future.wait
,它接受一个Future
数组参数,只有数组中所有Future
都执行成功后,才会触发then
的成功回调,只要有一个Future
执行失败,就会触发错误回调。下面,我们通过模拟Future.delayed
来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:
Future.wait([
// 2秒后返回结果
Future.delayed(Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(Duration(seconds: 4), () {
return "world";
})
]).then((result) {
print(result[0] + result[1]);
}).catchError((e) {
print(e);
});
执行上面代码,4秒后你会在控制台中看到“hello world”。
2. async/await
Dart中的async/await
的意思就是:异步任务串行化,可以避免我们在使用过程中遇到的回调地狱问题。 先来看看什么是回调地狱:
1)回调地狱(Callback Hell)
如果代码中有大量异步逻辑,并且出现大量异步任务依赖其他异步任务的结果时,必然会出现Future.then回调中套回调的情况。举个例子:现在有个需求场景是用户先登录,登录成功后会获得用户ID,然后通过用户ID,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下:
//先分别定义各个异步任务
Future<String>? login(String userName, String pwd){
...
//用户登录
};
Future<String>? getUserInfo(String id){
...
//获取用户信息
};
Future? saveUserInfo(String userInfo){
...
// 保存用户信息
};
接下来,执行整个任务流:
login("admin", "xxx")?.then((id) {
getUserInfo(id)?.then((userInfo) {
saveUserInfo(userInfo)?.then((value) {
// ...
});
});
});
可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback Hell) 。回调地狱问题在之前 JavaScript 中非常突出,也是 JavaScript 被吐槽最多的点,但随着 ECMAScript 标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是 ECMAScript6 引入了Promise
,以及ECMAScript7 中引入的async/await
。 而在 Dart 中几乎是完全平移了 JavaScript 中的这两者:Future
相当于Promise
,而async/await
连名字都没改。接下来我们看看通过Future
和async/await
如何消除上面示例中的嵌套问题。
2)消除回调地狱
消除回调地狱主要有两种方式:
一、使用Future消除Callback Hell
login("alice","******").then((id){
return getUserInfo(id);
}).then((userInfo){
return saveUserInfo(userInfo);
}).then((e){
//执行接下来的操作
}).catchError((e){
//错误处理
print(e);
});
正如上文所述, “Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用” ,如果在then
中返回的是一个Future
的话,该future
会执行,执行结束后会触发后面的then
回调,这样依次向下,就避免了层层嵌套。
二、使用 async/await 消除 callback hell
通过Future
回调中再返回Future
的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用async/await
了,下面我们先直接看代码,然后再解释,代码如下:
task() async {
try {
String? id = await login("admin", "xxx");
String? userInfo = await getUserInfo(id!);
await saveUserInfo(userInfo!);
//执行接下来的操作
} catch(e) {
//错误处理
print(e);
}
}
async
用来表示函数是异步的,定义的函数会返回一个Future
对象,可以使用then
方法添加回调函数。await
后面是一个Future
,表示等待该异步任务完成,异步完成后才会往下走。await
必须出现在async
函数内部。
可以看到,我们通过async/await
将一个异步流用同步的代码表示出来了。
Stream
Stream
也是用于接收异步事件数据,和 Future
不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream
常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
上面的代码依次会输出:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
转载自:https://juejin.cn/post/7120801875942703111