Flutter 全局彈窗

背景

  • 開發flutter-ui過程當中,遇到了全局彈窗問題
  • 友好的交互界面,可以產生更好的用戶體驗,好比查詢接口較久或須要耗時處理程序時,給個loading效果。
  • flutter組件中showDialog彈窗組件,能知足彈窗需求,但使用過程可能不太順手。
  • 源碼地址

將從如下幾點來分析與實現接口請求前的彈窗效果git

  • showDialog介紹
  • 實現簡單彈窗
  • 接入dio package
  • 彈窗關鍵點分析
  • 實現全局存儲context
  • 實現dio請求時loading
  • 併發請求時loading處理

本文相關連接github

準備

  • 新建項目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使用json

  • 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事件內容bash

RaisedButton(
  child: Text('close dialog'),
  onPressed: () {
    Navigator.of(context, rootNavigator: true).pop();
  },
),
複製代碼

這樣彈窗能夠經過按鈕控制關閉了併發

接入dio

在觸發接口請求時,先調用showDialog觸發彈窗,接口請求完成關閉窗口app

  • 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)會報錯。)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。佈局

實現全局存儲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. 你將會看到以下效果。

Alt 預覽

併發請求時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 );
複製代碼

歡迎你們交流~

相關文章
相關標籤/搜索