首先道個歉,最近公司很忙,又趕上十一假期,所以鴿了將近半個月。
不過,後續還是會每週最少更新兩篇的!
那說起網路請求的控制元件,我們首先是不是會想起 FutureBuilder
?
FutureBuilder
給我們封裝好了網路請求中的各種狀態。
如果沒有了解過,那麼可以看我這篇文章:Flutter - FutureBuilder 非同步UI神器。
這篇文章是早期寫的,有些地方寫的有些問題,但不重要!主要了解一下 FutureBuilder
的狀態就可以了。
本篇文章中只是提供一種思路,歡迎一起探討,也歡迎不吝賜教!
效果如下。
首先是沒有開啟服務的情況:
可以看到全部都是錯誤的資訊,
然後開啟服務:
1. 先定義一個通用的網路請求
那既然是網路請求,那首先我們要定義一個通用的網路請求方法。
每一家後臺 API 的風格都不一樣,有的是 RSETful,有的是我們最熟悉的 GET、POST。
這裡就以 GET 為例,API 介面為 GitHub - 網易雲音樂 Node.js API service。
網路請求使用的是 Dio,先建立一個 NetUtils.dart
。
初始化程式碼:
static Dio _dio;
static void init() async {
Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path;
CookieJar cj = PersistCookieJar(dir: tempPath);
_dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:3000'))
..interceptors.add(CookieManager(cj))
..interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
}
複製程式碼
在 runApp 前面呼叫即完成初始化。
接著定義一個通用的網路請求:
static Future<Response> _get(
BuildContext context,
String url, {
Map<String, dynamic> params,
}) async {
Loading.showLoading(context);
try {
return await _dio.get(url, queryParameters: params);
} on DioError catch (e) {
if (e.response is Map) {
return Future.value(e.response);
} else {
return Future.error(0);
}
} finally {
Loading.hideLoading(context);
}
}
複製程式碼
這裡程式碼很簡單,方法需要傳入三個引數:
- context:用於 showLoading
- url:API 地址
- params:該網路請求的引數,可以為空
方法內部我們捕獲了 DioError
,然後判斷介面是否還返回了正常的內容。
例如:狀態碼不為2xx,但是仍然返回了資料,這樣 Dio 是會丟擲 DioError
的,需要我們自己捕獲來處理。
如果返回了正常的資料,那我們還是返回回去,如果不是正常的資料,則直接丟擲 Future.error(0)
。
使用該通用方法:
/// 新碟上架
static Future<AlbumData> getAlbumData(
BuildContext context, {
Map<String, dynamic> params = const {
'offset': 0,
'limit': 10,
},
}) async {
var response = await _get(context, '/top/album', params: params);
return AlbumData.fromJson(response.data);
}
複製程式碼
我們就可以像這樣來使用剛才定義好的方法,也方便我們後續定義一個通用的 FutureBuilder
。
2. 確認網路請求控制元件所需要的功能
我們從最開始的圖中明顯能看出來的,其實是有三個功能:
- 請求資料並顯示 Loading
- 正常時返回正常資料,錯誤時返回錯誤 Widget
- 錯誤 Widget 可以點選重新請求
然鵝,細心的同學也發現問題了。
我們在網路請求中新增了一個 Loading,而且需要一個 BuildContext。
我們都知道,是不能在 initState()
方法中去使用這個 BuildContext 的。
所以,我們還要進行一個 第一幀回撥:
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((call) {
_request();
});
}
複製程式碼
這樣就完成了我們的需求調研。
3. 編寫通用網路請求控制元件
說的是一個通用的網路請求控制元件,其實就是把 FutureBuilder
封裝一層。
請求資料並顯示 Loading
但是,這裡也有一個問題:
我們在最開始定義網路請求工具類的時候,每一個網路請求都是一個方法,而每個方法中都有或者沒有引數。
我們也知道,FutureBuilder
需要傳入一個 Future,那這可怎麼辦?
而且我們不能在使用該控制元件的時候呼叫網路請求方法,因為網路請求中封裝了一個 Loading,這個 Loading 需要 BuildContext
。
既然如此,那我們只能傳入方法(Function)了:
typedef ValueWidgetBuilder<T> = Widget Function(
BuildContext context,
T value,
);
final ValueWidgetBuilder<T> builder;
final Function futureFunc;
final Map<String, dynamic> params;
CustomFutureBuilder({
@required this.futureFunc,
@required this.builder,
this.params,
});
複製程式碼
這樣,我們就可以在 第一幀回撥 中來呼叫該網路請求了,這樣一舉兩得:
既不用在使用該控制元件的時候呼叫方法,又避免了 Loading 使用 BuildContext 報錯的問題。
Future<T> _future;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((call) {
_request();
});
}
void _request() {
setState(() {
if (widget.params == null)
_future = widget.futureFunc(context);
else
_future = widget.futureFunc(context, params: widget.params);
});
}
複製程式碼
首先我們定義了一個 Future,然後在 第一幀回撥 中初始化該 Future 就可以了。
正常時返回正常資料,錯誤時返回錯誤 Widget
這就需要我們封裝好的網路請求和 FutureBuilder
有一個互動了,
網路請求的邏輯如下:
這樣正好就可以對應 FutureBuilder
的幾種狀態:
- 網路請求 ->
ConnectionState.none
、ConnectionState.waiting
- 顯示Loading ->
ConnectionState.active
- 請求結束 ->
ConnectionState.done
- 是否有資料(無論對錯)->
snapshot.hasData
- 丟擲錯誤 ->
snapshot.hasError
瞭解這些之後,我們就可以寫出程式碼:
Widget build(BuildContext context) {
return _future == null
? Container(
alignment: Alignment.center,
height: ScreenUtil().setWidth(200),
child: CupertinoActivityIndicator(),
)
: FutureBuilder(
future: _future,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return Container(
alignment: Alignment.center,
height: ScreenUtil().setWidth(200),
child: CupertinoActivityIndicator(),
);
case ConnectionState.done:
if (snapshot.hasData) {
return widget.builder(context, snapshot.data);
} else if (snapshot.hasError) {
return NetErrorWidget(
callback: () {
_request();
},
);
}
}
return Container();
},
);
}
複製程式碼
首先判斷 _future 是否為 null,如果為空,那麼則表示還沒有初始化該 Future,
個人建議這個時候返回自己定義好的載入中 Widget,因為後續在網路請求中的時候也返回該 Widget,這樣不會顯得亂。
然後在 ConnectionState.done
中判斷是否存在資料,如果有的話,就顯示傳進來的 Widget。
如果返回錯誤,則返回錯誤的 Widget。
錯誤 Widget 可以點選重新請求
這個邏輯其實很簡單,在我最開始說的文章中有講解一部分。
那就是什麼時候 FutureBuilder 會重新建立?
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
複製程式碼
可以很清晰的看到,在兩次 Future 不一樣的情況下會重新走一遍流程。否則是不會走的。
而我們在上面也已經定義好了,因為傳進來的是 Function 和 Params,我們可以隨時重新建立該 Future:
void _request() {
setState(() {
if (widget.params == null)
_future = widget.futureFunc(context);
else
_future = widget.futureFunc(context, params: widget.params);
});
}
複製程式碼
錯誤 Widget 的點選事件寫成這個就 ok 了,這樣就重新建立了該 FutureBuilder
,也就是重新請求了。
總結
程式碼的話,我就不傳上去了,因為這個只適用於一部分。
我這裡只是提供一種思路,個人覺得還是不錯的。
如果有什麼想法的話,歡迎一起探討,不吝賜教!
另我個人建立了一個「Flutter 交流群」,可以新增我個人微信 「17610912320」來入群。
推薦閱讀: