Flutter 中的資料的獲取

移動的小太陽發表於2021-04-06

Flutter 中的資料的獲取

APP 中的資料來源大致分為兩部分,一是本地,包括 檔案、資料庫;二是通過網路從遠處獲取,今天一起來學習Flutter 如何獲取這兩類資料的。

Dark 單執行緒如何實現非同步

在學習本地資料獲取和網路資料獲取之前,先了解Flutter 中如何處理完成非同步的處理的。

非同步:和同步相對,不等任務執行完,直接執行下一個任務。

首先我們瞭解到 Dark 是單執行緒,單執行緒是如何執行非同步呢?

因為App 絕大多數時間都在等待。比如,等使用者點選、等網路請求返回、等檔案 IO 結果,等等。而這些等待行為並不是阻塞的。比如說,網路請求,Socket 本身提供了 select 模型可以非同步查詢;而檔案 IO,作業系統也提供了基於事件的回撥機制。所以,基於這些特點,單執行緒模型可以在等待的過程中做別的事情,等真正需要響應結果了,再去做對應的處理。因為等待過程並不是阻塞的,所以給我們的感覺就像是同時在做多件事情一樣。但其實始終只有一個執行緒在處理你的事情。

和大多數語言一樣,完成非同步任務也是Event Loop來完成。把 需要在主執行緒響應的事件放入佇列中,然後再主執行緒空閒的時候,不斷的從 佇列中取出訊息,然後在主執行緒完成。

在Dark 中,其實有兩個佇列,一個是事件佇列(Event Queue)、一個是微任務佇列(Microtask Queue);微任務佇列的優先順序要比 事件佇列要高,所以每次都會先檢查 微任務佇列,有就優先執行,沒有了才從 事件佇列取出事件進行處理。

image.png

瞭解了Dark 的非同步處理機制後,看看在程式碼中如何實現的。

Dart 為 Event Queue 的任務建立提供了一層封裝,叫作 Future。從名字上也很容易理解,它表示一個在未來時間才會完成的任務。把一個函式體放到了 Future 中就完成了從同步到非同步的轉換。

void syncDemo1() {
  Future(() => print("print future1"));//a
  print("print 1");//b

  Future(() => print("print 2"))//c
    ..then((value) => print("print 3"))//d
    ..then((value) => print("print 4"));//e
}
複製程式碼
print 1
print future1
print 2
print 3
print 4
複製程式碼

呼叫的順序是:

  • a 新增到任務佇列;
  • b 在主執行緒,執行執行;
  • c 新增到任務佇列;這時候在主執行緒沒有任務了,從任務佇列取出 a,並執行
  • 執行a 後,主執行緒沒有任務,還是從任務佇列取出c 執行,then 後面的函式會依次同步執行;

非同步函式

對於一個非同步函式來說,其返回時內部執行動作並未結束,因此需要返回一個 Future 物件,供呼叫者使用。呼叫者根據 Future 物件,來決定:

  • 是在這個 Future 物件上註冊一個 then,等 Future 的執行體結束了以後再進行非同步處理;
  • 還是一直同步等待 Future 執行體結束。對於非同步函式返回的 Future 物件,如果呼叫者決定同步等待,則需要在呼叫處使用 await 關鍵字,並且在呼叫處的函式體使用 async 關鍵字。
void syncFunDemo() async{
  Future<String> future() =>  Future<String>.delayed(Duration(seconds: 3),()=>"hello 2021");
  // future.then((value) => print("獲取到非同步資料$value"));//等待完成後,執行then裡面的函式
  print("獲取到非同步資料"+ (await future()));//同步等待
}
複製程式碼

為什麼要加上 async?

因為 Dart 中的 await 並不是阻塞等待,而是非同步等待。Dart 會將呼叫體的函式也視作非同步函式,將等待語句的上下文放入 Event Queue 中,一旦有了結果,Event Loop 就會把它從 Event Queue 中取出,等待程式碼繼續執行。

Flutter 中的網路請求

在瞭解了非同步操作之後,趁熱打鐵,學習flutter 中的網路請求。

網路與服務端資料互動時,不可避免地需要用到三個概念:定位、傳輸與應用。

其中,定位,定義瞭如何準確地找到網路上的一臺或者多臺主機(即 IP 地址);傳輸,則主要負責在找到主機後如何高效且可靠地進行資料通訊(即 TCP、UDP 協議);而應用,則負責識別雙方通訊的內容(即 HTTP 協議)。

一般的網路請求框架中,依次http 網路呼叫 可以分為以下幾個部分:

  • 建立網路呼叫例項 client,設定通用請求行為(如超時時間);
  • 構造 URI,設定請求 header、body;
  • 發起請求, 等待響應;
  • 解碼響應的內容。

