上一篇中我記錄了基于Flutter的開源中國用戶端各個靜态頁面的實作,主要是UI的實作,沒有涉及到任何網絡請求,資料加載、存儲等方面。本篇記錄的是該項目中的網絡請求和資料存儲、加載的方式,希望自己在溫故知新的同時能給Flutter初學者帶來幫助。
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(, paramStr.length - );
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", );
sp.setBool("isLogin", false);
sp.setDouble("price", );
}
要擷取存儲的某個資料,隻需要使用
sp.get(key)
即可。
shared_preferences
插件的首頁在這裡。
關于插件的使用方法,這裡說明一下:https://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 * ,
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'] == ) {
// 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類中定義這個變量并初始化:
要監聽清單是否滾動到底的事件,還需要給這個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 = ;
getNewsList(false);
return null;
}
需要注意的是,onRefresh參數需要一個
Future<Null>
類型的資料,是以上面的_pullToRefresh才會傳回
Future<Null>
。
改造過後的資訊清單如下gif圖所示(圖比較大,加載會有點慢):
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicTMxgjN3QTPzZiZpdWPmZCMyQTPoZiNxITP39TMxITYkJmZkVmM1QjZ0YTMvwVMvwFOvwFOxAjMvw1bp5Sd0lGeu4GZj1CZs92ZtIXZzV3Lc9CX6MHc0RHaiojIsJye.jpg)
儲存登入後的使用者資料
由于擷取動彈資訊,評論動彈等,都需要調用開源中國的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, -);
await sp.setString(SP_TOKEN_TYPE, "");
await sp.setInt(SP_EXPIRES_IN, -);
await sp.setBool(SP_IS_LOGIN, false);
}
源碼
本篇相關的所有源碼都在GitHub上demo-flutter-osc項目的v0.3分支。
後記
本篇主要記錄的是基于Flutter的開源中國用戶端app中的網絡請求和資料存儲方式,寫得不清楚的地方請多包涵,有問題可以留言告訴筆者。下一篇将記錄Flutter中的插件使用。
我的開源項目
- 基于Google Flutter的開源中國用戶端,希望大家給個Star支援一下,源碼:
- GitHub
- 碼雲
- 基于Flutter的俄羅斯方塊小遊戲,希望大家給個Star支援一下,源碼:
- GitHub
- 碼雲