FLutter瞭解之文件操做、模型轉換、網絡請求

目錄
  1. 文件操做
  2. Json轉Model
  3. HttpClient
  4. dio三方庫
  5. Http分塊下載
  6. 使用WebSockets
  7. 使用Socket API(dart:io包中)
  8. http三方庫

1. 文件操做html

不管是Flutter仍是DartVM下的腳本(系統路徑不一樣,Dart VM運行在PC或服務器操做系統下,Flutter運行在移動操做系統下)都經過Dart IO庫來操做文件。android

在實際開發中,若是存儲一些簡單的數據,推薦使用shared_preferences插件。git

獲取目錄位置github

Android和iOS的應用存儲目錄不一樣,PathProvider三方插件提供了一種平臺透明的方式來訪問設備文件系統上的經常使用位置。web

 

1. 臨時目錄: getTemporaryDirectory()  
  系統可隨時清除的臨時目錄(存放緩存文件)。
  在iOS上,這對應於NSTemporaryDirectory() 返回的值。
  在Android上,這是getCacheDir())返回的值。

2. 文檔目錄: getApplicationDocumentsDirectory()
  當應用程序被卸載時,系統纔會清除該目錄。
  在iOS上,這對應於NSDocumentDirectory。
  在Android上,這是AppData目錄。

3. 外部存儲目錄(SD卡):getExternalStorageDirectory();
  在iOS下調用該方法會拋出UnsupportedError異常(iOS不支持外部目錄)。
  在Android下結果是android SDK中getExternalStorageDirectory的返回值。

算法

一個計數器,應用退出重啓後能夠恢復點擊次數。shell

 

1. 引入PathProvider插件;在pubspec.yaml文件中添加以下聲明:
path_provider: ^0.4.1
添加後,執行flutter packages get 獲取一下


2.完整代碼以下:
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:async';

void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home:MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  
  @override
  void initState() {
    super.initState();
    _readCounter().then((int value) {
      setState(() {
        _counter = value;
      });
    });
  }
  
  // 從文件讀取點擊次數
  Future<int> _readCounter() async {
    try {
      File file = await _getLocalFile();
      // 讀取點擊次數(以字符串)
      String contents = await file.readAsString();
      return int.parse(contents);
    } on FileSystemException {
      return 0;
    }
  }
  // 獲取文件
  Future<File> _getLocalFile() async {
    // 獲取應用的Documents目錄
    String dir = (await getApplicationDocumentsDirectory()).path;
    return new File('$dir/counter.txt');
  }

  // 點擊按鈕後自增,並將點擊次數以字符串類型寫到文件中
  Future<Null> _incrementCounter() async {
    setState(() {
      _counter++;
    });
    await (await _getLocalFile()).writeAsString('$_counter');
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text('文件操做')),
      body: new Center(
        child: new Text('點擊了 $_counter 次'),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }
}

2. Json轉Modelmacos

Flutter中沒有像Java開發中的Gson/Jackson同樣的Json序列化類庫,由於這樣的庫須要使用運行時反射,這在Flutter中是禁用的, 因此Flutter沒法實現動態轉化Model的功能。運行時反射會干擾Dart的tree shaking,使用tree shaking,能夠在release版中「去除」未使用的代碼,這能夠顯著優化應用程序的大小。因爲反射會默認應用到全部代碼,所以tree shaking會很難工做,由於在啓用反射時很難知道哪些代碼未被使用,所以冗餘代碼很難剝離。編程

  1. 手動序列化和反序列化 (適合小項目)

須要導入dart:convert庫json

json.decode(jsonStr) :將JSON格式的字符串轉爲Dart對象(List或Map)。
json.ecode(list) :將Dart對象轉爲JSON格式的字符串。

 

例

// 導入庫
import 'dart:convert'
// 一個JSON格式的用戶列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';
// json字符串轉爲Dart對象
List items=json.decode(jsonStr);  // items[0]["name"]
// Dart對象轉爲json字符串
String jsonString=json.ecode(items);

弊端

 

json.decode() 沒有外部依賴或其它的設置,對於小項目很方便。但當項目變大時,這種手動編寫序列化邏輯可能變得難以管理且容易出錯

String json='{"name": "John Smith","email": "john@example.com"}';
Map<String, dynamic> user = json.decode(json);
print(' ${user['name']}');
print(' ${user['email']}');
因爲json.decode()僅返回一個Map<String, dynamic>,這意味着直到運行時才知道值的類型。 經過這種方法,失去了大部分靜態類型語言特性:類型安全、自動補全和最重要的編譯時異常。這樣一來,代碼可能會變得很是容易出錯。例如,當訪問name或email字段時,輸入錯誤的字段名,因爲這個JSON在map結構中,因此編譯器不知道這個錯誤的字段名,因此編譯時不會報錯。

其實,這個問題在不少平臺上都會遇到,而也早就有了好的解決方法即「Json Model化」,具體作法就是,經過預約義一些與Json結構對應的Model類,而後在請求到數據後再動態根據數據建立出Model類的實例。

在模型類中序列化JSON

 

能夠經過引入一個簡單的模型類來解決前面提到的問題。
在User類內部有:
    1. 一個User.fromJson 構造函數, 用於從一個map構造出一個 User實例。
    2. 一個toJson 方法, 將 User 實例轉化爲一個map.