在 Flutter 中,Http 網路程式設計的實現方式主要分為三種:dart:io 裡的 HttpClient 實現、Dart 原生 http 請求庫實現、第三方庫 dio 實現。

HttpClient

Dart 原生 http 請求庫實現

第三方庫 dio 實現

HttpClient 和 http 使用方式雖然簡單,但其暴露的定製化能力都相對較弱,很多常用的功能都不支援(或者實現異常繁瑣),比如取消請求、定製攔截器、Cookie 管理等。因此對於複雜的網路請求行為,我推薦使用目前在 Dart 社群人氣較高的第三方 dio 來發起網路請求。

  • 加入dio 依賴
dio: 3.0.10
複製程式碼
  • 簡單的get 請求
void getRequest() async {
  //建立網路呼叫示例
  Dio dio = Dio();

  //設定URI及請求user-agent後發起請求
  var response = await dio.get("https://wanandroid.com/wxarticle/chapters/json",
      options: Options(headers: {"user-agent": "Custom-UA"}));

  //列印請求結果
  if (response.statusCode == HttpStatus.ok) {
    print(response.data.toString());
  } else {
    print("Error: ${response.statusCode}");
  }
}
複製程式碼
  • 同時發起多個請求
// 同時發起多個請求
void getRequest2() async {
  //建立網路呼叫示例
  Dio dio = Dio();
//同時發起兩個並行請求
  List<Response> responseX = await Future.wait([
    dio.get("https://wanandroid.com/wxarticle/chapters/json"),
    dio.get("https://www.wanandroid.com/article/list/0/json")
  ]);

//列印請求1響應結果
  print("Response1: ${responseX[0].toString()}");
//列印請求2響應結果
  print("Response2: ${responseX[1].toString()}");
}
複製程式碼
  • 新增攔截器
// 新增攔截器
void getRequest3() async {
  //建立網路呼叫示例
  Dio dio = Dio();

  //增加攔截器
  dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
    //為每個請求頭都增加user-agent
    options.headers["user-agent"] = "Custom-UA";
    print("interceptor request ${options.uri}");
    print("interceptor request ${options.headers}");
    print("interceptor request ${options.data}");
    //放行請求
    return options;
  }, onResponse: (Response response) {
    print("interceptor response ${response.data.toString()}");
  }));

//增加try catch,防止請求報錯
  try {
    await dio.get("https://wanandroid.com/wxarticle/chapters/json");
  } catch (e) {
    print(e);
  }
}
複製程式碼

以上都是dio 簡單的網路請求示例,更多的高階用法,可以到 GitHub 去看看。

Json 解析

在完成了網路請求後,得到的資料我們還不能直接使用,要先解析伺服器給我們返回的資料。而json 是常用的服務端和客戶端傳輸一種資料格式。

在拿到服務端返回的json 資料後,如何解析呢?

由於 Flutter 不支援執行時反射,因此並沒有提供像 Gson、Mantle 這樣自動解析 JSON 的庫來降低解析成本。在 Flutter 中,JSON 解析完全是手動的,開發者要做的事情多了一些,但使用起來倒也相對靈活。

自動解析:使用 dart:convert 庫中內建的 JSON 解碼器,將 JSON 字串解析成自定義物件的過程。使用這種方式,我們需要先將 JSON 字串傳遞給 JSON.decode 方法解析成一個 Map,然後把這個 Map 傳給自定義的類,進行相關屬性的賦值。

  • json 解析
class Student{
  //屬性id,名字與成績
  String id;
  String name;
  int score;
  //構造方法
  Student({
    this.id,
    this.name,
    this.score
  });
  //JSON解析工廠類,把map 解析成model 物件
  factory Student.fromJson(Map<String, dynamic> parsedJson){
    return Student(
        id: parsedJson['id'],
        name : parsedJson['name'],
        score : parsedJson ['score']
    );
  }
}

void parseJson1(){

var jsonString = """{ "id":"1234", "name":"周結", "score" : 95}""" ;
    //jsonString為JSON文字
    final jsonResponse = json.decode(jsonString);
    Student student = Student.fromJson(jsonResponse);
    print(student.name);

}
複製程式碼

json 資料解析生成類中,都有一個 json 解析工廠;要是裡面巢狀 比較多層,估計解析起來也挺累的,這樣的事情交給外掛就可以了。 在 Android studio plugins 中搜尋 FlutterJsonBeanFactory安裝該外掛,然後重啟studio。

QQ20210325-211230.gif

/// 使用 FlutterJsonBeanFactory外掛實現 json轉model
void parseJson2() {
  var jsonString = """{ "id":"123", "name":"張三", "score" : 95}""";
  //jsonString為JSON文字
  final jsonResponse = json.decode(jsonString);
  UserEntity student = JsonConvert.fromJsonAsT(jsonResponse);
  print(student.name);
}
複製程式碼

