dart-logo

近来,flutter的热度在上升。flutter应用的主要开发语言是dart, 因此,欲练flutter, 必先了解dart.

dart是由google开发的编程语言,可用于开发移动应用,桌面应用,h5应用,后端服务。

本文将简单介绍flutter的语言特性、基础语法,以及在日常开发中非常实用的如何请求数据、如何处理异步、如何序列化与反序列化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

变量

  1. 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)变量
}
  1. 数据类型 变量名[=值];

这种方式可以显式声明变量类型,以便做一些静态检查。

void main() {
  // 声明并初始化
  int var1 = 1;
  print(var1); // 1

  // 先声明后初始化
  int var2;
  var2 = 1;
  print(var2); // 1

//  var2 = '1'; 这是错误的,类型不匹配
}
  1. dynamic 变量名[=值]

这个dynamic意思是动态类型,这种变量可以随便给他赋什么类型的值

void main() {
  dynamic var1 = 1;
  var1 = '2'; // 动态类型可以赋任意类型的值
  print(var1); // 2
}

常量

  1. final [数据类型] 常量名=值

在运行时确定其值, 可以作为类的普通成员变量

  1. 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; 错误,必须在编译时可以确定值
}

数据类型

  1. 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
}
  1. 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<String> 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
}
  1. bool

布尔值,字面值只能是true或false, 不能为其他

void main() {
  bool bool1 = true;
  print(bool1); // true
}
  1. List

类似js中的数组, 长度可变。

void main() {
  List<int> 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); // []
}
  1. Set

表示集合,无重复值,加重复值不会报错,但是进不去,无序。

void main() {
  Set<int> 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
}
  1. Map

表示k-v结构数据,key不能重。

void main() {
  Map<String, int> 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
}
  1. 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();
}

操作符

操作符与其他语言基本相同,这里说明几个特殊的,其他简单罗列。

  1. 算术运算符

+、-、*、/、%、++、–

比较特殊的是~/, 表示整除

void main() {
  int int1 = 3;
  print(int1 ~/ 2); // 1
}
  1. 比较运算符

==、!=、>、<、<=、>=

  1. 赋值运算符

=、-=、~/=等等

比较特殊的是 变量名??=值, 表示如果左边变量是null则赋值,否则不赋值

void main() {
  int a;
  a ??= 1;
  print(a); // 1

  int b = 2;
  b ??= 1;
  print(b); // 2
}
  1. 逻辑运算符

!、||、&&

  1. 位运算符

&、|、^、~expr、<<、>>

  1. 条件表达式

condition ? expr1 : expr2

  1. 级联运算符

这是比较特殊的运算符,类似于jquery中的连续的.操作, 上一次操作还是返回当前对象,因此可以继续调用其方法。大多语言没有这种操作。

class User {
  say() {
    print("say");
  }

  run() {
    print("run");
  }
}

void main() {
  User user = User();
  user..say()..run();
  // say
  // run
}
  1. 类型运算符

这是比较特殊的运算符,用于判断类型和强转,类似java里边(User) instance这种

  1. instance as className

用于将某个类型转换为其他类型,必须满足父子关系,否则报错。

  1. import ‘xxx’ as variable;

用于指定导入库的别名,相当于将库里导出来的东西挂到一个map上,可以解决重名问题。

  1. instance is classNmae

用于判断一个对象是否是某个类的实例

  1. 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内置库,其他三方库,也可以将自己的代码拆分成不同模块。

  1. 导入
  • 导入内置库
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的按需加载。

  1. 创建库

内容较多,此处略过,笔者也尚未研究,可参考官网

  1. 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);
}

注释

  1. 单行注释
// 我是一行注释
  1. 多行注释
/**
 * 多行注释
 * 多行注释
*/
  1. 文档注释
/// 文档注释
/// 文档注释

面向对象

类的定义与组成