這樣,調用代碼如今能夠具備類型安全、自動補全字段以及編譯時異常。若是將拼寫錯誤字段視爲int類型而不是String, 那麼代碼就不會經過編譯,而不是在運行時崩潰。

user.dart文件
class User {
  final String name;
  final String email;
  User(this.name, this.email);
  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];
  Map<String, dynamic> toJson() =>
    <String, dynamic>{
      'name': name,
      'email': email,
    };
}

如今,序列化和反序列化的邏輯移到了模型自己內部。
這樣,調用代碼就不用擔憂JSON序列化了,可是Model類仍是必須的。
在實踐中,User.fromJson和User.toJson方法都須要單元測試到位,以驗證正確的行爲。

反序列化
Map userMap = json.decode(json);
var user = new User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');

序列化
// 只是將該User對象傳遞給該json.encode方法。不須要手動調用toJson這個方法,由於JSON.encode內部會自動調用。
String json = json.encode(user);
  1. 經過代碼生成自動序列化和反序列化(大中型項目)

 

代碼生成功能的JSON序列化是指經過外部庫自動生成序列化模板。
須要一些初始設置,並運行一個文件觀察器,從model類生成代碼。
若是訪問JSON字段時拼寫錯誤,會在編譯時捕獲。
缺點:生成的源文件可能會在項目導航器會顯得混亂。

json_serializable三方庫(官方推薦): 一個自動化的源代碼生成器,能夠在開發階段生成JSON序列化模板。

第一步:pubspec.yaml文件(添加依賴包並下載)

 

dependencies:
  json_annotation: ^2.0.0
dev_dependencies:
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

運行 flutter packages get

第二步:user.dart文件(以json_serializable的方式建立model類)

 

import 'package:json_annotation/json_annotation.dart';
// user.g.dart文件 會在運行生成命令後自動生成。此處必須先寫上。
part 'user.g.dart';
/// 這個標註是告訴生成器,這個類是須要生成Model類的
@JsonSerializable()
class User{
  String name;
  String email;
  User(this.name, this.email);
  // 忽略這裏的錯誤,$UserFromJson、_$UserToJson會在下面的步驟中在user.g.dart文件中自動生成。
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);  
  Map<String, dynamic> toJson() => _$UserToJson(this);  
}

 

自定義命名策略

例如,若是正在使用的API返回帶有snake_case的對象,但想在模型中使用lowerCamelCase, 那麼可使用@JsonKey標註:
// 顯式關聯JSON字段名與Model屬性的對應關係 
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;

第三步:運行代碼生成器,爲Model自動生成json序列化代碼

 

方式1. 一次性生成
flutter packages pub run build_runner build
在項目根目錄下運行,該命令經過源文件找出須要生成Model類的源文件(包含@JsonSerializable標註的)來生成對應的.g.dart文件。


方式2. 持續生成
flutter packages pub run build_runner watch
在項目根目錄下運行,該命令會啓動watcher,watcher會監視項目中文件的變化,並在須要時自動構建必要的文件。

上面的方法有一個最大的問題就是要爲每個json寫模板。
解決:自動化生成模板。用dart實現一個腳本或者使用IDE插件,將JSON文本轉換爲模板。

  1. 自動化生成模板(腳本)

 

1. 定義一個"模板的模板",命名爲"template.dart"。
模板中的「%t」、「%s」爲佔位符,將在腳本運行時動態被替換爲合適的導入頭和類名。

import 'package:json_annotation/json_annotation.dart';
%t
part '%s.g.dart';
@JsonSerializable()
class %s {
    %s();

    %s
    factory %s.fromJson(Map<String,dynamic> json) => _$%sFromJson(json);
    Map<String, dynamic> toJson() => _$%sToJson(this);
}

 

2. 寫一個自動生成模板的腳本(mo.dart),它能夠根據指定的JSON目錄,遍歷生成模板,在生成時定義一些規則:
    1. 若是JSON文件名如下劃線「_」開始,則忽略此JSON文件。
    2. 複雜的JSON對象每每會出現嵌套,能夠經過一個特殊標誌來手動指定嵌套的對象。

import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
const TAG="\$";
const SRC="./json"; //JSON 目錄
const DIST="lib/models/"; //輸出model目錄

void walk() { // 遍歷JSON目錄生成模板
  var src = new Directory(SRC);
  var list = src.listSync();
  var template=new File("./template.dart").readAsStringSync();
  File file;
  list.forEach((f) {
    if (FileSystemEntity.isFileSync(f.path)) {
      file = new File(f.path);
      var paths=path.basename(f.path).split(".");
      String name=paths.first;
      if(paths.last.toLowerCase()!="json"||name.startsWith("_")) return ;
      if(name.startsWith("_")) return;
      //下面生成模板
      var map = json.decode(file.readAsStringSync());
      //爲了不重複導入相同的包,咱們用Set來保存生成的import語句。
      var set= new Set<String>();
      StringBuffer attrs= new StringBuffer();
      (map as Map<String, dynamic>).forEach((key, v) {
          if(key.startsWith("_")) return ;
          attrs.write(getType(v,set,name));
          attrs.write(" ");
          attrs.write(key);
          attrs.writeln(";");
          attrs.write("    ");
      });
      String  className=name[0].toUpperCase()+name.substring(1);
      var dist=format(template,[name,className,className,attrs.toString(),
                                className,className,className]);
      var _import=set.join(";\r\n");
      _import+=_import.isEmpty?"":";";
      dist=dist.replaceFirst("%t",_import );
      //將生成的模板輸出
      new File("$DIST$name.dart").writeAsStringSync(dist);
    }
  });
}

