上一篇: 一、Flutter环境搭建
下一篇: 三、Flutter基础—ListView入门
我之前只有OC开发经验,可能相对于使用Java或JS开发的要上手慢一些。
还在学习,陆续更新吧……
参考资料
- http://wiki.jikexueyuan.com/project/dart-language-tour/ 【极客学院,可下载pdf版】
- https://www.dartlang.org/guides/language/language-tour 【官方英文文档】
大纲
一、 VSCode安装Dart遇到的问题
二、 函数
三、 类
四、 泛型
五、 异常抛出及捕获
六、 库和可见性
七、 异步
一、 VSCode安装Dart遇到的问题
在VSCode下载完Dart Code插件后,新建了个flutter项目,能成功在iOS模拟器上运行。但今天想学习下dart语法,新建个.dart文件,执行run code却报/bin/sh: dart: command not found
的错:
原因是下载Fluter SDK时,Dart SDK已经在捆绑Fluter里了,所以需要设置下Dart环境变量的路径:
- 命令打开环境变量:
sudo vi ~/.bash_profile
- 配置Dart SDK所在路径:
export PATH=${PATH}:/Users/ganlan/SDK所在路径
- 保存并退出,终端输入
dart
,检查下是否配置成功。 - 重启VSCode,重新run一下.dart文件即可。
二、 函数
1. 无返回值函数,void可省略
void printNumber(num number) {
print('The number is $number.');
}
对于仅含有一个表达式的方法,可以使用简写的语法:
void printNumber(num number) =>
print('The number is $number.');
2. 可选参数
Dart方法中有两种类型的参数:必需和可选的。必需的我们一般都知道,这里主要分析可选参数。
可选参数可以是可选位置参数或者可选命名参数,但不能既是可选位置参数又是可选命名参数。
这两种可选参数都可以定义默认值。但是默认值必须是编译时的常量,比如字面值。如果没有为之提供默认值,那么该参数的默认值将会是 null。
- 可选命名参数
在方法参数中,使用"{}"包围的参数属于可选命名参数,也可为可选参数添加默认参数,比如:
// 可选命名参数
studentInfo(int num, {String name, int range = 15}) {
print('Student num: $num,name: $name,range: $range');
}
调用包含可选命名参数的方法时,需要使用paramName:value的形式指定为哪个可选参数赋值:
studentInfo(62, name:'Olive'); // print Student num: 62,name: Olive,range: 15
studentInfo(10, range: 31); // print Student num: 10,name: null,range: 31
- 可选位置参数
在方法参数中,使用"[]"包围的参数属于可选位置参数:
// 可选位置参数
userInfo(int age, [String name, int num = 5]) {
print('User age: $age,name: $name, num: $num');
}
调用包含可选位置参数的方法时,无需使用paramName:value的形式,因为 可选位置参数是位置,如果想指定某个位置上的参数值,则必须前面位置的已经有值,即使前面的值存在默认值:
userInfo(15, 'Tom'); // 可行
userInfo(22, 'Marry', 20); // 可行
userInfo(18); // 可行
userInfo(10, 6); // 报错,不可跳过“name”参数为“num”赋值
3. 匿名函数
可以创建一个没有名字函数(匿名函数),有时也可以创建lambda或闭包。可将匿名函数分配给变量。匿名与命名函数类似,可以有零个或多个参数,括号中用逗号和可选类型注释隔开。
下面的代码块包含函数的主体:
([[Type] param1[, …]]) {
codeBlock;
};
下面的示例使用非类型化参数item定义了一个匿名函数。为列表中的每个项目调用的函数会打印一个字符串,其中包含指定索引处的值。
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
如果函数只包含一条语句,可以使用=>
来缩短它:
list.forEach(
(item) => print('${list.indexOf(item)}: $item'));
4. 闭包
- 闭包是一个方法(对象)
- 闭包定义在其它方法内部
- 闭包能够访问外部方法内的局部变量,并持有其状态
函数可以关联周围作用域中定义的变量。下面例子中,makeAdder()获取变量a。无论返回的函数去哪里,它都会记住a。
void main (){
var add = makeAdder(5);
print(add(4)); // 9
}
makeAdder(num a) {
print('a = $a'); // a = 5
return (int b) => a + b; // 返回匿名函数
}
三、类
1. 构造函数
要声明一个构造函数,只需创建一个与类同名的方法(或者加上一个额外的标识符命名构造函数的描述)
class MyClass {
String name;
int age;
MyClass(this.name, this.age);
}
-
命名构造函数
使用命名构造函数可以为一个类声明多个构造函数,或者说是提供额外的声明:
class MyClass {
String name;
int age;
MyClass(this.name, this.age);
// 命名构造函数
MyClass.fromJson(Map json) {
name = json['name'];
age = json['age'];
}
}
构造函数不能继承,这意味着子类不会继承父类的构造函数。如果你希望子类在创建之后能够拥有在父类中声明的命名构造函数,你就必须在子类中实现该构造函数。
- 调用非默认的父类的构造函数
默认情况下,在子类的构造函数将会调用父类的无参数构造函数。如果父类没有构造函数,则必须手动调用父类的构造函数中的一个。在冒号(:)之后、构造函数之前指定父类的构造函数(如果有的话)。
main() {
var student = new Student.fromJson({'name':'Olive','age':18});
/// print:
/// In MyClass! name=Olive, age=18
/// In Student!
}
class MyClass {
String name;
int age;
// 命名构造函数
MyClass.fromJson(Map json) {
name = json['name'];
age = json['age'];
print('In MyClass! name=$name, age=$age');
}
}
class Student extends MyClass {
// MyClass没有默认构造函数;
// 必须调用 super.fromJson(json)
Student.fromJson(Map data) : super.fromJson(data) {
print('In Student!');
}
}
警告:父类构造函数的参数不能访问 this 。例如,参数可调用静态方法但是不能调用实方法。
2. 初始化列表
除了调用父类构造函数,你也可以在构造函数体运行之前初始化实例变量。用逗号隔开使其分别初始化。
class MyClass {
String name;
int age;
// 初始化列表在构造函数运行前设置实例变量。
MyClass.fromJson(Map json)
: name = json['name'],
age = json['age'] {
print('In MyClass! name=$name, age=$age');
}
}
警告:右手边的初始化程序无法访问 this 关键字。
3. 重定向构造函数
有时一个构造函数的目的只是重定向到同一个类中的另一个构造函数。如果一个重定向的构造函数的主体为空,那么调用这个构造函数的时候,直接在冒号后面调用这个构造函数即可。
class Point {
num x;
num y;
// 这个类的主构造函数。
Point(this.x, this.y);
// 主构造函数的代表。
Point.alongXAxis(num x) : this(x, 0);
}
四、 泛型
泛型通常是为了类型安全而必需的,但是它们比仅仅允许您的代码运行有更多的好处:
- 适当地指定泛型类型会生成更好的代码。
- 您可以使用泛型来减少代码重复。
- 比如,想要List只包含字符串,可以将其声明为List <String>。通过这种方式,您的程序员伙伴和您的工具可以检测到向列表分配非字符串可能是一个错误:
var names = new List<String>();
names.addAll([‘Seth’,’Kathy’,’Lars’]);
//...
names.add(42); //在调试模式中失败 (在生产模式中成功).
- 为了在许多类型间共享一个接口,以减少代码的重复,也可以使用到泛型。例如,创建了一个缓存对象的接口:
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
但又发现需要此接口的特定字符串版本,因此创建了另一个接口:
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
后续开发中,你发现还需要这个接口的数字专用的接口……这时,就可以用到泛型了:泛型类型可以减少你创建这些接口的困难。
取而代之的是,你只需要创建一个带有一个类型参数的接口即可:
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
在这段代码中,T是替代类型。它是一个占位符,您可以将其视为开发人员稍后将定义的类型。
五、Dart异常抛出及捕获
1. throw 抛出异常
这里抛出了一个异常:
throw new FormatException('Expected at least 1 section');
你也可以将任意对象作为异常抛出:
throw 'Out of llamas!';
由于抛出异常是单个表达式,所以你可以将 in=> 语句作为异常抛出,也可以在其他任何地方抛出异常。
distanceTo(Point other) =>
throw new UnimplementedError();
2. catch捕获异常
捕获了一个异常后,就停止了捕获异常过程。捕获一个异常,你就有机会去处理它:
try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}
为了处理含有多种类型异常的代码,你可以选择多个catch子句。第一个匹配抛出对象类型的catch子句将会处理这个异常。如果catch子句未说明所捕获的异常类型,这个子句就可处理任何被抛出的对象。
try {
breedMoreLlamas();
} on OutOfLlamasException {
// 一个具体异常
buyMoreLlamas();
} on Exception catch (e) {
// 任意一个异常
print('Unknown exception: $e');
} catch (e) {
// 非具体类型
print('Something really unknown: $e');
}
像上面展示的代码一样,你可以用on
或者catch
,或者两者都用。当你需要指定异常类型的时候用on
,当你的异常处理者需要异常对象时用catch
。
3. finally
为了确保不论是否抛出异常,代码都正常运行,可使用finally
子句。如果没有catch
匹配子句的异常,finally
子句运行以后异常将被传播:
try {
breedMoreLlamas();
} finally {
// 即使抛出一个异常时也会进行清理
cleanLlamaStalls();
}
在匹配了所有 catch 之后,子句 finally 运行了。
try {
breedMoreLlamas();
} catch(e) {
print('Error: $e'); // 先处理异常
} finally {
cleanLlamaStalls(); // 然后清理
}
4. Assert
如果一个布尔条件值为false,使用assert语句来中断正常执行的代码。你可以在本教程中找到一些assert语句的样例。这里有一些例子:
//确保这个变量不为空值.
assert(text != null);
//确保这个变量小于100.
assert(number < 100);
//确保它是一个https协议类型的URL.
assert(urlString.startsWith(‘https’));
提示:Assert语句仅仅只能在调试模式下使用,在生产模式下没有任何作用。
在assert语句后面的括号中,你可以加入任何表示布尔值或者函数的表达式。如果表达式的值或者函数返回值true,则assert语句成功并继续执行代码。如果值为false,则assert语句失败并抛出一个异常 (an AssertionError)。
六、Dart - 库和可见性
import,part,library指令可以帮助创建一个模块化的,可共享的代码库。库不仅提供了API,还提供隐私单元:以下划线(_)开头的标识符只对内部库可见。每个Dartapp就是一个库,即使它不使用库指令。
1. 使用库
使用 import 来指定如何从一个库命名空间用于其他库的范围。
唯一需要 import 的参数是一个指向库的 URI。对于内置库,URI中具有特殊dart:scheme
。对于其他库,你可以使用文件系统路径或package:scheme
。包 package:scheme specifies libraries ,如pub工具提供的软件包管理器库。例如:
import 'dart:io';
import 'package:mylib/mylib.dart';
import 'package:utils/utils.dart';
注:URI代表统一资源标识符。网址(统一资源定位器)是一种常见的URI的。
2. 指定库前缀
如果导入两个库是有冲突的标识符,那么你可以使用as
指定一个或两个库的前缀。例如,如果 library1 和 library2 都有一个元素类,那么你可能有这样的代码:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// ...
var element1 = new Element(); // 使用lib1里的元素
var element2 = new lib2.Element(); // 使用lib2里的元素
3. 导入部分库
如果想使用的库一部分,你可以使用show
或hide
选择性导入库。例如:
// 只导入foo库
import 'package:lib1/lib1.dart' show foo;
//导入所有除了foo
import 'package:lib2/lib2.dart' hide foo;
4. 延迟加载库
延迟加载允许应用程序按需加载库。下面是当你可能会使用延迟加载某些情况:
- 为了减少应用程序的初始启动时间;
- 执行A / B测试-尝试的算法的替代实施方式中;
- 加载很少使用的功能,例如可选的屏幕和对话框。
为了延迟加载一个库,你必须使用deferred as
先导入它。
import 'package:deferred/hello.dart' deferred as hello;
当需要库时,使用该库的调用标识符(hello
)调用LoadLibrary()
:
greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
在前面的代码,在库加载好之前,await
关键字都是暂停执行的。
您可以在一个库调用loadLibrary()
多次都没有问题。该库也只被加载一次。
当您使用延迟加载,请记住以下内容:
- 延迟库的常量在其作为导入文件时不是常量。记住,这些常量不存在,直到迟库被加载完成。
- 你不能在导入文件中使用延迟库常量的类型。相反,考虑将接口类型移到同时由延迟库和导入文件导入的库。
- Dart隐含调用LoadLibrary()插入到定义
deferred as namespace
。在调用LoadLibrary()
函数返回一个Future。
七、 异步
Dart 添加了一些新的语言特性用于支持异步编程。最通常使用的特性是
async
方法和await
表达式。Dart 库大多方法返回Future
和Stream
对象。这些方法是异步的:它们在设置一个可能的耗时操作(比如 I/O 操作)之后返回,而无需等待操作完成。
使用async
和await
的代码是异步的,比如这里有一段使用await
等待一个异步函数结果的代码,要使用await
,代码必须用await
标记:
checkVersion() async {
var version = await lookUpVersion();
if (version == expectedVersion) {
// Do something.
} else {
// Do something else.
}
}
1. 什么是Future
Future表示在将来某时获取一个值的方式。当一个返回Future的函数被调用的时候,做了两件事情:
- 函数把自己放入队列和返回一个未完成的Future对象
- 之后当值可用时,Future带着值变成完成状态。
为了获得Future的值,有两种方式:
- 使用async和await
- 使用Future的接口
2. 声明异步函数
一个异步函数是一个由async
修饰符标记的函数。虽然一个异步函数可能在操作上比较耗时,但是它可以立即返回-在任何方法体执行之前。
checkVersion() async {
// ...
}
lookUpVersion() async => /* ... */;
添加了async
的函数,将返回一个Future
。
例如,下面为返回一个字符串的同步函数:
String lookUpVersionSync() => '1.0.0';
将上面方法改为异步方法,因为在以后的实现中,可能有很耗时的操作,它的返回值为Future
:
Future<String> lookUpVersion() async => '1.0.0';
3. Async和await
async
和await
关键字是Dart异步支持的一部分。他们允许你像写同步代码一样写异步代码和不需要使用Future接口。
async
函数执行时,不是立即挂起,而是要执行到函数内部的第一个await
。
如下面一段代码:
import 'dart:async';
printDailyNewsDigest() async {
String news = await gatherNewsReports();
print(news);
}
main() {
printDailyNewsDigest();
printWinningLotteryNumbers();
printWeatherForecast();
printBaseballScore();
}
printWinningLotteryNumbers() {
print('Winning lotto numbers: [23, 63, 87, 26, 2]');
}
printWeatherForecast() {
print("Tomorrow's forecast: 70F, sunny.");
}
printBaseballScore() {
print('Baseball score: Red Sox 10, Yankees 0');
}
const news = '<gathered news goes here>';
Duration oneSecond = const Duration(seconds: 1);
final newsStream = new Stream.periodic(oneSecond, (_) => news);
// Imagine that this function is more complex and slow. :)
Future gatherNewsReports() => newsStream.first;
执行结果为:
Winning lotto numbers: [23, 63, 87, 26, 2]
Tomorrow's forecast: 70F, sunny.
Baseball score: Red Sox 10, Yankees 0
<gathered news goes here>
从执行结果我们可以注意到printDailyNewsDigest()
是第一个调用的,但是新闻是最后才打印,即使只要一行内容。这是因为代码读取和打印内容是异步执行的。
-
连续执行
你可以使用多个await表达式,保证一个await执行完成过后再执行下一个:
// Sequential processing using async and await.
main() async {
await expensiveA();
await expensiveB();
doSomethingWith(await expensiveC());
}
4. 异步事件流streams
顾名思义Stream 就是流的意思,表示发出的一系列的异步数据。可以简单地认为 Stream 是一个异步数据源。它是 Dart 中处理异步事件流的统一 API。
4.1 如何使用steam
获得Stream的方法:
- 通过构造函数
- 使用StreamController
- IO Stream
stream有三个构造方法:
Stream.fromFuture : 从Future创建新的单订阅流,当future完成时将触发一个data或者error,然后使用Down事件关闭这个流。
Stream.fromFutures : 从一组Future创建一个单订阅流,每个future都有自己的data或者error事件,当整个Futures完成后,流将会关闭。如果Futures为空,流将会立刻关闭。
Stream.fromIterable : 创建从一个集合中获取其数据的单订阅流:
Stream.fromIntreable([1,2,3]);
监听Stream的方法:
监听一个流最常见的方法就是listen。当有事件发出时,流将会通知listener。Listen方法提供了这几种触发事件:
- onData(必填):收到数据时触发
- onError:收到Error时触发
- onDone:结束时触发
- unsubscribeOnError:遇到第一个Error时是否取消订阅,默认为false
StreamController
如果想创建一条新的流的话,使用StreamController,它为你提供了非常丰富的功能,你能够在streamController上发送数据,处理错误,并获得结果!
//任意类型的流
StreamController controller = StreamController();
controller.sink.add(123);
controller.sink.add("xyz");
controller.sink.add(Anything);
//创建一条处理int类型的流
StreamController<int> numController = StreamController();
numController.sink.add(123);
泛型定义了我们能向流上推送什么类型的数据,它可以是任何类型。
我们再来看看如何获取最后的结果。
StreamController controller = StreamController();
// 监听这个流的出口,当有data流出时,打印这个data
StreamSubscription subscription =
controller.stream.listen((data)=>print("data"));
controller.sink.add(123);
输出: 123
你需要将一个方法交给stream的listen
函数,这个方法入参(data)是我们的StreamController处理完毕后产生的结果,我们监听出口,并获得了这个结果(data)。这里可以使用lambda表达式,也可以是其他任何函数。
4.2 结合 streams 使用异步循环
一个异步循环具有以下形式:
await for (variable declaration in expression) {
// Executes each time the stream emits a value.
}
表达式
的值必须有Stream 类型(流类型)。执行过程如下:
- 在 stream 发出一个值之前等待;
- 执行 for 循环的主体,把变量设置为发出的值;
- 重复 1 和 2,直到 Stream 关闭。
如果要停止监听 stream ,你可以使用break
或者return
语句,跳出循环并取消来自stream
的订阅 。
如果一个异步 for 循环没有正常运行,请确认它是一个异步方法。 比如,在应用的main()
方法中使用异步的for
循环时,main() 的方法体必须被async
标记。
main() async {
...
await for (var request in requestServer) {
handleRequest(request);
}
...
}