类使用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<String, double> 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<String, double> 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<T> {
  Map<String, T> _cache = {};

  T getByKey(String key) {
    return _cache[key];
  }

  // 泛型方法
  void setByKey(String key, T value) {
    _cache[key] = value;
  }
}

// 做类型约束,必须是num的子类
class NumCache<T extends num> {
  Map<String, T> _cache = {};

  T getByKey(String key) {
    return _cache[key];
  }

  void setByKey(String key, T value) {
    _cache[key] = value;
  }
}

void main() {
  Cache<String> cache = Cache();
  cache.setByKey("a", "1");
//  cache.setByKey("b", 2); 错误,类型不匹配
  print(cache.getByKey("a") is String); // 类型被保持, 输出true

//  NumCache<String> numCache = NumCache(); 错误String不是num的子类
  NumCache<int> numCache = NumCache();
  numCache.setByKey('a', 1);
}

异常

要点:

  • 无需显式指定抛出哪些异常,这与java不同,java需要使用throws 异常类
  • 不是必须对异常处理,java如果有throws那么必须捕获
  • 有Error、Exception及其子类作为异常类,也可自定义
  • 抛出异常时可以抛出异常类,也可以抛出非null的任何对象
  • 可以使用try catch 处理异常,也可以再次向上抛出
// 抛出一个异常实例
void testThrow() {
  throw Exception("异常发生");
}

// 可以抛出一个任意对象
void testThrow2() {
  throw "抛出字符串也可以";
}

void testThrow3() {
  try {
    testThrow();
  } catch(e) {
    // 捕获到异常不处理,再次向上抛出
    rethrow;
  }
}

void main() {
  try {
    testThrow();
  } catch(e, s) { // 这里e是指抛出的异常类,s是堆栈信息
    print(e is Exception); // true
    print(s);
  }

  try {
    testThrow2();
  } catch(e) {
    print(e is Exception); // false
    print(e); // 抛出字符串也可以
  }
  
  testThrow3(); // 这里有异常抛出,不捕获也是可以通过静态检查的
  // 下面的语句无法执行
  print("未捕获异常之后还能执行吗,不能");
}

异步编程

dart提供了对异步的支持,核心类包括Future和Stream,都位于dart:async包下,这里介绍非常常用的Future.

Future被用来表示在未来才能知道其值或错误的一个类,和js中的Promise是类似的。

  1. Future的创建

可以使用Future的构造方法创建Future

  • Future Future(FutureOr computation()) 可以异步的执行computation函数,其结果就是Future的结果
  • Future Future.delayed(Duration duration, [ FutureOr computation() ]) 延迟去执行里边的函数
  • Future Future.error(Object error, [ StackTrace stackTrace ]) 延迟抛出一个错误
  • Future.sync(FutureOr computation()) 立即执行
  • Future Future.microtask(FutureOr computation()) 以微任务方式执行
  • Future Future.value([FutureOr value ]) 参数是一个Future,直接以参数返回值为值作为返回Future的返回值

这几个构造方法使用比较简单,一下是简单示例

// 使用非命名构造
Future<String> getFuture() {
  return Future<String>(() {
    return "hello";
  });
}

// Future.delayed
Future<String> getFuture2() {
  return Future.delayed(Duration(seconds: 2), () {
    return "Future.delayed";
  });
}

// Future.error
Future<String> getFuture3() {
  return Future.error("错误对象");
}

// Future.microtask
Future<String> getFuture4() {
  return Future.microtask(() {
    return "Future.microtask";
  });
}

// Future.sync
Future<String> getFuture5() {
  return Future.sync(() {
    return "Future.sync";
  });
}

Future.value(getFuture4());

关于delayed, microtask,sync对比参见后文

  1. Future 结果的获取
  • 如果一个函数返回Future, 那么可以使用async await 获取其结果,可以使用try catch 捕获异常
  • 可以使用.then获取正常结果,.catchError获取抛出的异常
import 'dart:async';

// 使用非命名构造
Future<String> getFuture() {
  return Future<String>(() {
    return "hello";
  });
}