String changeFirstChar(String str, [bool upper=true] ){
  return (upper?str[0].toUpperCase():str[0].toLowerCase())+str.substring(1);
}

//將JSON類型轉爲對應的dart類型
 String getType(v,Set<String> set,String current){
  current=current.toLowerCase();
  if(v is bool){
    return "bool";
  }else if(v is num){
    return "num";
  }else if(v is Map){
    return "Map<String,dynamic>";
  }else if(v is List){
    return "List";
  }else if(v is String){ //處理特殊標誌
    if(v.startsWith("$TAG[]")){
      var className=changeFirstChar(v.substring(3),false);
      if(className.toLowerCase()!=current) {
        set.add('import "$className.dart"');
      }
      return "List<${changeFirstChar(className)}>";

    }else if(v.startsWith(TAG)){
      var fileName=changeFirstChar(v.substring(1),false);
      if(fileName.toLowerCase()!=current) {
        set.add('import "$fileName.dart"');
      }
      return changeFirstChar(fileName);
    }
    return "String";
  }else{
    return "String";
  }
 }

//替換模板佔位符
String format(String fmt, List<Object> params) {
  int matchIndex = 0;
  String replace(Match m) {
    if (matchIndex < params.length) {
      switch (m[0]) {
        case "%s":
          return params[matchIndex++].toString();
      }
    } else {
      throw new Exception("Missing parameter for string format");
    }
    throw new Exception("Invalid format string: " + m[0].toString());
  }
  return fmt.replaceAllMapped("%s", replace);
}

void main(){
  walk();
}

 

3. 寫一個shell(mo.sh),先生成Model,再爲Model自動生成json序列化代碼。

    dart mo.dart
    flutter packages pub run build_runner build --delete-conflicting-outputs

 

4. 在根目錄下新建一個json目錄,而後把user.json移進去,而後在lib目錄下建立一個models目錄,用於保存最終生成的Model類。
如今只須要一句命令便可生成Model類了:

./mo.sh

嵌套JSON的狀況

 

一個person.json內容以下:
{
  "name": "John Smith",
  "email": "john@example.com",
  "mother":{
    "name": "Alice",
    "email":"alice@example.com"
  },
  "friends":[
    {
      "name": "Jack",
      "email":"Jack@example.com"
    },
    {
      "name": "Nancy",
      "email":"Nancy@example.com"
    }
  ]
}

每一個Person都有name 、email 、 mother和friends四個字段,因爲mother也是一個Person,朋友是多個Person(數組),因此指望生成的Model是下面這樣:

import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable()
class Person {
    String name;
    String email;
    Person mother;
    List<Person> friends;
    Person();
    factory Person.fromJson(Map<String,dynamic> json) => _$PersonFromJson(json);
    Map<String, dynamic> toJson() => _$PersonToJson(this);
}


這時,只須要簡單修改一下JSON,添加一些特殊標誌,從新運行mo.sh便可:
{
  "name": "John Smith",
  "email": "john@example.com",
  "mother":"$person",
  "friends":"$[]person"
}

腳本在遇到特殊標誌符後會先把相應字段轉爲相應的對象或對象數組。若是與內容衝突,能夠修改mo.dart中的TAG常量,自定義標誌符。
  1. 對象使用 $
  2. 對象數組使用 $[] 後跟具體類型名

若是每一個項目都手動構建一個這樣的腳本顯然很麻煩,爲此,將腳本和生成模板封裝成一個包(已經發布到了Pub上,包名爲Json_model),開發者直接添加該依賴包,即可以用一條命令根據Json文件生成Dart類。

  1. 自動化生成模板(IDE插件)

 

IDE插件和Json_model對比:

1. Json_model須要單獨維護一個存放Json文件的文件夾,若是有改動,只需修改Json文件即可從新生成Model類;而IDE插件通常須要用戶手動將Json內容拷貝複製到一個輸入框中,這樣生成以後Json文件沒有存檔的話,以後要改動就須要手動。
2. Json_model能夠手動指定某個字段引用的其它Model類,能夠避免生成重複的類;而IDE插件通常會爲每個Json文件中全部嵌套對象都單獨生成一個Model類,即便這些嵌套對象可能在其它Model類中已經生成過。
3. Json_model 提供了命令行轉化方式,能夠方便集成到CI等非UI環境的場景。

3. 經過HttpClient發起HTTP請求(Dart IO庫提供)

支持GET、POST、PUT、DELETE等經常使用http操做

 

import 'dart:io';

