近来,flutter的热度在上升。flutter应用的主要开发语言是dart, 因此,欲练flutter, 必先了解dart. dart是由google开发的编程语言,可用于开发移动应用,桌面应用,h5应用,后端服务。 本文将简单介绍dart的语言特性、基础语法,以及在日常开发中非常实用的如何请求数据、如何处理异步、如何序列化与反序列化json等技能。 文章比较长,熟悉的部分各位看官可快速浏览,文末也给出了小小福利,供大家参考。疏漏之处请见谅,错误之处请指正。 语言特性 面向对象 dart是一门纯粹的面向对象语言,在dart中一切皆对象。 函数也是对象,函数能被赋值给变量,也可以作为函数的参数或返回值 基础类型是对象,字面值也是对象,比如可以像下边这样使用 1.toString(); 支持接口、抽象类、泛型、多态 支持顶级函数和变量 与java不同的是,java的变量和方法只能在类里边,而dart可以有不在类里的方法和变量。 带有面向过程类语言的特点,比如像c。 强类型弱类型均支持 在dart中你可以显式的指定变量的类型,也可以不指定类型,由dart推断类型。指定类型的话编译器可以做静态类型检查。 在类型方面dart既有弱类型语言(像js)也有强类型(像java)的特点。 兼具解释性语言和编译型语言特点 对开发web应用来讲:dart会被转译成js 对app、服务端、桌面应用讲: dart提供了开发模式,通过vm解释执行,支持热更新,具有解释型语言特点 也提供了生产模式,通过编译成机器码执行,具有编译型语言特点 小结 dart看起来是希望融合多种主流语言的特性,在dart中能够看到多种语言的影子。 基础语法 程序入口 dart的程序执行入口是main函数,这跟c很像,main函数一个顶级函数。 返回值通常是void, 写为int, double等也不会报错,不过没有意义。 void main() { print('Hello, World!'); } 变量和常量 dart中的变量或常量必须先定义后使用 未被初始化的变量和常量默认值为null 变量 var 变量名[=值]; void main() { var var1 = '1'; print(var1); // 1 // var1 = 1; 这样是错误的, 声明且初始化会推断出类型,后面不能赋值其他类型的值 var var2; print(var2); // null var2 = 2; print(var2); // 2 var2 = '3'; print(var2); // 3 正确,声明时不赋值则等同于声明了一个动态类型(dynamic)变量 } 数据类型 变量名[=值]; 这种方式可以显式声明变量类型,以便做一些静态检查。 void main() { // 声明并初始化 int var1 = 1; print(var1); // 1 // 先声明后初始化 int var2; var2 = 1; print(var2); // 1 // var2 = '1'; 这是错误的,类型不匹配 } dynamic 变量名[=值] 这个dynamic意思是动态类型,这种变量可以随便给他赋什么类型的值 void main() { dynamic var1 = 1; var1 = '2'; // 动态类型可以赋任意类型的值 print(var1); // 2 } 常量 final 数据类型 常量名=值 在运行时确定其值, 可以作为类的普通成员变量 const 数据类型 常量名=值 必须在编译时确定其值,只能作为类的静态成员变量,不能作为普通成员变量 class User { final int var1=1; static const int var2=2; // const int var3 = 3; 错误,不能作为类的普通成员变量 } void main() { int var1 = 1; int var2 = 2; final int const1 = 1; const int const2 = 2; // const1 = 2; 错误,不能再改变 // const2 = 1; 错误,不能再改变 // final int const3; 错误,必须被初始化 // const int const4; 错误,必须被初始化 final int const5 = var1 + var2; // const int cont6 = var1+var2; 错误,必须在编译时可以确定值 } 数据类型 num、int、double int类型和double类型都是num类型 都是占8byte 是对象,有一些方法和属性 没有无符号类型 void main() { int int1 = 1; double double1 = 1.3; num num1 = 2.0; // 以下引发运行时异常 // int1 = num1; // print(int1); print(int1.toString()); // 1 print(int1.isEven); // false print(int1.floor()); // 1 print(int1.isNaN); // false print(double1.toString()); // 1.3 print(double1.floor()); // 1 print(double1.isNaN); // false } String 字面值表示法 单行字面值表示方法:'xx'或"xx" 多行字面值表示方法: '''xxx'''或"""xxx""" 忽略转译符: r'xx\nxx' void main() { String str1 = 'str1'; print(str1); // str1 String str2 = "str2"; print(str2); // str2 String str3 = '''a b c '''; print(str3); // a // b // c String str4 = "a\nb"; String str5 = r"a\nb"; print(str4); // a // b print(str5); // a\nb } 常用属性和方法 String 的属性和方法和其他语言类似,这里列举几个常用方法和属性 void main() { String str1 = "我和我的祖国"; String str2 = ",一刻也不能分割"; String str3 = str1 + str2; print(str1.length); // 6 print(str1.substring(2)); // 我的祖国 List list = str3.split(","); print(list); // [我和我的祖国, 一刻也不能分割] print(str1.startsWith("我")); // true print(str1.indexOf("我")); // 0 print(str1.lastIndexOf("我")); // 2 print(str1.replaceAll("我", "你")); // 你和你的祖国 print("a".compareTo("b")); // -1 print(str1.contains('祖国')); // true } bool 布尔值,字面值只能是true或false, 不能为其他 void main() { bool bool1 = true; print(bool1); // true } List 类似js中的数组, 长度可变。 void main() { List list = [1, 3, 4]; print(list.length); // 3 print(list); // [1, 3, 4] list.add(5); print(list); // [1, 3, 4, 5] list.addAll([6, 7]); print(list); // [1, 3, 4, 5, 6, 7] print(list.removeLast()); // 7 print(list); // [1, 3, 4, 5, 6] list.removeAt(0); print(list); // [3, 4, 5, 6] list.remove(3); print(list); // [4, 5, 6] list.sort((item1, item2) { return item2 - item1; }); print(list); // [6, 5, 4] list.forEach((item) { print(item); }); // 6 // 5 // 4 print(list.indexOf(4)); // 2 list.clear(); print(list); // [] } Set 表示集合,无重复值,加重复值不会报错,但是进不去,无序。 void main() { Set set = {1, 3, 3, 4}; print(set); // {1, 3, 4} set.add(9); print(set); // {1, 3, 4, 9} print(set.contains(3)); // true print(set.length); // 4 set.remove(3); print(set); // {1, 4, 9} print(set.isEmpty); // true } Map 表示k-v结构数据,key不能重。 void main() { Map map = { "a": 1, "b": 2, }; print(map); // {a: 1, b: 2} map['c'] = 3; print(map); // {a: 1, b: 2, c: 3} map.remove('a'); print(map); // {b: 2, c: 3} print(map.containsKey('a')); // false print(map.length); // 2 print(map.containsValue(3)); // true } enum 表示枚举,是一种特殊的类,官方文档没有将其纳入到基础类型,这里为了方便理解放到这里。 enum Color { red, blue, yellow, } void main() { Color c = Color.red; print(Color.blue); // Color.blue print(Color.values); // [Color.red, Color.blue, Color.yellow] print(c); // Color.red } 数据类型转换 java中有自动类型转换机制,例如可以将int型自动转为double;dart中类型不匹配时必须强转,没有自动类型转换 其他类型转String或数字之间相互转换 toXxx()方法 String转数字使用数字类(int,double,num)的parse()方法 void main() { int int1 = 1; // 错误,不能自动转换 // double double1= int1; // double 和int的相互转换 double double1 = int1.toDouble(); int1 = double1.toInt(); // num 是int 和 double的父类型, 可以直接转换 num num1 = double1; num1 = int1; // String 与double相互转换 String str1 = double1.toString(); double1 = double.parse(str1); // String 与int相互转换 String str2 = int1.toString(); int1 = int.parse(str2); // Map,Set,List用其toString方法可以转换为String Map map = {"a": 1, "b": 2}; List list = [1, 2, 3]; Set set = {1, 2, 3}; String str3 = map.toString(); list.toString(); set.toString(); } 操作符 操作符与其他语言基本相同,这里说明几个特殊的,其他简单罗列。 算术运算符 +、-、*、/、%、++、-- 比较特殊的是~/, 表示整除 void main() { int int1 = 3; print(int1 ~/ 2); // 1 } 比较运算符 ==、!=、>、<、<=、>= 赋值运算符 =、-=、~/=等等 比较特殊的是 变量名??=值, 表示如果左边变量是null则赋值,否则不赋值 void main() { int a; a ??= 1; print(a); // 1 int b = 2; b ??= 1; print(b); // 2 } 逻辑运算符 !、||、&& 位运算符 &、|、^、~expr、<<、>> 条件表达式 condition ? expr1 : expr2 级联运算符 这是比较特殊的运算符,类似于jquery中的连续的.操作, 上一次操作还是返回当前对象,因此可以继续调用其方法。大多语言没有这种操作。 class User { say() { print("say"); } run() { print("run"); } } void main() { User user = User(); user..say()..run(); // say // run } 类型运算符 这是比较特殊的运算符,用于判断类型和强转,类似java里边(User) instance这种 instance as className 用于将某个类型转换为其他类型,必须满足父子关系,否则报错。 import 'xxx' as variable; 用于指定导入库的别名,相当于将库里导出来的东西挂到一个map上,可以解决重名问题。 instance is classNmae 用于判断一个对象是否是某个类的实例 instance is! classNmae is 的结果取反 import 'dart:math' as math; // 这里导入库时使用as指定了别名 class Person { String name; Person(); say() { print("person say $name"); } } class User extends Person { String password; User(this.password); run() { print("user run"); } } void main() { print(math.max(4, 5)); // 指定别名的库调用 User u = User("password"); print(u is User); // true u.name = "name"; u.run(); // user run Person p = u as Person; // 通过as将子类型强转为父类型 print(p is User); // true print(p is Person); // true print(p is List); // fasle p.say(); // person say name // p.run(); // 错误,已经被转换成了Person, 不能再调用User的方法 } 流程控制语句 流程控制与其他语言相同,这里简单列举 if .. else if..else for while、do..while break、continue switch .. case assert assert表示断言,判断一个表达式的真假,若为假则抛出一个异常 模块化 dart支持模块化编程,可以导入dart内置库,其他三方库,也可以将自己的代码拆分成不同模块。 导入 导入内置库 import "dart:math"; 导入三方库 需要在项目描述文件pubspec.yaml(类似js中的package.json或者java的pom.xml)中声明你要依赖的库,然后安装,最后导入。 pubspec.yaml dependencies: http: ^0.12.0+2 安装 pub get 导入 import 'package:http/http.dart'; 导入本地文件 导入本地的文件后可以使用其中定义的类,变量,函数等 import '../common/Utils.dart'; 导入时指定别名 相当于将一个库导出的东西挂到一个对象,可以解决重名问题(上边讲as有提到) import 'package:http/http.dart' as http; 条件导入 可以只导入指定内容,或者不导入某些内容 import 'package:lib1/lib1.dart' show foo; import 'package:lib2/lib2.dart' hide foo; 动态导入或者按需加载 可以在运行时导入依赖,针对web应用,类似webpack的按需加载。 创建库 内容较多,此处略过,笔者也尚未研究,可参考官网 dart内置库介绍 dart:core dart核心内库,默认导入,类似java的java.lang包。提供内置数据类型等。 dart:async 异步支持库,大名鼎鼎的Future就在这里边。 dart:math 复杂数学计算相关 dart:convert json序列化、反序列化,字符集转换相关。 dart:html web应用相关api dart:io io相关,与发请求相关的HttpClient在这里边 函数 函数可以在顶层,也就是不属于任何一个类 函数可以是类的成员方法 可以有返回值也可以没有 函数是对象,可以赋值给变量,可以作为参数或返回值 参数可以是常见的位置参数,也可以是命名参数,命名参数传递时不必考虑顺序,命名参数大多数语言没有,听说是从oc借鉴过来的 只有一条语句的函数可以使用箭头函数 可以使用typedef定义特定的函数类型 函数参数可以指定默认值 // 没有返回值的函数 import 'package:flutter/foundation.dart'; void printInfo(String name, int age) { print('$name:$age'); } // 有返回值的函数 String getInfo(String name, int age) { return '$name:$age'; } // 函数作为参数传递 void excuteFn(var function, String name, int age) { function(name, age); } // 返回一个函数 Function getPrintInfoFn(int age) { return (String name) { print('$name:$age'); }; } // 函数体只有一条语句,可以使用箭头函数 void printInfo2(String name, int age) => print('$name:$age'); // 可以使用命名参数 void printInfo3({String name, int age}) { print('$name:$age'); } // 位置参数和命名参数混用,指定默认值 void printInfo4(String name, { int age}) { print('$name:$age'); } // 定义一种函数类型 typedef PrintFn = void Function(String name, int age); // 将特定类型的函数作为参数传递 void excuteFn2(PrintFn function, String name, int age) { function(name, age); } class User { // 函数作为类的成员方法 say() { print("hello"); } } void main() { printInfo("张三", 18); print(getInfo('李四', 18)); User user = User(); user.say(); excuteFn(printInfo, "王五", 18); Function fn1 = getPrintInfoFn(19); fn1("小明"); fn1("小花"); printInfo("小张", 18); printInfo2('小李', 20); printInfo3(name: "小华", age: 21); // 命名参数函数调用方式 printInfo4("AA"); printInfo4("aa", age: 30); excuteFn2(printInfo, "王五", 18); } 注释 单行注释 // 我是一行注释 多行注释 /** * 多行注释 * 多行注释 */ 文档注释 /// 文档注释 /// 文档注释 面向对象 类的定义与组成 类使用class关键字定义 一个类可以由如下部分组成: 普通成员变量 静态成员变量 普通方法 静态方法 构造函数,不声明的话默认有无参构造 命名构造函数 set,get方法 说明 不支持重载 静态成员不用实例化即可访问,只能通过类名访问,不像java可以通过实例访问静态成员变量 静态成员方法中不能使用this,且只能访问静态成员变量 不写set,get方法会有默认的set,get方法 不支持public、private等访问修饰符,一般约定_开头的变量为私有的,但是只是约定,实际还是可以访问到 // 使用class关键字定义类 class Rectangle { // 两个成员变量,变量名加_表示是私有的,只是一种约定,实质仍然能访问到 double _height; double _width; // 静态成员变量无需实例化即可通过类型访问 static String name = "长方形"; Rectangle(double width, double height) { this._width = width; this._height = height; } // 非命名构造方法若只是为成员变量赋值也可以这样写,与上边写法等价 // Rectangle(this._width, this._height); // 命名构造方法 Rectangle.fromMap(Map map) { this._width = map['width']; this._height = map['height']; } static String getName() { // return this._height.toString(); 错误, 静态成员方法不能使用this,不能使用非静态成员变量 return name; } double getArea() { return _height * _width; } // double getArea 已存在,不能这样写,不支持重载 // double getArea(double width, double height) { // // } // get set方法 double get width { print("调用 get width"); return _width; } set width(double value) { print("调用set width"); _width = value; } double get height => _height; set height(double value) { _height = value; } } void main() { print(Rectangle.name); print(Rectangle.getName()); Rectangle rectangle = Rectangle.fromMap({'width': 10, 'height': 20}); print(rectangle.getArea()); rectangle.width = 1; print(rectangle.width); } 继承 要点: 使用extends关键字 只支持单继承 子类可以获得父类的普通成员方法和普通成员变量,无法得到静态成员,构造方法也继承不到 可以重写父类的方法,可以使用super访问父类 如果父类没有无参构造的话,子类必须显示调用父类的构造,否则报错 // 使用extends继承 class Square extends Rectangle { // 默认会访问父类的无参构造,如果父类没有无参构造需要这样显式指定调用哪个构造 Square(double width):super(width, width); // 重写父类的方法 @override double getArea() { // 使用super访问父类的成员变量 print('重写父类方法: ${super._width}'); // 使用super访问父类的成员方法 return super.getArea(); } } void main() { // Square.getName(); 错误, 无法继承静态成员 // Square.name; 错误,无法继承静态成员 Square square = Square(10); print(square.getArea()); print(square._width); } 抽象类 要点: 在class前加abstract关键字定义抽象类 抽象类不能被实例化,只能被继承(后面会讲到,也可以被当成接口实现) 抽象类可以有抽象方法(不含方法体的方法),也可以有普通方法和成员变量 继承抽象类的类,要么重写其所有抽象方法,要么该类也要被被定义为抽象类,要么增加一个noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);方法 // 定义一个抽象类 abstract class Shape { // 抽象方法,没有方法体 double getArea(); double getPerimeter(); } class Rectangle extends Shape { double _height; double _width; Rectangle(double width, double height) { this._width = width; this._height = height; } Rectangle.fromMap(Map map) { this._width = map['width']; this._height = map['height']; } // 必须重写抽象类的抽象方法,否则需要也要声明为抽象类 @override double getArea() { return _height * _width; } // 必须重写抽象类的抽象方法 @override double getPerimeter() { return _height + _width; } } void main() { // Shape shape = Shape(); 错误,抽象类不能被实例化 Shape shape = Rectangle(10, 2); // 这样是可以的 print(shape.getArea()); print(shape.getPerimeter()); } 接口 关于接口,dart的设计比较奇怪。 官方文档对接口描述一下 每一个类都显式的声明了一个包含其成员变量和成员方法及他实现的接口的接口。如果你想创建一个支持B类API的类,实现B类就好了。 也即定义一个类就相当于定义了一个接口,可以直接去实现他,也可以实现多个接口。extends只能继承一个类,implements可以实现多个类(抱歉恨不厚道的说了实现一个类,这里应为接口)。 在上一个例子中直接写成下边这样也是可以的 class Rectangle implements Shape 泛型 dart提供了对泛型的支持,泛型可以增加程序的通用性,并可提供一些静态检查,减少了一些强制类型转换的工作。 // 定义一个泛型类 class Cache { Map _cache = {}; T getByKey(String key) { return _cache[key]; } // 泛型方法 void setByKey(String key, T value) { _cache[key] = value; } } // 做类型约束,必须是num的子类 class NumCache { Map _cache =