void method1() {
  getFuture().then((res) {
    print(res);
  }).catchError((e) {
    print(e);
  });
  print("后边的先执行了");
}

void method2() async {
  String str = await getFuture();
  print(str);
  print('按顺序执行');
}

void main(){
  method1();
  method2();
  // 输出
  // 后边的先执行了
  // hello
  // hello
  // 按顺序执行
}
  1. Future执行顺序比较

通过测试得出:

  • microtask 和async是写在前边的先执行
  • 直接使用构造函数的在最后执行
  • delayed在microtask和async之后执行

关于这几者的比较,有兴趣的同学可以自行研究。

网络请求

常用的发网络请求的方法有两种:

  • 一是使用dart:io下的HttpClient
  • 二是使用package:http/http.dart

HttpClient

使用步骤:

  • 创建HttpClient
  • [可选]配置HttpClient,比如设置超时时间,代理等
  • 获取HttpClientRequest
  • [可选]设置httpClientRequest,如header
  • 获取httpClientResponse
  • 处理响应结果
  • 关闭连接
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/cupertino.dart';

// 这里封装了一个发get请求的方法,将返回的json字符串反序列化
Future<Map<String, dynamic>> httpGet(
    {@required String url,
    Map<String, dynamic> headers = const {},
    Map<String, dynamic> params = const {},
    int timeout = 10}) async {
  // 创建HttpClient实例
  HttpClient client = HttpClient();
  // 我在本地启动一个代理服务,这样方便抓包,没有代理服务可以去掉
  client.findProxy = (uri) {
    // 如果需要过滤uri,可以手动判断
    return "PROXY 127.0.0.1:8888";
  };
  // 超时时间设置
  client.connectionTimeout = Duration(seconds: timeout);
  // 通过url(包括协议 主机 路径)创建Uri对象, 便于获取url的各个部分
  Uri uri = Uri.parse(url);
  // 使用getUrl创建 HttpClientRequest对象
  HttpClientRequest httpClientRequest = await client.getUrl(Uri(
      scheme: uri.scheme,
      host: uri.host,
      path: uri.path,
      queryParameters: params));
  // 加上自定义header
  headers.forEach((key, value) {
    httpClientRequest.headers.add(key, value);
  });

  // 调用HttpClientRequest的close得到HttpClientResponse
  HttpClientResponse httpClientResponse = await httpClientRequest.close();
  // 使用特定字符集解码
  var str = await httpClientResponse.transform(utf8.decoder).join();
  // 将字符串序列化为Map, 这个将在后文详解
  Map<String, dynamic> map = jsonDecode(str);
  // 关闭连接
  client.close(force: false);
  return map;
}

void main() async {
  Map<String, dynamic> map = await httpGet(
      url: 'http://t.weather.sojson.com/api/weather/city/101030100',
      headers: <String, dynamic>{
        'custom_header': 'customheader',
      },
      params: <String, dynamic>{
        "version": 'v1',
        'cityid': '101120201',
        'city': '青岛'
      });

  print(map);
}

以上代码简单封装了一个发get请求的方法,get请求的参数是带在url上的,而post则不同,post的参数通常是在body中,下面演示如何发一个post请求, 数据格式采用json, 表单格式的类似,修改application即可。

void main() async {
  HttpClient client = HttpClient();
  // 本地搭了一个服务,用于测试
  HttpClientRequest httpClientRequest = await client.postUrl(Uri.parse('http://127.0.0.1:3000/users/save'));

  // 关键步骤,告诉服务器请求参数格式
  httpClientRequest.headers.contentType
  = new ContentType("application", "json", charset: "utf-8");
  // 关键步骤,写数据到body
  httpClientRequest.write(jsonEncode({"name":'lily', 'age': 20}));
  HttpClientResponse httpClientResponse = await httpClientRequest.close();
  var str = await httpClientResponse.transform(utf8.decoder).join();
  Map<String, dynamic> map = jsonDecode(str);
  print(map);
}