// 使用HttpClient發起請求分爲五步
get() async {

  // 1. 建立一個HttpClient
  var httpClient = new HttpClient();
  // 2. 建立URL
  var uri = new Uri.http(
      'example.com', '/path1/path2', {'param1': '42', 'param2': 'foo'});
/*
  var uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
    "xx":"xx",
    "yy":"dd"
  });
*/
  // 3. 發送請求
  var request = await httpClient.getUrl(uri);
/*
  // 設置請求header
  request.headers.add("user-agent", "test");
  // post時設置請求體
  request.add(utf8.encode("hello world")); 
  //request.addStream(_inputStream); //能夠直接添加輸入流
*/
  // 4. 獲取響應
  var response = await request.close();
  // 解析響應內容
  var responseBody = await response.transform(UTF8.decoder).join();
}

  // 5. 關閉client(經過該client發起的全部請求都會停止)
  httpClient.close();

 

import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:convert';

void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home:MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _ipAddress = '未知';

  _getIPAddress() async {
    //
    var httpClient = new HttpClient();
    //
    var url = 'https://httpbin.org/ip';
    String result;
    try {
      //
      var request = await httpClient.getUrl(Uri.parse(url));
      //
      var response = await request.close();
      if (response.statusCode == HttpStatus.ok) {
        //
        var json = await response.transform(utf8.decoder).join();
        //
        var data = jsonDecode(json);
        result = data['origin'];
      } else {
        result =
        '獲取IP地址失敗:\nHttp status ${response.statusCode}';
      }
    } catch (exception) {
      result = '獲取IP地址失敗';
    }
    
    // 組件沒有被移除時更新UI
    if (!mounted) return;
    setState(() {
      _ipAddress = result;
    });
  }

  @override
  Widget build(BuildContext context) {
    var spacer = new SizedBox(height: 32.0);

    return new Scaffold(
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text('當前IP地址:'),
            new Text('$_ipAddress.'),
            spacer,
            new RaisedButton(
              onPressed: _getIPAddress,
              child: new Text('獲取IP地址'),
            ),
          ],
        ),
      ),
    );
  }
}

例2

 

點擊「獲取百度首頁」按鈕後,會請求百度首頁,請求成功後,將返回內容顯示出來並在控制檯打印響應header

import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:convert';

void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _loading = false;
  String _text = "";

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(),
      child: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            RaisedButton(
                child: Text("獲取百度首頁"),
                onPressed: _loading
                    ? null
                    : () async {
                        setState(() {
                          _loading = true;
                          _text = "正在請求...";
                        });
                        try {
                          //
                          HttpClient httpClient = new HttpClient();
                          //
                          HttpClientRequest request = await httpClient
                              .getUrl(Uri.parse("https://www.baidu.com"));
                          // user-agent
                          request.headers.add("user-agent",
                              "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
                          //
                          HttpClientResponse response = await request.close();
                          //
                          _text = await response.transform(utf8.decoder).join();
                          // 響應頭
                          print(response.headers);

                          //關閉client後,經過該client發起的全部請求都會停止。
                          httpClient.close();
                        } catch (e) {
                          _text = "請求失敗:$e";
                        } finally {
                          setState(() {
                            _loading = false;
                          });
                        }
                      }),
            Container(
                width: MediaQuery.of(context).size.width - 50.0,
                child: Text(_text.replaceAll(new RegExp(r"\s"), "")))
          ],
        ),
      ),
    );
  }
}



connection: Keep-Alive
cache-control: no-cache
set-cookie: ....  //有多個,省略...
transfer-encoding: chunked
date: Tue, 30 Oct 2018 10:00:52 GMT
content-encoding: gzip
vary: Accept-Encoding
strict-transport-security: max-age=172800
content-type: text/html;charset=utf-8
tracecode: 00525262401065761290103018, 00522983

HttpClient配置

HttpClient提供的這些屬性和方法最終都會做用在請求header裏,也能夠直接去設置header。
不一樣的是經過HttpClient設置的對整個httpClient都生效,而經過HttpClientRequest設置的只對當前請求生效。

 

idleTimeout 
對應請求頭中的keep-alive字段值
爲了不頻繁創建鏈接,httpClient在請求結束後會保持鏈接一段時間,超過這個閾值後纔會關閉鏈接。

connectionTimeout   
和服務器創建鏈接的超時,若是超過這個值則會拋出SocketException異常。

maxConnectionsPerHost   
同一個host,同時容許創建鏈接的最大數量。

autoUncompress  
對應請求頭中的Content-Encoding
若是設置爲true,則請求頭中Content-Encoding的值爲當前HttpClient支持的壓縮算法列表,目前只有"gzip"

userAgent   
對應請求頭中的User-Agent字段。

HTTP請求認證(Authentication)

Http協議的認證機制能夠用於保護非公開資源。若是Http服務器開啓了認證,那麼用戶在發起請求時就須要攜帶用戶憑據。

若是在瀏覽器中訪問了啓用Basic認證的資源時,瀏覽就會彈出一個登陸框。
除了Basic認證以外還有:Digest認證、Client認證、Form Based認證等,目前Flutter的HttpClient只支持Basic和Digest兩種認證方式,這兩種認證方式最大的區別是發送用戶憑據時,對於用戶憑據的內容,前者只是簡單的經過Base64編碼(可逆),然後者會進行哈希運算,相對來講安全一點,可是爲了安全起見,不管是採用Basic認證仍是Digest認證,都應該在Https協議下,這樣能夠防止抓包和中間人攻擊。

 

