本文將簡單介紹flutter的語言特性、基礎語法,以及在日常開發中非常實用的如何請求數據、如何處理非同步、如何序列化與反序列化json等技能。 ...
近來,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<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
}
- bool
布爾值,字面值只能是true或false, 不能為其他
void main() {
bool bool1 = true;
print(bool1); // true
}
- 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); // []
}
- 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
}
- 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
}
- 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<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是類似的。
- 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對比參見後文
- 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
// 按順序執行
}
- 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
- 不關註類型,直接用動態類型,這種方式會導致無法錯類型檢查,可能不安全,好處是簡單,不用寫實體類
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);
}
- 使用類型轉換,在實體類定義一些轉換規則,好處是可以有靜態類型檢查,缺點是比較麻煩
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入坑快樂!~
最後獻上文章開始提到的小福利。圖片較小,可下載源文件查看,該思維導圖源文件下載地址參見思維導圖, 如果可以的話,順手幫忙點個星星啊☺
參考文獻
- https://flutter.dev/docs/development/data-and-backend/json#manual-encoding
- https://dart.dev/guides/language/language-tour