從0開始寫一個基於Flutter的開源中國客戶端(7)——App網路請求和資料儲存

yuxiyu發表於2018-08-01

上一篇中我記錄了基於Flutter的開源中國客戶端各個靜態頁面的實現,主要是UI的實現,沒有涉及到任何網路請求,資料載入、儲存等方面。本篇記錄的是該專案中的網路請求和資料儲存、載入的方式,希望自己在溫故知新的同時能給Flutter初學者帶來幫助。

索引 文章
1 從0開始寫一個基於Flutter的開源中國客戶端(1)
Flutter簡介及開發環境搭建 | 掘金技術徵文
2 從0開始寫一個基於Flutter的開源中國客戶端(2)
Dart語法基礎
3 從0開始寫一個基於Flutter的開源中國客戶端(3)
初識Flutter & 常用的Widgets
4 從0開始寫一個基於Flutter的開源中國客戶端(4)
Flutter佈局基礎
5 從0開始寫一個基於Flutter的開源中國客戶端(5)
App整體佈局框架搭建
6 從0開始寫一個基於Flutter的開源中國客戶端(6)
各個靜態頁面的實現
?7 從0開始寫一個基於Flutter的開源中國客戶端(7)
App網路請求和資料儲存
8 從0開始寫一個基於Flutter的開源中國客戶端(8)
外掛的使用

Flutter中的網路請求

Flutter中已內建了網路請求庫,可直接匯入使用:

import 'package:http/http.dart' as http;
複製程式碼

一個最簡單的get請求程式碼如下:

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

main() async {
  http.Response res = await http.get("https://cn.bing.com");
  print(res.body); // 列印出get請求返回的字串資料
}
複製程式碼

控制檯中會列印出請求返回的字串資料。

另外也有一些開源的網路請求庫,由於筆者暫時沒有用過,所以在本篇中不詳細說了。

在基於Flutter的開源中國客戶端中,使用的也是Flutter內建的網路請求庫,但是做了一些簡單的封裝,主要程式碼在lib/util/NetUtils.dart檔案中,程式碼如下:

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

class NetUtils {
  // get請求的封裝,傳入的兩個引數分別是請求URL和請求引數,請求引數以map的形式傳入,會在方法體中自動拼接到URL後面
  static Future<String> get(String url, {Map<String, String> params}) async {
    if (params != null && params.isNotEmpty) {
      // 如果引數不為空,則將引數拼接到URL後面
      StringBuffer sb = new StringBuffer("?");
      params.forEach((key, value) {
        sb.write("$key" + "=" + "$value" + "&");
      });
      String paramStr = sb.toString();
      paramStr = paramStr.substring(0, paramStr.length - 1);
      url += paramStr;
    }
    http.Response res = await http.get(url);
    return res.body;
  }
  
  // post請求
  static Future<String> post(String url, {Map<String, String> params}) async {
    http.Response res = await http.post(url, body: params);
    return res.body;
  }
}
複製程式碼

使用該工具類的方法也很簡單,如下程式碼所示:

import 'util/NetUtils.dart';

main() {
  Map<String, String> map = new Map();
  map['name'] = 'zhangsan';
  map['age'] = '20';
  NetUtils.get("http://www.baidu.com", params: map).then((res) {
    print(res);
  });
}
複製程式碼

Flutter中的資料儲存

一般移動應用開發中的資料儲存基本上都是檔案、資料庫等方式。Flutter沒有提供直接運算元據庫的API,但是有第三方的外掛可以用,比如sqflite,關於這個外掛的使用方法,可以檢視這裡,由於在基於Flutter的開源中國客戶端專案中沒有用到資料庫,所以這幾也不做詳細說明了。

本專案中針對token,使用者資訊的儲存,使用的是Flutter提供的類似於Android的SharedPreferences,這個庫是以外掛的形式提供的,並沒有內建到Flutter中,所以我們需要為專案配置外掛,在pubspec.yaml檔案中,加入如下配置:

dependencies:
  flutter:
    sdk: flutter
    
  shared_preferences: "^0.4.1"
複製程式碼

然後執行flutter packages get命令即可自動安裝外掛,如果你使用AndroidStudio作為開發工具,當pubspec.yaml檔案做了修改後,頁面上方會自動出現提示,點選Packages get即可。

外掛安裝成功後,使用起來很容易,如下程式碼所示:

import 'package:shared_preferences/shared_preferences.dart';

main() async {
  SharedPreferences sp = await SharedPreferences.getInstance();
  sp.setString("name", "zhangsan");
  sp.setInt("age", 20);
  sp.setBool("isLogin", false);
  sp.setDouble("price", 100.5);
}
複製程式碼