还有关于上传文件的内容,本文篇幅过长了,后面再写一个。

http

我们发现使用原生的HttpClient来发请求还是比较麻烦的,我们可以使用package:http/http.dart这个包可以大大简化开发。包安装可以参考上文模块化部分

import 'package:http/http.dart' as http;

void main() async {
  // 演示post请求
  var url = 'http://127.0.0.1:3000/users/save';
  var response = await http.post(url, body: {'name': 'lily', 'age': '20'});
  print('Response status: ${response.statusCode}');
  print('Response body: ${response.body}');

  // 演示get请求
  var response2 = await http.get('http://127.0.0.1:3000?a=1',);
  print(response2.body);

}

可以发现使用package:http/http.dart极其方便。

json序列化与反序列化

json是一种非常灵活的数据交换格式。

json序列化与反序列化通俗讲就是将 语言定义的非字符串类型(比如java中的实体类,dart中的Map)转换为json字符串,便于传输和存储,这是序列化;反序列化则是这个逆过程,方便操作数据。

在java中一般使用阿里的fastjson做json的序列化与反序列化;

在js使用JSON.stringify()、JSON.parse()做序列化与反序列化。

在dart中json序列化与反序列化有两种方式:

  • 一是 使用内置库dart:convert的jsonDecode和jsonEncode
  • 二是 使用package:json_serializable,这个库跟下问所说的第二种方法差不多,只不过他会自动帮你生成下边会讲到的那些样板代码,这里不进行详细介绍了。

使用dart:convert

  1. 不关注类型,直接用动态类型,这种方式会导致无法错类型检查,可能不安全,好处是简单,不用写实体类
import 'dart:convert';

void main() async {
  String jsonString = '[{"name":"name1","age":1},{"name":"name2","age":2}]';
  List list = jsonDecode(jsonString);

  list.forEach((item) {
    print(item['name']);
  });
  String jsonString2 = jsonEncode(list);
  print(jsonString2);
}
  1. 使用类型转换,在实体类定义一些转换规则,好处是可以有静态类型检查,缺点是比较麻烦
import 'dart:convert';

class User {
  String name;
  int age;

  User(this.name, this.age);

  // 将map转为User
  User.fromJson(Map<String, dynamic> json) {
    name = json['name'];
    age = json['age'];
  }

  // 将User转成map
  Map<String, dynamic> toJson() =>
      {
        'name': name,
        'age': age,
      };
}

void main() async {

  List<User> list = List();

  list.add(User('name1', 1));
  list.add(User('name2', 2));

  // 这里会自动调用toJson
  String str = jsonEncode(list);
  print(str.runtimeType); // String
  print(str); // [{"name":"name1","age":1},{"name":"name2","age":2}]

  // 这里希望反序列化为List<User> 但是尚未找到方法,
  // 网上文档基本就是翻译翻译官方文档,官网也没找到解决方案
  List userList = jsonDecode(str);

  userList.forEach((item) {
    // 这里不得不手动强转
    User u = User.fromJson(item);
    print(u.name);
  });

  // name1
  // name2
}

因为之前写js的原因,更倾向使用第一种方式,因为不用写实体类;而且第二种方案即使转换成特定类型,除了可以使用.直接访问到属性,也没太大意义。js没有这种定义实体类的习惯,照样玩耍。

最后的话

本以为三言两语就可以说完dart, 最后发现竟然写了1300多行,而且还省略了一些琐碎的东西(比如dart的集合框架)。

祝大家flutter入坑快乐!~

最后献上文章开始提到的小福利。图片较小,可下载源文件查看,该思维导图源文件下载地址参见思维导图, 如果可以的话,顺手帮忙点个星星啊☺

dart基础语法思维导图

参考文献

  • https://flutter.dev/docs/development/data-and-backend/json#manual-encoding
  • https://dart.dev/guides/language/language-tour

版权声明:本文为floor原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/floor/p/12070991.html