Basic認證的基本過程:
    1. 客戶端發送http請求給服務器,服務器驗證該用戶是否已經登陸驗證過了,若是沒有的話, 服務器會返回一個401 Unauthozied給客戶端,而且在響應header中添加一個 「WWW-Authenticate」 字段,例如:
    WWW-Authenticate: Basic realm="admin"
    其中"Basic"爲認證方式,realm爲用戶角色的分組,能夠在後臺添加分組。
    2. 客戶端獲得響應碼後,將用戶名和密碼進行base64編碼(格式爲用戶名:密碼),設置請求頭Authorization,繼續訪問 :
    Authorization: Basic YXXFISDJFISJFGIJIJG
    服務器驗證用戶憑據,若是經過就返回資源內容。

HttpClient關於Http認證的方法和屬性

 

若是全部請求都須要認證,那麼應該使用方法1: 在HttpClient初始化時就調用addCredentials()來添加全局憑證,而不是方法2: 去動態添加。

1. addCredentials(Uri url, String realm, HttpClientCredentials credentials)
    該方法用於添加用戶憑據,如:
    httpClient.addCredentials(_uri,
     "admin", 
      new HttpClientBasicCredentials("username","password"), //Basic認證憑據
    );

    若是是Digest認證,能夠建立Digest認證憑據:
    HttpClientDigestCredentials("username","password")

2. authenticate(Future<bool> f(Uri url, String scheme, String realm))
    這是一個setter,類型是一個回調,當服務器須要用戶憑據且該用戶憑據未被添加時,httpClient會調用此回調,在這個回調當中,通常會調用addCredential()來動態添加用戶憑證,例如:
    httpClient.authenticate=(Uri url, String scheme, String realm) async{
      if(url.host=="xx.com" && realm=="admin"){
        httpClient.addCredentials(url,
          "admin",
          new HttpClientBasicCredentials("username","pwd"), 
        );
        return true;
      }
      return false;
    };

代理

 

能夠經過findProxy來設置代理策略

有時代理服務器也啓用了身份驗證,這和http協議的認證是類似的,HttpClient提供了對應的Proxy認證方法和屬性:
set authenticateProxy(
    Future<bool> f(String host, int port, String scheme, String realm));
void addProxyCredentials(
    String host, int port, String realm, HttpClientCredentials credentials);
使用方法和addCredentials和authenticate 相同

 

例

將全部請求經過代理服務器(192.168.1.2:8888)發送出去:
  client.findProxy = (uri) {
    // 若是須要過濾uri,能夠手動判斷
    // findProxy 回調返回值是一個遵循瀏覽器PAC腳本格式的字符串,若是不須要代理,返回"DIRECT"便可。
    return "PROXY 192.168.1.2:8888";
 };

證書校驗

Https中爲了防止經過僞造證書而發起的中間人攻擊,客戶端應該對自簽名或非CA頒發的證書進行校驗。

 

證書校驗其實就是提供一個badCertificateCallback回調

HttpClient對證書校驗的邏輯以下:
    1. 若是請求的Https證書是可信CA頒發的,而且訪問host包含在證書的domain列表中(或者符合通配規則)而且證書未過時,則驗證經過。
    2. 若是第一步驗證失敗,但在建立HttpClient時,已經經過SecurityContext將證書添加到證書信任鏈中,那麼當服務器返回的證書在信任鏈中的話,則驗證經過。
    3. 若是一、2驗證都失敗了,若是用戶提供了badCertificateCallback回調,則會調用它,若是回調返回true,則容許繼續連接,若是返回false,則終止連接。

 

例

假設後臺服務使用的是自簽名證書,證書格式是PEM格式。將證書的內容保存在本地字符串中,那麼校驗邏輯以下:
String PEM="XXXXX";//能夠從文件讀取
...
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
  if(cert.pem==PEM){
    return true; //證書一致,則容許發送數據
  }
  return false;
};

X509Certificate是證書的標準格式,包含了證書除私鑰外全部信息。另外,上面的示例沒有校驗host,是由於只要服務器返回的證書內容和本地的保存一致就已經能證實是咱們的服務器了(而不是中間人),host驗證一般是爲了防止證書和域名不匹配。

對於自簽名的證書,也能夠將其添加到本地證書信任鏈中,這樣證書驗證時就會自動經過,而不會再走到badCertificateCallback回調中:

SecurityContext sc=new SecurityContext();
//file爲證書路徑
sc.setTrustedCertificates(file);
//建立一個HttpClient
HttpClient httpClient = new HttpClient(context: sc);

注意,經過setTrustedCertificates()設置的證書格式必須爲PEM或PKCS12,若是證書格式爲PKCS12,則需將證書密碼傳入,這樣則會在代碼中暴露證書密碼,因此客戶端證書校驗不建議使用PKCS12格式的證書。

例(http庫)

 

添加http依賴


import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Post> fetchPost() async {
  // http.get方法返回類型:Future<http.Response>
  final response =
      await http.get('https://jsonplaceholder.typicode.com/posts/1');
  final response = await http.get(
    'https://jsonplaceholder.typicode.com/posts/1',
    headers: {HttpHeaders.AUTHORIZATION: "Basic your_api_token_here"},    // 認證請求
  );
  final responseJson = json.decode(response.body);

  return new Post.fromJson(responseJson);
}