要獲取儲存的某個資料,只需要使用sp.get(key)即可。shared_preferences外掛的主頁在這裡

關於外掛的使用方法,這裡說明一下:pub.flutter-io.cn/是Flutter提供的一個外掛倉庫,可以釋出有關dart或flutter的外掛。如果我們需要實現某個功能,而flutter又沒有提供類似的功能時,可以上這個網站上搜尋相關關鍵字,也許就有人已經發布了他寫的庫,正好可以實現我們需要的功能。

上面簡要說明了Flutter中的網路請求和資料儲存,下面結合專案來說明如何載入網路資料,如何儲存使用者資訊等資料。

從網路載入資訊列表並顯示

上一篇中我記錄瞭如何顯示資訊列表,但是完全是一個靜態的資訊列表,裡面的資料都是測試的假資料,這一篇就記錄下如何從介面獲取真實的資訊資料並顯示出來。

在基於Flutter的開源中國客戶端專案中,由於開源中國官方的openapi提供的資料比較少,故資訊列表沒有使用開源中國官方提供的介面,是筆者用python抓的網站資料,介面部署在香港的雲伺服器上,若有訪問較慢的情況,請諒解。另外,介面沒有做任何認證,請不要頻繁請求介面。

顯示載入中的Loading

既然是從網路上載入資料,那必然會有一個耗時的等待期,需要給載入過程展示一個Loading,這裡我們為NewsListPage新增一個listData變數,如果該變數為null,則顯示Loading,否則就顯示列表資料,顯示Loading的同時從網路上請求資料,一旦有資料後,就通過setState更新listData,主要程式碼如下(NewsListPage.dart檔案):

  @override
  Widget build(BuildContext context) {
    // 無資料時,顯示Loading
    if (listData == null) {
      return new Center(
        // CircularProgressIndicator是一個圓形的Loading進度條
        child: new CircularProgressIndicator(),
      );
    } else {
      // 有資料,顯示ListView
      Widget listView = new ListView.builder(
        itemCount: listData.length * 2,
        itemBuilder: (context, i) => renderRow(i),
        controller: _controller,
      );
      // RefreshIndicator為ListView增加了下拉重新整理能力,onRefresh引數傳入一個方法,在下拉重新整理時呼叫
      return new RefreshIndicator(child: listView, onRefresh: _pullToRefresh);
    }
  }
  
  @override
  void initState() {
    super.initState();
    getNewsList(false);
  }
  
  // 從網路獲取資料,isLoadMore表示是否是載入更多資料
  getNewsList(bool isLoadMore) {
    String url = Api.NEWS_LIST;
    // curPage是定義在NewsListPageState中的成員變數,表示當前載入的頁面索引
    url += "?pageIndex=$curPage&pageSize=10";
    NetUtils.get(url).then((data) {
      if (data != null) {
        // 將介面返回的json字串解析為map型別,需要匯入包:import 'dart:convert';
        Map<String, dynamic> map = json.decode(data);
        if (map['code'] == 0) {
          // code=0表示請求成功
          var msg = map['msg'];
          // total表示資訊總條數
          listTotalSize = msg['news']['total'];
          // data為資料內容,其中包含slide和news兩部分,分別表示頭部輪播圖資料,和下面的列表資料
          var _listData = msg['news']['data'];
          var _slideData = msg['slide'];
          setState(() {
            if (!isLoadMore) {
              // 不是載入更多,則直接為變數賦值
              listData = _listData;
              slideData = _slideData;
            } else {
              // 是載入更多,則需要將取到的news資料追加到原來的資料後面
              List list1 = new List();
              // 新增原來的資料
              list1.addAll(listData);
              // 新增新取到的資料
              list1.addAll(_listData);
              // 判斷是否獲取了所有的資料,如果是,則需要顯示底部的"我也是有底線的"佈局
              if (list1.length >= listTotalSize) {
                list1.add(Constants.END_LINE_TAG);
              }
              // 給列表資料賦值
              listData = list1;
              // 輪播圖資料
              slideData = _slideData;
            }
          });
        }
      }
    });
  }
複製程式碼

上面的程式碼中是處理顯示Loading和顯示資料列表的不同邏輯,然後還有載入更多的邏輯處理,但是什麼時候去載入更多資料呢?很顯然,應該監聽列表的滾動,當列表滾動到底時,主動去載入下一頁資料。

載入下一頁資料

