背景
- 開發flutter-ui過程中,遇到了全域性彈窗問題
- 友好的互動介面,能夠產生更好的使用者體驗,比如查詢介面較久或需要耗時處理程式時,給個loading效果。
- flutter元件中showDialog彈窗元件,能滿足彈窗需求,但使用過程可能不太順手。
- 原始碼地址
將從以下幾點來分析與實現介面請求前的彈窗效果
- showDialog介紹
- 實現簡單彈窗
- 接入dio package
- 彈窗關鍵點分析
- 實現全域性儲存context
- 實現dio請求時loading
- 併發請求時loading處理
本文相關連結
準備
- 新建專案flutter create xxx (有專案就用自己專案,影響的地方不大)
- pubspec.yaml增加dio依賴包
dependencies:
flutter:
sdk: flutter
dio: ^2.1.0 # dio依賴包 2019/03/30
複製程式碼
- 建立http資料夾與http/index.dart, http/loading.dart檔案
lib
|--http #檔案
|--index.dart # dio
|--loading.dart #loading
|--main.dart #入口
複製程式碼
showDialog介紹
showDialog{
@required BuildContext context,
bool barrierDismissible = true,
@Deprecated(
'Instead of using the "child" argument, return the child from a closure '
'provided to the "builder" argument. This will ensure that the BuildContext '
'is appropriate for widgets built in the dialog.'
) Widget child,
WidgetBuilder builder,
}
複製程式碼
- builder:建立彈窗的元件,這些可以建立需要的互動內容
- context:上下文,這裡只要打通了,就能實現全域性。這是關鍵
檢視showDialog原始碼,呼叫順序是
showDialog -> showGeneralDialog -> Navigator.of(context, rootNavigator: true).push() context作為引數,作用是提供給了Navigator.of(context, rootNavigator: true).push使用
- showGeneralDialog的註釋內容,介紹了關閉彈窗的重點
/// The dialog route created by this method is pushed to the root navigator.
/// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
/// dialog rather than just `Navigator.pop(context, result)`.
///
/// See also:
///
/// * [showDialog], which displays a Material-style dialog.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
複製程式碼
實現簡單彈窗
- demo中floatingActionButton中_incrementCounter事件,事件觸發後顯示彈窗,具體內容可結合程式碼註解
void _incrementCounter() {
showDialog(
context: context,
builder: (context) {
// 用Scaffold返回顯示的內容,能跟隨主題
return Scaffold(
backgroundColor: Colors.transparent, // 設定透明背影
body: Center( // 居中顯示
child: Column( // 定義垂直佈局
mainAxisAlignment: MainAxisAlignment.center, // 主軸居中佈局,相關介紹可以搜下flutter-ui的內容
children: <Widget>[
// CircularProgressIndicator自帶loading效果,需要寬高設定可在外加一層sizedbox,設定寬高即可
CircularProgressIndicator(),
SizedBox(
height: 10,
),
Text('loading'), // 文字
// 觸發關閉視窗
RaisedButton(
child: Text('close dialog'),
onPressed: () {
print('close');
},
),
],
), // 自帶loading效果,需要寬高設定可在外加一層sizedbox,設定寬高即可
),
);
},
);
}
複製程式碼
點選後出來了彈窗了,這一切還沒有結束,只是個開始。 關閉彈窗,點選物理返回鍵就後退了。(尷尬不) 在上面showDialog介紹中最後提供了一段關於showGeneralDialog的註釋程式碼,若需要關閉視窗,可以通過呼叫 Navigator.of(context, rootNavigator: true).pop(result)。 修改下RaisedButton事件內容
RaisedButton(
child: Text('close dialog'),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
複製程式碼
這樣彈窗可以通過按鈕控制關閉了
接入dio
在觸發介面請求時,先呼叫showDialog觸發彈窗,介面請求完成關閉視窗
- http/index.dart 實現get介面請求,同時增加interceptors,接入onRequest、onResponse、onError函式,虛擬碼如下
import 'package:dio/dio.dart' show Dio, DioError, InterceptorsWrapper, Response;
Dio dio;
class Http {
static Dio instance() {
if (dio != null) {
return dio;// 例項化dio
}
dio = new Dio();
// 增加攔截器
dio.interceptors.add(
InterceptorsWrapper(
// 介面請求前資料處理
onRequest: (options) {
return options;
},
// 介面成功返回時處理
onResponse: (Response resp) {
return resp;
},
// 介面報錯時處理
onError: (DioError error) {
return error;
},
),
);
return dio;
}
/**
* get介面請求
* path: 介面地址
*/
static get(path) {
return instance().get(path);
}
}
複製程式碼
- http/loading.dart 實現彈窗,dio在onRequest時呼叫 Loading.before,onResponse/onError呼叫Loading。complete完畢視窗,虛擬碼如下
import 'package:flutter/material.dart';
class Loading {
static void before(text) {
// 請求前顯示彈窗
// showDialog();
}
static void complete() {
// 完成後關閉loading視窗
// Navigator.of(context, rootNavigator: true).pop();
}
}
// 彈窗內容
class Index extends StatelessWidget {
final String text;
Index({Key key, @required this.text}):super(key: key);
@override
Widget build(BuildContext context) {
return xxx;
}
}
複製程式碼
彈窗關鍵點分析
context
解決了showDialog中的context,即能實現彈窗任意呼叫,不侷限於dio請求。context不是任意的,只在Scaffold中能夠使Navigator.of(context)中找得到Navigator物件。(剛接觸時很多時候會覺得同樣都是context,為啥呼叫of(context)會報錯。)
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('main${Navigator.of(context)}'); // !!!這裡發報錯!!!
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
... // 省略其它內容
}
錯誤內容如下:
I/flutter ( 9137): Navigator operation requested with a context that does not include a Navigator.
I/flutter ( 9137): The context used to push or pop routes from the Navigator must be that of a widget that is a
I/flutter ( 9137): descendant of a Navigator widget.
即在MaterialApp中未能找到。
複製程式碼
讓我們在_MyHomePageState中檢視下build返回Scaffold時,context物件內容是否有Navigator物件
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print('home${Navigator.of(context)}'); // 正常列印NavigatorState#600dc(tickers: tracking 1 ticker)
}
... // 省略其它內容
}
複製程式碼
所以全域性彈窗的context,需要scaffold中的context。專案初始時在build第一次返回scaffold元件前,把context全域性儲存起來,提供能showDialog使用。(第一次返回沒有侷限,只要在呼叫showDiolog呼叫前全域性儲存context即可,自行而定。),至此可以解決了dio中呼叫showDialog時,context經常運用錯誤導致報錯問題。
擴充套件分析flutter-ui中與provide結合使用後遇到的context。 flutter-ui先通過Store.connect封裝provide資料層,這裡的context返回的provide例項的上下文,接著return MaterialApp中,這裡的上下文也是MaterialApp本身的,這些都沒法使用Navigator物件,最終在build Scaffold時,通過Provide資料管理提前setWidgetCtx,全域性儲存Scaffold提供的context。
實現全域性儲存context
1 在http/loading.dart檔案的Loading類暫存一個context靜態變數。
class Loading {
static dynamic ctx;
static void before(text) {
// 請求前顯示彈窗
// showDialog(context: ctx, builder: (context) {
// return Index(text:text);
// );
}
static void complete() {
// 完成後關閉loading視窗
// Navigator.of(ctx, rootNavigator: true).pop();
}
}
複製程式碼
2 在main.dart中_MyHomePageState build函式返回前注入Loading.ctx = context; 為了便於區別,我們使用ctx來儲存
import 'package:flutter_loading/http/loading.dart' show Loading;
... // 省略部分程式碼
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print('home $context');
print('home ${Navigator.of(context)}');
Loading.ctx = context; // 注入context
return ...;
}
複製程式碼
實現dio請求時loading
上述內容解決了context關鍵點。接下來實現介面互動。點選按鈕,呼叫dio.get介面拉取資料,在onRequest前呼叫Loading.before(); onResponse呼叫Loading.complete()進行關閉。
import 'package:flutter/material.dart';
class Loading {
static dynamic ctx;
static void before(text) {
// 請求前顯示彈窗
showDialog(
context: ctx,
builder: (context) {
return Index(text: text);
},
);
}
static void complete() {
// 完成後關閉loading視窗
Navigator.of(ctx, rootNavigator: true).pop();
}
}
複製程式碼
修改下dio的內容,介面請求返回較快時,為了看到loading效果,故在onResponse增加了Future.delayed,延遲3s返回資料。
import 'package:dio/dio.dart' show Dio, DioError, InterceptorsWrapper, Response;
import 'loading.dart' show Loading;
Dio dio;
class Http {
static Dio instance() {
if (dio != null) {
return dio;// 例項化dio
}
dio = new Dio();
// 增加攔截器
dio.interceptors.add(
InterceptorsWrapper(
// 介面請求前資料處理
onRequest: (options) {
Loading.before('正在加速中...');
return options;
},
// 介面成功返回時處理
onResponse: (Response resp) {
// 這裡為了讓資料介面返回慢一點,增加了3秒的延時
Future.delayed(Duration(seconds: 3), () {
Loading.complete();
return resp;
});
},
// 介面報錯時處理
onError: (DioError error) {
return error;
},
),
);
return dio;
}
/**
* get介面請求
* path: 介面地址
*/
static get(path) {
return instance().get(path);
}
}
複製程式碼
修改下_incrementCounter函式的內容為通過http.get觸發介面呼叫
import 'package:flutter/material.dart';
import 'package:flutter_loading/http/loading.dart' show Loading;
import 'http/index.dart' show Http;
... // 省略程式碼
void _incrementCounter() {
// Loading.before('loading...');
Http.get('https://raw.githubusercontent.com/efoxTeam/flutter-ui/master/version.json');
}
... // 省略程式碼
複製程式碼
ok. 你將會看到如下效果。
併發請求時loading處理
併發請求,loading只需要保證有一個在當前執行。介面返回結束,只需要保證最後一個完成時,關閉loading。
- 使用Set有排重作用,比較使用用來管理併發請求地址。通過Set.length控制彈窗與關閉視窗。
- 增加LoadingStatus判斷是否已經有彈窗存在
- 修改onRequest/onResponse/onError入參
import 'package:flutter/material.dart';
Set dict = Set();
bool loadingStatus = false;
class Loading {
static dynamic ctx;
static void before(uri, text) {
dict.add(uri); // 放入set變數中
// 已有彈窗,則不再顯示彈窗, dict.length >= 2 保證了有一個執行彈窗即可,
if (loadingStatus == true || dict.length >= 2) {
return ;
}
loadingStatus = true; // 修改狀態
// 請求前顯示彈窗
showDialog(
context: ctx,
builder: (context) {
return Index(text: text);
},
);
}
static void complete(uri) {
dict.remove(uri);
// 所有介面介面返回並有彈窗
if (dict.length == 0 && loadingStatus == true) {
loadingStatus = false; // 修改狀態
// 完成後關閉loading視窗
Navigator.of(ctx, rootNavigator: true).pop();
}
}
}
複製程式碼
http/index.dart
onReuest: Loading.before(options.uri, '正在加速中...');
onReponse: Loading.complete(resp.request.uri);
onError: Loading.complete(error.request.uri );
複製程式碼
歡迎大家交流~