class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({this.userId, this.id, this.title, this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return new Post(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Fetch Data Example',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Fetch Data Example'),
        ),
        body: new Center(
          child: new FutureBuilder<Post>(
            future: fetchPost(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return new Text(snapshot.data.title);
              } else if (snapshot.hasError) {
                return new Text("${snapshot.error}");
              }

              // By default, show a loading spinner
              return new CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

直接使用HttpClient發起網絡請求是比較麻煩的,不少事情得手動處理,若是再涉及到文件上傳/下載、Cookie管理等就會很是繁瑣。

4. dio庫

支持:Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載、超時、請求配置等。

 

一個dio實例能夠發起多個http請求,通常來講,APP只有一個http數據源時,dio應該使用單例模式。

 

1. 添加dio依賴包並下載:
dependencies:
  dio: #lastverssion


2. 導入並建立dio實例:
import 'package:dio/dio.dart';
Dio dio =  Dio();


3.
GET 請求 :
Response response;
// 等價於response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"});
response=await dio.get("/test?id=12&name=wendu");
print(response);
print(response.data.toString());

POST 請求:
response=await dio.post("/test",data:{"id":12,"name":"wendu"})

發起多個併發請求:
response= await Future.wait([dio.post("/info"),dio.get("/token")]);

下載文件:
response=await dio.download("https://www.google.com/",_savePath);

發送 FormData:
// 若是發送的數據是FormData,則dio會將請求header的contentType設爲「multipart/form-data」。
FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
});
response = await dio.post("/info", data: formData)

經過FormData上傳多個文件:
FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
   "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
   "file2": new UploadFileInfo(new File("./upload.txt"), "upload2.txt"),
     // 支持文件數組上傳
   "files": [
      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
    ]
});
response = await dio.post("/info", data: formData)


dio內部仍然使用HttpClient發起的請求,因此代理、請求認證、證書校驗等和HttpClient是相同的,能夠在onHttpClientCreate回調中設置,例如:
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    //設置代理 
    client.findProxy = (uri) {
      return "PROXY 192.168.1.2:8888";
    };
    //校驗證書
    httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
      if(cert.pem==PEM){
      return true; //證書一致,則容許發送數據
     }
     return false;
    };   
  };
注意,onHttpClientCreate會在當前dio實例內部須要建立HttpClient時調用,因此經過此回調配置HttpClient會對整個dio實例生效,若是你想針對某個應用請求單獨的代理或證書校驗策略,能夠建立一個新的dio實例便可。

 

經過Github開放的API來請求flutterchina組織下的全部公開的開源項目,實現:
    在請求階段彈出loading
    請求結束後,若是請求失敗,則展現錯誤信息;若是成功,則將項目名稱列表展現出來。

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  //
  Dio _dio = new Dio();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        body: new Container(
      alignment: Alignment.center,
      child: FutureBuilder(
          //
          future: _dio.get("https://api.github.com/orgs/flutterchina/repos"),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            // 請求完成
            if (snapshot.connectionState == ConnectionState.done) {
              Response response = snapshot.data;
              // 發生錯誤
              if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              }
              // 請求成功,經過項目信息構建用於顯示項目名稱的ListView
              return ListView(
                children: response.data
                    .map<Widget>((e) => ListTile(title: Text(e["full_name"])))
                    .toList(),
              );
            }
            // 請求未完成時彈出loading
            return CircularProgressIndicator();
          }),
    ));
  }
}

5. Http分塊下載

 

1. 分塊下載的最終速度受設備所在網絡帶寬、源出口速度、每一個塊大小、以及分塊的數量等諸多因素影響,實際過程當中很難保證速度最優。下載速度的主要瓶頸是取決於網絡速度和服務器的出口速度,若是是同一個數據源,分塊下載的意義並不大,由於服務器是同一個,出口速度肯定的,主要取決於網速。若是有多個下載源,而且每一個下載源的出口帶寬都是有限制的,這時分塊下載可能會更快一下,之因此說「可能」,是因爲這並非必定的,好比有三個源,三個源的出口帶寬都爲1G/s,而咱們設備所連網絡的峯值假設只有800M/s,那麼瓶頸就在咱們的網絡。即便咱們設備的帶寬大於任意一個源,下載速度依然不必定就比單源單線下載快,試想一下,假設有兩個源A和B,速度A源是B源的3倍,若是採用分塊下載,兩個源各下載一半的話。
    
2. 分塊下載有一個比較使用的場景是斷點續傳,能夠將文件分爲若干個塊,而後維護一個下載狀態文件用以記錄每個塊的狀態,這樣即便在網絡中斷後,也能夠恢復中斷前的狀態。分塊大小、下載到一半的塊如何處理、要不要維護一個任務隊列


Http協議定義了分塊傳輸的響應header字段,但具體是否支持取決於Server的實現,
能夠指定請求頭的"range"字段來驗證服務器是否支持分塊傳輸。

 

利用curl命令來驗證:

$ curl -H "Range: bytes=0-10" http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg -v
輸出:

