Flutter入门概览2-Dart篇
本系列文章共有三篇,计划分别从Flutter、Dart、工具三个方面介绍,快速形成对Flutter开发的一个综合概念。
本文主要介绍Dart相关知识。
Flutter语言使用 Dart,开发IDE使用VSCode、Android Studio 或 IntelliJ IDEA,个人推荐使用VSCode,因为前端无缝切换。
语言概要
Dart语言有点像C语言和JS语言。按照官网的说法,Dart继承了C、Java、JS以及其他语言相同的语句和表达式语法,让开发者感到亲切。
Dart是强类型语言,在开发时会按照JIT模式,编译成App后时会按照AOT模式。
// dart函数,跟c一模一样
void main() {
print('Hello, World!');
}
基本语法基本看一个官方的 Dart语言概览 就差不多,后面边写边看。
通过DartPod可以在线运行Dart代码。
⚠️注意:Dart中所有数据结构都是对象。
语言基础
变量
var
,变量,声明一个变量,变量类型需要能够自动推导出类型
,类型变量,例如num
,list
,String
,当变量不能自动推导出,一般显式给出类型const
,常量,编译时常量,编译期间就确定,不能再改变final
,终值,运行时常量,一旦创建不能再改变,用于运行时动态创建的常量,只能赋值一次,多用于构造函数中late
,声明一个延迟初始化的非空变量,该变量在声明时未初始化,在使用时需要初始化。一般用于显式声明一个非空变量但不初始化(不用late不能通过静态检查)和延迟初始化变量。Object
,Dart中任何类型都是Object的子类型,因此可以用Object表示所有类型。dynamic
,类似TS中的any,可以表示任何类型,dynamic
和Object
声明的变量,类型可以更改,运行时才会确定。dynamic
一般用在解析JSON字符串的场景。Object
一般用在需要使用多态特性实现基类和子类中方法的调用。另外,dynamic
可以表示null,Object
需要使用Object?
表示可以为空。dynamic
还有一个作用,编译阶段允许对其进行任何值的访问而编译不报错,但是运行时如果没有则会报错,这块详看下面示例说明。?
,空修饰符,允许变量为空,Dart语言为空安全方式,正常变量不允许为空,如果可能为空必须使用?
声明,跟在修饰符后面表示变量可为空
。
// var,声明name变量,该变量会自动推导出String,且该变量可修改
var name = 'Bob';
// 类型,显式声明name变量为String类型,效果同var,变量可修改
String name = 'Bob';
// const 常量,声明后不能在改变
const bar = 1000000;
const int bar = 1000000; // 可加类型说明
// final 终值,赋值后不能再改变
final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby'; // 可加类型说明
//late
// 声明一个非空变量,但不在声明时初始化,不使用late无法通过静态检查
late String description;
// 延迟初始化
late String temperature = readThermometer(); // 使用时才会调用 readThermometer 函数
// ? ,空修饰符
String? name = null; // 此时name可以为空,类型为 String or null。
// error, var? name = null; // 不能使用 var? ,因为无法推断出类型
// Object,可以表示任何类型,类型可以改变
Object name = "123";
name = 123;
name = true;
print(name);
// 使用 Object? 允许为空
Object? name = "123";
name = null;
print(name);
// 使用dynamic 表示任何类型,且可以表示null
dynamic name = "123";
name = 123;
name = null;
print(name);
// 编译阶段允许对dynamic进行任何值的访问而编译不报错
// 可用于一些比较灵活的场景,但是需要自己确保运行时不报错
dynamic name = "123";
print(name.a); // 编译阶段是可以通过的,但是运行时会报错。
// dynamic 主要还是用于 JSON类型转换
Map<String, dynamic>
运算符
操作符特别值得一说的主要是空合运算符和级联运算符:
??
,空合运算符(空值合并运算符)// 当name != null时,使用name,否则选取 'unkown' var test = name ?? 'unknown'; // ??=,当b为空时则赋值为value,否则继续使用b b ??= value; // 等同于 b = b ?? value;
..
、..?
,级联运算符(cascade),一种简化写法,可以在同一对象上执行一系列操作// 使用级联运算符可以在之前对象上继续操作 var person = Person() ..name = "Tom" ..age = 18; // 等同于 var person = Person(); person.name = "Tom"; person.age = 18;
其他:
- 类型运算符:
as
,类型假定,用于编译通过,不会做类型转换,用于静态检查(如果对象不是假定的类型,运行时会报错)is
,判断是否是某个类型is!
,判断是否不是某个类型,与is正好相反
// as,假定 employee 为 Person (employee as Person).firstName = 'Bob'; // is,判断是否是某类型 if (employee is Person) { // Type check employee.firstName = 'Bob'; } // is!,判断是否不是某类型,与is正好相反 if (employee is! Person) { // Type check employee.firstName = 'Bob'; }
- 条件访问运算符:
?.
,对象访问属性,如果对象为空则返回null,不会触发错误?[]
,数组访问元素
// ?. foo?.bar // foo为空时整个表达式返回null,不会报错;不为空时正常赋值 // ?[] list?[0] // 当数组list为空时不会报错
!
,将表达式视为不为空类型(运行时如果为空则会报错)// 把foo视为非空对象,如果为空则运行时会报错 foo!.bar
注释
//
,单行注释/* */
,多行注释///
,文档注释,放在文件顶部用于文档说明/// A domesticated South American camelid (Lama glama). /// /// Andean cultures have used llamas as meat and pack /// animals since pre-Hispanic times. /// /// Just like any other animal, llamas need to eat, /// so don't forget to [feed] them some [Food]. class Llama { } 注:[Food] 表示一个类跳转,当使用dart doc生成文档时,会自动变成该类的点击跳转
注解
@deprecated
,标记废弃,无任何说明文案@Deprecated
,标记废弃,可以指定一条说明文案,推荐总是使用这个@override
,重写父类方法,方法名不正确时编译器会提示,避免意外创建新方法,-
@pragma
,编译优化相关 自定义注解
,定义一个class就可以当注解class Television extends TV { // 标记废弃,但是没有说明文案 @deprecated void turnOn() { } // 标记废弃,可以带一条说明文案 @Deprecated('Use turnOn instead') void activate() { turnOn(); } // 重写父类方法 @override void tvName() { } } // 自定义注解, // 1. 建立一个class class Todo { final String who; final String what; const Todo(this.who, this.what); } // 2. 使用@className标注,参数即为class的参数 @Todo('Dash', 'Implement this function') void doSomething() { print('Do something'); }
库导入
import
,导入库或文件。dart:
,导入内置库,使用import 'dart:io';
。package:
,导入三方库,示例:import 'package:test/test.dart';
as
,给库或文件指定别名,示例:import 'package:lib2/lib2.dart' as lib2;
show
,只导出指定的类、方法、变量hide
,与show
相反,导出除了hide指定以外的所有library
,放在库根文件的顶部,用于声明库
// dart: 引入dart核心库
import 'dart:io';
// package: 引入三方库
import 'package:test/test.dart';
// as,使用as指定别名,当有重名冲突时可以比较方便
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// show,指定导出类、函数、变量
import 'package:lib1/lib1.dart' show foo;
// hide,指定 不导出的 类、函数、变量
import 'package:lib2/lib2.dart' hide foo;
// 在库的根文件顶部声明,一般用不上
library my_cli;
数据类型
在Dart中所有类型都是Object,数字、布尔、字符串、对象、函数等等都是Object的子类。
Dart支持以下10种数据类型:
- Numbers,
num
、int
、double
,数字类型 - Strings,
String
,字符串 - Booleans,
bool
,布尔 - Lists,
List
,有序列表 - Sets,
Set
,无序集合 - Maps,
Map
,映射 - Records,
Record
,记录类型,Dart中的元组概念(Dart3.0引入) - null,
Null
,空类型 - Symbols,
Symbol
,标识符,类似JS中的Symbol - Runes,符文类型,处理Unicode符号
代码风格
包管理工具Pub
Pub是Dart的包管理工具,使用pubspec.yaml
作为依赖项管理,pub.dev是包站点。同web的npm 或者iOS的cocoapods。
// 安装包依赖
dart pub get
// 添加transmogrify包
dart pub add transmogrify
// 升级包
dart pub upgrade transmogrify
Dart SDK安装
安装、更新Dart SDK,具体参考:获取 Dart SDK
// 使用brew 安装 dart
brew tap dart-lang/dart
brew install dart
// 更新dart到最新版本
brew upgrade dart
// 切换本地Dart SDK版本
// 1. 先安装对应Dart SDK版本
brew install dart@2.12
// 2. 切换版本,取消当前版本链接,链接目标版本
brew unlink dart@<old> && brew unlink dart@<new> && brew link dart@<new>
// 查看已经安装的Dart版本
brew info dart
异步
Dart是单线程语言,类似JS,通过异步编程模型(Event Loop)来实现异步操作,这块跟JS比较相似。核心概念是 async/await/Future/Stream。
EventLoop
Event Loop的执行过程:
Dart有两个事件队列:
Microtask Queue
,微队列,优先级最高,Event Loop会优先检查MicroTask队列,用于非常简短且需要异步执行的内部动作(比如Stream的异步通知),该队列一般交给Dart处理,开发者很少用到。Event Queue
,事件队列,包含所有的外部事件,比如 I/O,绘制事件,Timer,isolate之间的通信等等,Async/Future/Stream基本上都使用Event Queue。
Async/Await
Dart的异步借鉴了JS的概念,async/await的用法基本同JS中使用。await函数会返回一个Future对象。
注意点:
- await必须在async函数中,这点同js。
- await的报错只能通过trycatch解决,不能await后面跟
catchError
。
// Future类似JS中Promise的概念
Future<String> _test() async{
return 'hello test';
}
// await必须在async函数中
void _incrementCounter() async {
final res = await _test();
print(res);
}
Futrue
Futrue的使用基本上与Promise相同。
一个Future对象会有以下两种状态:
- pending:表示Future对象的计算过程仍在执行中,这个时候还没有可以用的result。
- completed:表示Future对象已经计算结束了,可能会有两种情况,一种是正确的结果,一种是失败的结果。
Futrue核心方法:
then
,获取执行成功的结果,同JS中thencatchError
,获取Future和then中的异常,捕获来自try-catch异常、throw错误等,同JS中的catchwhenComplete
,无论成功或失败总是会被调用,同JS中的finally
// then,catchError,whenComplete
Future(() {
return "1";
}).then((value) {
print('then$value');
}).catchError((error) {
print("catchError$error");
}).whenComplete(() {
print("whenComplete");
});
// Future与async配合使用
void _test() async {
final res = await Future(() => "1");
print('res$res');
}
Futrue其他API:
Future.value()
,Creates a future completed withvalue
,用于需要把数据包装成Future对象的场景,该Future为成功状态,会触发thenFuture.error()
,Creates a future that completes with anerror
,用于需要把数据包装成Future对象的场景,该Future为失败窗台,会触发catchError// 用于需要把数据包装成Future对象的场景 Future<String> fetchDataFromCache(String key) { if (cachedData.containsKey(key)) { return Future.value(cachedData[key]); } else { return Future.error("Data not found in cache"); } }
Future.wait()
,等待执 行多个Futrue,等待所有Future执行完,类似JS中的Promise.all,但有所不同。当所有Future都成功时会执行then,但是如果有一个错误,依然会等待所有的执行完成,然后走catchError,捕捉到第一个错误(如果有多个,也只会捕捉到第一个报错),这是默认行为(因为eagerError
默认为false,如果设置为true,则遇到错误会直接执行catchError,但是其他Future依然会执行)。// 考虑这段代码 Future.wait([ Future(() { print("future 1"); return "1"; }), Future(() { print("future 2"); throw "2 throw"; // return "2"; }), Future(() { print("future 3"); return "3"; }), Future.delayed(Duration(seconds: 3), () { print("future 4 Delayed"); }), ], eagerError: false) .then((res) { print("then $res"); }).catchError((error) { // catchError只会捕捉到第一个错误,其他错误会抛弃 print("catchError $error"); }); // 默认 eagerError == false,此时遇到throw,依然会执行完所有Future,然后走catchError // 此时输出: flutter: future 1 flutter: future 2 flutter: future 3 flutter: future 4 Delayed flutter: catchError 2 throw // 设置 eagerError == true,此时输出: flutter: future 1 flutter: future 2 flutter: catchError 2 throw // 遇到错误会立即走到catchError,但是其他的future依然会执行 flutter: future 3 flutter: future 4 Delayed
Future.any()
,执行多个Future,先执行完先返回,类似JS的Promise.any()timeout
,设置Future的超时时间,超过时间后会抛出 TimeoutException// 示例代码 Future<String> _testTimeout() async { await Future.delayed(Duration(seconds: 3)); return "_testTimeout"; } void _incrementCounter() async { Future(() async { final res = await _testTimeout(); print("Future res $res"); }).timeout(Duration(seconds: 1)).then((value) { print("then $value"); }).catchError((error) { print("catchError $error"); }); } // 点击按钮执行 _incrementCounter(),输出: flutter: catchError TimeoutException after 0:00:01.000000: Future not completed flutter: Future res _testTimeout
delayed()
,延迟执行,时间对Timer的包装Future.delayed(const Duration(seconds: 1), () { print('One second has passed.'); // Prints after 1 second. });
Furture.microtask()
,创建一个Future在MicroTask队列执行。Completer
,通过该Class可以手动控制Future完成,即Completer包装了一个 Future对象Completer()
,构造一个 Completer 对象completer.future
,返回一个Future对象complete()
,表示 Completer 完成,会触发then
completeError()
,表示 Completer 报错,会触发catchError
Future(() { // 创建 Completer,手动控制Future Completer<int> completer = Completer<int>(); Timer(const Duration(seconds: 2), () { print("timer 2s"); // 手动控制 Future完成 completer.complete(2); // or // completer.completeError(Error()); // 走到 catchError }); // 返回 completer的future, return completer.future; }).then((value) { print(value); }).catchError((onError) { print(onError); });
并发
Isolate - 并发
虽然有了异步队列机制,但是当涉及到比较耗时的操作时,为了发挥cpu多核的优势,dart创建了isolate的机制来实现并发。
isolate是Dart中的多线程概念,但不完全等于线程或进程,是一种更高级的抽象封装。isolate的核心就是每个isolate都是独立的,之间不共享任何数据,这点有点像进程的概念。最后isolate的数据传递是直接返回的内存块,不是新的拷贝。所以取了一个isolate的概念,就像孤岛,彼此之间没有数据共享,不会牵涉到多线程之间的相关操作。
Xcode工程测试,每新建一个Isolate,就会创建一个线程,如下图DartWorker34和DartWorker35。其中DartWorker33一直存在,推测该线程负责创建Isolate对应的DartWorker线程。另外,Isolate创建开始运行后,观察发现会创建多于1个线程,最后会被释放剩下一个DartWorker线程。基本符合Isolate的描述,即Isolate是对线程和进程的抽象,在不同平台会依据平台创建线程或进程。
另外,在Flutter平台推荐使用foundation中compute
API,是对isolate在flutter中的封装。
总结下,isolate特点:
- isolate是基于对底层系统的线程和进程的高级封装,不完全等于线程/进程的概念
- 每个isoate都是独立的,isolate之间不共享对象,使用消息机制进行通信
- isolate之间传递的数据是内存块,不是值的拷贝,被称为 closure,closure中的数据是受限制的,不是所有的数据结构都被允许,比如Socket就无法被传递,具体参考send()文档确定可以传递的类型
- isolate在web上不被支持,web上依然需要使用web worker
- 在Flutter平台上,推荐使用
compute()
,该API内部封装了isolate
isolate生命周期:
main isolate的运行模型:
isolate相关的API:
- spawn,创建新的Isolate
- ReceivePort/SendPort,通过ReceivePort实现消息通讯,ReceivePort负责接收消息,SendPort负责发送消息 ,SendPort是由ReceivePort创建的。
- run,对spawn和ReceivePort的封装,简化调用,建议直接使用run方法
- spawnUri,创建新的isolate,与spawn不同的是,spawnUri可以指定新的Isolate代码文件路径,该文件中必须有一个main函数,注意Flutter不支持该API
- exit,立即同步终止当前isolate
// Demo 1,通过 spawn/ReceivePort/SendPort 建立 isolate单向通信
var recv = ReceivePort();
//
Isolate.spawn<SendPort>((SendPort port) {
port.send("hello isolate send!");
}, recv.sendPort);
// 这里可以简单使用 first 只接受第一个事件,然后停止监听
final res = await recv.first;
print("recv: $res");
// 或者使用 recv.listen 监听
// recv.listen((message) {
// print("recv: $message");
// });
// Demo 2. 通过Isolate.run()方法,建议直接使用这个方法
final res = await Isolate.run(() {
return "hello isolate run!";
});
print("recv:$res");
// Demo3, Isolate.spawnUri,使用场景时执行指定文件的dart代码
// 注意Flutter不支持,这里只作为了解
ReceivePort rp = new ReceivePort();
Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], rp.sendPort);
// Demo4,Isolate的异常处理
const String downloadLink = '下载链接';
final resultPort = ReceivePort();
await Isolate.spawn(
readAndParseJsonWithErrorHandle,
[resultPort.sendPort, downloadLink],
onError: resultPort.sendPort,
onExit: resultPort.sendPort,
);
// 获取结果
final response = await resultPort.first;
if (response == null) { // 没有消息
print('没有消息');
} else if (response is List) { // 异常消息
final errorAsString = response[0]; //异常
final stackTraceAsString = response[1]; // 堆栈信息
print('error: $errorAsString \nstackTrace: $stackTraceAsString');
} else { // 正常消息
print(response);
}
flutter中的并发
在flutter中推荐使用compute()
API,该API封装了Isolate.run()
,支持web平台。
使用起来非常简单。
import 'package:flutter/foundation.dart';
// compute简单使用
final res = await compute((message) {
return 'Hello compute!';
},'hello');
print(res);
awesome-dart
参考资料
转载自:https://juejin.cn/post/7377277576652111891