FlutterJsonBeanFactory 外掛幫我們做了好多事情,讓我們更能專注於開發。

Flutter 中本地資料管理

通過上面的學習,我們瞭解了在dart 中的網路請求和json 解析,完成了從遠端獲取資料的學習,接下來看看 flutter 如何處理本地資料的。

Flutter 提供了三種資料持久化方法,即檔案、SharedPreferences 與資料庫。

檔案

檔案是儲存在某種介質(比如磁碟)上指定路徑的、具有檔名的一組有序資訊的集合。從其定義看,要想以檔案的方式實現資料持久化,我們首先需要確定一件事兒:資料放在哪兒?這,就意味著要定義檔案的儲存路徑。

Flutter 提供了兩種檔案儲存的目錄,即臨時(Temporary)目錄與文件(Documents)目錄:

  • 臨時目錄是作業系統可以隨時清除的目錄,通常被用來存放一些不重要的臨時快取資料。這個目錄在 iOS 上對應著 NSTemporaryDirectory 返回的值,而在 Android 上則對應著 getCacheDir 返回的值。
  • 文件目錄則是隻有在刪除應用程式時才會被清除的目錄,通常被用來存放應用產生的重要資料檔案。在 iOS 上,這個目錄對應著 NSDocumentDirectory,而在 Android 上則對應著 AppData 目錄。

// 檔案的讀取,要先依賴 path_provider: ^2.0.1

class LocalDataDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("本地資料讀取"),
      ),
      body: ListView(
        children: [
          ListTile(
            title: Text("本地檔案讀寫"),
            onTap: () => Navigator.pushNamed(context, "LocalFileDemo"),
          ),ListTile(
            title: Text("SharePreferenceDemoDemo"),
            onTap: () => Navigator.pushNamed(context, "SharePreferenceDemo"),
          )
        ],
      ),
    );
  }
}

class LocalFileDemo extends StatelessWidget {
//建立檔案目錄
  Future<File> get _localFile async {
    final directory = await getApplicationDocumentsDirectory();
    final path = directory.path;
    return File('$path/test.txt');
  }

//將字串寫入檔案
  Future<File> writeContent(String content) async {
    final file = await _localFile;
    return file.writeAsString(content);
  }

//從檔案讀出字串
  Future<String> readContent() async {
    try {
      final file = await _localFile;
      String contents = await file.readAsString();
      return contents;
    } catch (e) {
      return "";
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("本地檔案讀寫"),
      ),
      body: Center(
        child: Column(
          children: [
            MaterialButton(
              onPressed: () {
                writeContent("hello jay 2021");
              },
              child: Text("寫入資料"),
            ),
            MaterialButton(
              onPressed: () async {
                print("從檔案讀取到的資料:${await readContent()}");
              },
              child: Text("讀取資料"),
            )
          ],
        ),
      ),
    );
  }
}
複製程式碼

sharePreference

使用前先新增依賴 shared_preferences: ^2.0.5,接下來的使用就很簡單了。

// sharePreference 適合儲存資料量比較小的鍵值對,要先依賴 shared_preferences: ^2.0.5
class SharePreferenceDemo extends StatelessWidget {
  String spName = "sp_name";

  _spSaveString() async {
    SharedPreferences.setMockInitialValues({});// 需要新增,否則會報錯 No implementation found for method getAll on channel plugins.flutter.io/shared_preferences
    var sharePreference = await SharedPreferences.getInstance();
    await sharePreference.setString(spName, "this is sharePreference");
  }

  _readSpString() async {
    var sharePreference = await SharedPreferences.getInstance();
    print("這是從sharePreference 讀取的資料:${sharePreference.getString(spName)}");
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("sharePreferenceDemo"),
      ),
      body: Center(
        child: Column(
          children: [
            MaterialButton(
              onPressed: () {
                _spSaveString();
              },
              child: Text("寫入資料"),
            ),
            MaterialButton(
              onPressed: () {
                _readSpString();
              },
              child: Text("讀取資料"),
            )
          ],
        ),
      ),
    );
  }
}
複製程式碼

當然 sharePreference 提供了基本資料型別的儲存,更多api 可以看看 sharePreference 的 GitHub

資料庫

除了上面兩種儲存方式外,Flutter 還提供了資料庫的儲存,適用於需要持久化大量格式化後的資料,並且這些資料還會以較高的頻率更新。與檔案和 SharedPreferences 相比,資料庫在資料讀寫上可以提供更快、更靈活的解決方案。

下面看一個小案例,瞭解資料庫的使用; 首先需要 依賴 sqflite: ^1.3.0

參考連結

www.jianshu.com/p/2eafae001…

相關文章