# 請求頭
> GET /HBuilder.9.0.2.macosx_64.dmg HTTP/1.1
> Host: download.dcloud.net.cn
> User-Agent: curl/7.54.0
> Accept: */*
> Range: bytes=0-10
# 響應頭
< HTTP/1.1 206 Partial Content
< Server: Tengine
< Content-Type: application/octet-stream
< Content-Length: 11
< Connection: keep-alive
< Date: Fri, 25 Sep 2020 16:02:41 GMT
< Content-Range: bytes 0-10/233295878
。。。

說明:
  在請求頭中添加"Range: bytes=0-10"的做用是,告訴服務器本次請求只想獲取文件0-10(包括10,共11字節)這塊內容。若是服務器支持分塊傳輸,則響應狀態碼爲206,表示「部份內容」,而且同時響應頭中包含「Content-Range」字段,若是不支持則不會包含。
  0-10表示本次返回的區塊,233295878表明文件的總長度,單位都是byte。

例2

 

設計一個簡單的多線程的文件分塊下載器,實現的思路是:
    1. 先檢測是否支持分塊傳輸,若是不支持,則直接下載;若支持,則將剩餘內容分塊下載。
    2. 各個分塊下載時保存到各自臨時文件,等到全部分塊下載完後合併臨時文件。
    3. 刪除臨時文件。

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'dart:io';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  /// 
  Future downloadWithChunks(
    url,
    savePath, {
    ProgressCallback onReceiveProgress,
  }) async {
    const firstChunkSize = 102;
    const maxChunk = 3;

    int total = 0;
    var dio = Dio();
    var progress = <int>[];

    createCallback(no) {
      return (int received, _) {
        progress[no] = received;
        if (onReceiveProgress != null && total != 0) {
          onReceiveProgress(progress.reduce((a, b) => a + b), total);
        }
      };
    }

    // 使用dio的download API 實現downloadChunk:
    //start 表明當前塊的起始位置,end表明結束位置
    //no 表明當前是第幾塊
    Future<Response> downloadChunk(url, start, end, no) async {
      progress.add(0); //progress記錄每一塊已接收數據的長度
      --end;
      return dio.download(
        url,
        savePath + "temp$no", //臨時文件按照塊的序號命名,方便最後合併
        onReceiveProgress: createCallback(no), // 建立進度回調,後面實現
        options: Options(
          headers: {"range": "bytes=$start-$end"}, //指定請求的內容區間
        ),
      );
    }

    Future mergeTempFiles(chunk) async {
      File f = File(savePath + "temp0");
      IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
      //合併臨時文件
      for (int i = 1; i < chunk; ++i) {
        File _f = File(savePath + "temp$i");
        await ioSink.addStream(_f.openRead());
        await _f.delete(); // 刪除臨時文件
      }
      await ioSink.close();
      await f.rename(savePath); // 合併後的文件重命名爲真正的名稱
    }

    // 經過第一個分塊請求檢測服務器是否支持分塊傳輸
    Response response = await downloadChunk(url, 0, firstChunkSize, 0);
    if (response.statusCode == 206) {
      // 若是支持
      // 解析文件總長度,進而算出剩餘長度
      total = int.parse(response.headers
          .value(HttpHeaders.contentRangeHeader)
          .split("/")
          .last);
      int reserved = total -
          int.parse(response.headers.value(HttpHeaders.contentLengthHeader));
      // 文件的總塊數(包括第一塊)
      int chunk = (reserved / firstChunkSize).ceil() + 1;
      if (chunk > 1) {
        int chunkSize = firstChunkSize;
        if (chunk > maxChunk + 1) {
          chunk = maxChunk + 1;
          chunkSize = (reserved / maxChunk).ceil();
        }
        var futures = <Future>[];
        for (int i = 0; i < maxChunk; ++i) {
          int start = firstChunkSize + i * chunkSize;
          // 分塊下載剩餘文件
          futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
        }
        // 等待全部分塊所有下載完成
        await Future.wait(futures);
      }
      // 合併文件文件
      await mergeTempFiles(chunk);
    }
  }

  main() async {
    var url = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg";
    var savePath = "./example/HBuilder.9.0.2.macosx_64.dmg";
    await downloadWithChunks(url, savePath,
        onReceiveProgress: (received, total) {
      if (total != -1) {
        print("${(received / total * 100).floor()}%");
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: main,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

6. 使用WebSockets

Http協議是無狀態的,只能由客戶端主動發起,服務端再被動響應,服務端沒法向客戶端主動推送內容,而且一旦服務器響應結束,連接就會斷開,因此沒法進行實時通訊。WebSocket協議正是爲解決客戶端與服務端實時通訊而產生的技術,如今已經被主流瀏覽器支持,Flutter也提供了專門的包來支持WebSocket協議。

 

Http協議中雖然能夠經過keep-alive機制使服務器在響應結束後連接會保持一段時間,但最終仍是會斷開,keep-alive機制主要是用於避免在同一臺服務器請求多個資源時頻繁建立連接,它本質上是支持連接複用的技術,而並不是用於實時通訊。

WebSocket協議本質上是一個基於tcp的協議,它是先經過HTTP協議發起一條特殊的http請求進行握手後,若是服務端支持WebSocket協議,則會進行協議升級。WebSocket會使用http協議握手後建立的tcp連接,和http協議不一樣的是,WebSocket的tcp連接是個長連接(不會斷開),因此服務端與客戶端就能夠經過此TCP鏈接進行實時通訊。

要接收二進制數據仍然使用StreamBuilder,由於WebSocket中全部發送的數據使用幀的形式發送,而幀是有固定格式,每個幀的數據類型均可以經過Opcode字段指定,它能夠指定當前幀是文本類型仍是二進制類型(還有其它類型),因此客戶端在收到幀時就已經知道了其數據類型,因此flutter徹底能夠在收到數據後解析出正確的類型,因此就無需開發者去關心,當服務器傳輸的數據是指定爲二進制時,StreamBuilder的snapshot.data的類型就是List<int>,是文本時,則爲String。

web_socket_channel包提供了鏈接到WebSocket服務器的工具。該package提供了一個WebSocketChannel容許既能夠監聽來自服務器的消息,又能夠將消息發送到服務器的方法。

 

使用步驟:

1. 鏈接到WebSocket服務器
// 建立一個WebSocketChannel,並鏈接到一臺服務器:
final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');

2. 監聽來自服務器的消息
// WebSocketChannel提供了一個來自服務器的消息Stream 。該Stream類是dart:async包中的一個基礎類。它提供了一種方法來監聽來自數據源的異步事件。與Future返回單個異步響應不一樣,Stream類能夠隨着時間推移傳遞不少事件。該StreamBuilder 組件將鏈接到一個Stream, 並在每次收到消息時通知Flutter從新構建界面。
new StreamBuilder(
  stream: widget.channel.stream,
  builder: (context, snapshot) {
    return new Text(snapshot.hasData ? '${snapshot.data}' : '');
  },
);

3. 將數據發送到服務器
// 將數據發送到服務器,WebSocketChannel提供了一個StreamSink,它將消息發給服務器。StreamSink類提供了給數據源同步或異步添加事件的通常方法。
channel.sink.add('Hello!');

4. 關閉WebSocket鏈接
// 使用WebSocket後,要關閉鏈接:
channel.sink.close();

 

import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';

void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _controller = new TextEditingController();
  IOWebSocketChannel channel;
  String _text = "";

  @override
  void initState() {
    // 建立websocket鏈接
    channel = new IOWebSocketChannel.connect('ws://echo.websocket.org');
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("WebSocket(內容回顯)"),
      ),
      body: new Padding(
        padding: const EdgeInsets.all(20.0),
        child: new Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            new Form(
              child: new TextFormField(
                controller: _controller,
                decoration: new InputDecoration(labelText: '發送內容'),
              ),
            ),
            new StreamBuilder(
              stream: channel.stream,
              builder: (context, snapshot) {
                //網絡不通會走到這
                if (snapshot.hasError) {
                  _text = "網絡不通...";
                } else if (snapshot.hasData) {
                  _text = "echo: "+snapshot.data;
                }
                return new Padding(
                  padding: const EdgeInsets.symmetric(vertical: 24.0),
                  child: new Text(_text),
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _sendMessage,
        tooltip: 'Send message',
        child: new Icon(Icons.send),
      ),
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      channel.sink.add(_controller.text);
    }
  }

  @override
  void dispose() {
    channel.sink.close();
    super.dispose();
  }
}

7. 使用Socket API(dart:io包中)

 

Http協議和WebSocket協議都屬於應用層協議,除了它們,應用層協議還有不少如:SMTP、FTP等,這些應用層協議的實現都是經過Socket API來實現的。其實,操做系統中提供的原生網絡請求API是標準的,在C語言的Socket庫中,它主要提供了端到端創建連接和發送數據的基礎API,而高級編程語言中的Socket庫其實都是對操做系統的socket API的一個封裝。
若是須要自定義協議或者想直接來控制管理網絡連接、又或者想從新實現一個HttpClient,這時就須要使用Socket。

使用Socket須要本身實現Http協議(須要本身實現和服務器的通訊過程)

 

_request() async{
  //創建鏈接
  var socket=await Socket.connect("baidu.com", 80);
  //根據http協議,發送請求頭
  socket.writeln("GET / HTTP/1.1");
  socket.writeln("Host:baidu.com");
  socket.writeln("Connection:close");
  socket.writeln();
  await socket.flush(); //發送
  //讀取返回內容
  _response =await socket.transform(utf8.decoder).join();
  await socket.close();
}

8. http三方庫

 

1.添加依賴包,並下載
http: #lastversion

2.導入庫
import 'package:http/http.dart' as http;
import 'dart:convert' as convert;    


3. 使用(在initState中調用請求方法)
  var url = 'https://...';

  // post
  var response = await http.post(url, body: {'name': '張三', 'password': '123456'});
  // get
  // var response = await http.get(url);

  if (response.statusCode == 200) { // 請求成功
    // 解析數據
    var jsonResponse = convert.jsonDecode(response.body);
    var name = jsonResponse['name'];
  } else {
    print('請求失敗 狀態碼: ${response.statusCode}.');
  }

做者:風雨路上砥礪前行 連接:https://www.jianshu.com/p/695efe77597b 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索