在上面的程式碼中,我們在建立ListView時,傳入了一個controller引數,這個controller就是為了監聽列表滾動事件而傳入的,它是一個ScrollController物件,我們在NewsListPageState類中定義這個變數並初始化:

ScrollController _controller = new ScrollController();
複製程式碼

要監聽列表是否滾動到底的事件,還需要給這個controller新增Listener,在NewsListPageState類的構造方法中新增如下程式碼:

  NewsListPageState() {
    _controller.addListener(() {
      // 表示列表的最大滾動距離 
      var maxScroll = _controller.position.maxScrollExtent;
      // 表示當前列表已向下滾動的距離
      var pixels = _controller.position.pixels;
      // 如果兩個值相等,表示滾動到底,並且如果列表沒有載入完所有資料
      if (maxScroll == pixels && listData.length < listTotalSize) {
        // scroll to bottom, get next page data
        curPage++; // 當前頁索引加1
        getNewsList(true); // 獲取下一頁資料
      }
    });
  }
複製程式碼

給ListView加入下拉重新整理能力

其實在上面的程式碼中已經為ListView新增了下拉重新整理的能力,就是build方法返回時,為ListView包裹了一層RefreshIndicator:

return new RefreshIndicator(child: listView, onRefresh: _pullToRefresh);
複製程式碼

_pullToRefresh方法會在下拉重新整理的時候呼叫,因為是下拉重新整理,所以取的是第一頁資料,並且不是載入更多,所以方法體如下:

  Future<Null> _pullToRefresh() async {
    curPage = 1;
    getNewsList(false);
    return null;
  }
複製程式碼

需要注意的是,onRefresh引數需要一個Future<Null>型別的資料,所以上面的_pullToRefresh才會返回Future<Null>

改造過後的資訊列表如下gif圖所示(圖比較大,載入會有點慢):

從0開始寫一個基於Flutter的開源中國客戶端(7)——App網路請求和資料儲存

儲存登入後的使用者資料

由於獲取動彈資訊,評論動彈等,都需要呼叫開源中國的openapi,而這些介面都是需要AccessToken和使用者id的,所以我們必須把使用者登入後的資料儲存下來,以便在需要用到這些資料時能獲取到。具體的如何實現登入將會放在下一篇——Flutter外掛的使用中說明。本篇暫時忽略登入的過程,只說明登入後如何儲存使用者資訊。

為了統一管理SharedPreferences,這裡我們新建一個工具類DataUtils,檔案目錄在lib/util/DataUtils.dart。開源中國openapi呼叫介面成功登入後,會返回以下資訊:

欄位名 欄位型別 說明
access_token String access_token值
refresh_token String refresh_token值
uid int 授權使用者的uid
tokenType String access_token型別
expires_in int 超時時間(單位秒)

為了在SharedPreferences中儲存以上資訊,先在DataUtils中宣告每個欄位對應的key,程式碼如下:

  static final String SP_AC_TOKEN = "accessToken";
  static final String SP_RE_TOKEN = "refreshToken";
  static final String SP_UID = "uid";
  static final String SP_IS_LOGIN = "isLogin"; // SP_IS_LOGIN標記是否登入
  static final String SP_EXPIRES_IN = "expiresIn";
  static final String SP_TOKEN_TYPE = "tokenType";
複製程式碼

然後提供一個靜態方法用於一次性儲存這些資訊:

  // 儲存使用者登入資訊,data中包含了token等資訊
  static saveLoginInfo(Map data) async {
    if (data != null) {
      SharedPreferences sp = await SharedPreferences.getInstance();
      String accessToken = data['access_token'];
      await sp.setString(SP_AC_TOKEN, accessToken);
      String refreshToken = data['refresh_token'];
      await sp.setString(SP_RE_TOKEN, refreshToken);
      num uid = data['uid'];
      await sp.setInt(SP_UID, uid);
      String tokenType = data['tokenType'];
      await sp.setString(SP_TOKEN_TYPE, tokenType);
      num expiresIn = data['expires_in'];
      await sp.setInt(SP_EXPIRES_IN, expiresIn);

      await sp.setBool(SP_IS_LOGIN, true); // SP_IS_LOGIN標記是否登入
    }
  }
複製程式碼

登入成功後就可以呼叫開源中國的openapi獲取使用者資訊了,跟上面類似,先定義使用者資訊每個欄位對應的key:

  static final String SP_USER_NAME = "name";
  static final String SP_USER_ID = "id";
  static final String SP_USER_LOC = "location";
  static final String SP_USER_GENDER = "gender";
  static final String SP_USER_AVATAR = "avatar";
  static final String SP_USER_EMAIL = "email";
  static final String SP_USER_URL = "url";
