- 開發flutter-ui過程當中,遇到了全局彈窗問題
- 友好的交互界面,可以產生更好的用戶體驗,好比查詢接口較久或須要耗時處理程序時,給個loading效果。
- flutter組件中showDialog彈窗組件,能知足彈窗需求,但使用過程可能不太順手。
- 源碼地址
將從如下幾點來分析與實現接口請求前的彈窗效果git
本文相關連接github
dependencies:
flutter:
sdk: flutter
dio: ^2.1.0 # dio依賴包 2019/03/30
複製代碼
lib
|--http #文件
|--index.dart # dio
|--loading.dart #loading
|--main.dart #入口
複製代碼
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,
}
複製代碼
查看showDialog源碼,調用順序是
showDialog -> showGeneralDialog -> Navigator.of(context, rootNavigator: true).push() context做爲參數,做用是提供給了Navigator.of(context, rootNavigator: true).push使用json
/// 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.
複製代碼
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事件內容bash
RaisedButton(
child: Text('close dialog'),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
複製代碼
這樣彈窗能夠經過按鈕控制關閉了併發
在觸發接口請求時,先調用showDialog觸發彈窗,接口請求完成關閉窗口app
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);
}
}
複製代碼
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;
}
}
複製代碼
解決了showDialog中的context,即能實現彈窗任意調用,不侷限於dio請求。context不是任意的,只在Scaffold中可以使Navigator.of(context)中找獲得Navigator對象。(剛接觸時不少時候會以爲一樣都是context,爲啥調用of(context)會報錯。)less
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對象ide
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。佈局
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 ...;
}
複製代碼
上述內容解決了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。
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 );
複製代碼
歡迎你們交流~