複製程式碼

根據命名就知道每個欄位代表的什麼含義,這裡就不細說了,然後還是提供一個靜態方法,用於一次性儲存使用者資訊:

  // 儲存使用者個人資訊
  static Future<UserInfo> saveUserInfo(Map data) async {
    if (data != null) {
      SharedPreferences sp = await SharedPreferences.getInstance();
      String name = data['name'];
      num id = data['id'];
      String gender = data['gender'];
      String location = data['location'];
      String avatar = data['avatar'];
      String email = data['email'];
      String url = data['url'];
      await sp.setString(SP_USER_NAME, name);
      await sp.setInt(SP_USER_ID, id);
      await sp.setString(SP_USER_GENDER, gender);
      await sp.setString(SP_USER_AVATAR, avatar);
      await sp.setString(SP_USER_LOC, location);
      await sp.setString(SP_USER_EMAIL, email);
      await sp.setString(SP_USER_URL, url);
      UserInfo userInfo = new UserInfo(
        id: id,
        name: name,
        gender: gender,
        avatar: avatar,
        email: email,
        location: location,
        url: url
      );
      return userInfo;
    }
    return null;
  }
複製程式碼

儲存使用者資訊是一個非同步的過程,其中UserInfo是定義在lib/model/下的一個實體類,程式碼如下:

// 使用者資訊
class UserInfo {

  String gender;
  String name;
  String location;
  num id;
  String avatar;
  String email;
  String url;

  UserInfo({this.id, this.name, this.gender, this.avatar, this.email, this.location, this.url});

}
複製程式碼

為了方便的拿到儲存的使用者資訊和AccessToken資料,以及判斷當前是否登入,為DataUtils提供三個靜態方法:

  // 獲取使用者資訊
  static Future<UserInfo> getUserInfo() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    bool isLogin = sp.getBool(SP_IS_LOGIN);
    if (isLogin == null || !isLogin) {
      return null;
    }
    UserInfo userInfo = new UserInfo();
    userInfo.id = sp.getInt(SP_USER_ID);
    userInfo.name = sp.getString(SP_USER_NAME);
    userInfo.avatar = sp.getString(SP_USER_AVATAR);
    userInfo.email = sp.getString(SP_USER_EMAIL);
    userInfo.location = sp.getString(SP_USER_LOC);
    userInfo.gender = sp.getString(SP_USER_GENDER);
    userInfo.url = sp.getString(SP_USER_URL);
    return userInfo;
  }

  // 是否登入
  static Future<bool> isLogin() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    bool b = sp.getBool(SP_IS_LOGIN);
    return b != null && b;
  }

  // 獲取accesstoken
  static Future<String> getAccessToken() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    return sp.getString(SP_AC_TOKEN);
  }
複製程式碼

如果使用者登出登入,需要清除已儲存的使用者資訊:

  // 清除登入資訊
  static clearLoginInfo() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    await sp.setString(SP_AC_TOKEN, "");
    await sp.setString(SP_RE_TOKEN, "");
    await sp.setInt(SP_UID, -1);
    await sp.setString(SP_TOKEN_TYPE, "");
    await sp.setInt(SP_EXPIRES_IN, -1);
    await sp.setBool(SP_IS_LOGIN, false);
  }
複製程式碼

原始碼

本篇相關的所有原始碼都在GitHub上demo-flutter-osc專案的v0.3分支

後記

本篇主要記錄的是基於Flutter的開源中國客戶端app中的網路請求和資料儲存方式,寫得不清楚的地方請多包涵,有問題可以留言告訴筆者。下一篇將記錄Flutter中的外掛使用。

我的開源專案

  1. 基於Google Flutter的開源中國客戶端,希望大家給個Star支援一下,原始碼:
從0開始寫一個基於Flutter的開源中國客戶端(7)——App網路請求和資料儲存 從0開始寫一個基於Flutter的開源中國客戶端(7)——App網路請求和資料儲存
  1. 基於Flutter的俄羅斯方塊小遊戲,希望大家給個Star支援一下,原始碼:
從0開始寫一個基於Flutter的開源中國客戶端(7)——App網路請求和資料儲存 從0開始寫一個基於Flutter的開源中國客戶端(7)——App網路請求和資料儲存
上一篇 下一篇
從0開始寫一個基於Flutter的開源中國客戶端(6)
——各個靜態頁面的實現
從0開始寫一個基於Flutter的開源中國客戶端(8)
——外掛的使用

相關文章