從0開始設計Flutter獨立APP | 第三篇: 一勞永逸解決全局BuildContext問題

鑑於Flutter的高性能渲染、跨平臺、多端一致性等優點,閃點清單在移動端APP上,使用了完整的Flutter框架來開發。既然是完整APP,架構搭建徹底不受歷史Native APP的影響,沒有歷史包袱的沉澱,設計也能更靈活和健壯。bash

全局BuildContext,幾乎是全部Flutter開發者的一個痛點。這個痛點有多痛呢?咱們來列舉一下場景:markdown

  1. 路由跳轉、彈窗、媒體查詢,所有依賴於BuildContext,若是在Service層(或其餘非UI層)作這些操做,必需要逐層傳遞正確的BuildContext實例。
  2. 依賴於BuildContext的邏輯,必須寫在某一個頁面的Widget初始化中,不然沒法拿到正確的BuildContext;而一些全局初始化的邏輯必需要寫在某一個頁面裏,而若是首次喚起的不是這個頁面,須要手動保證初始化邏輯不出問題。
  3. 獲取當前前臺頁面的路由,能夠用ModalRoute對象,但必須拿到目標頁面的BuildContext才能夠,Navigator的BuildContext是拿不到的。
  4. MediaQuery、Navigator、Overlays的BuildContext不是一個,不能用錯。
  5. Flutter絕大部分第三方UI庫是依賴於BuildContext,意味着你必需要在APP初始化後才能使用這些庫,即便是toast這樣的工具UI。
  6. 等等等等......

Flutter全局BuildContext解決方案

社區推薦方案

在Android中,咱們能夠用getApplicationContext解決全局context問題,Flutter官方並無提供建議的方案,不過社區有一些推薦的解決方案,好比使用GlobalKey的方案:架構

@override
Widget build(BuildContext context) {
  return MaterialApp(
    navigatorKey: globalNavigatorKey, // GlobalKey()
  )
}

globalNavigatorKey.currentState.push(
  MaterialPageRoute(builder: (context) => SomePage()),
);
複製代碼

首先咱們定義一個GlobalKey,而後在初始化MaterialApp的時候傳入navigatorKey,而後咱們在須要使用路由跳轉的地方,不使用原始的方式,而使用navigatorKey來調用:框架

globalNavigatorKey.currentState.push(...)
複製代碼

社區推薦方案的問題

看起來上述方案好像能夠解決問題,可是目前只能解決頁面路由跳轉問題,而若是使用Overlays(好比Dialog)、MediaQuery等就會出現問題了,error提示context不合法:less

The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
複製代碼

而直接使用navigatorKey.currentState.context獲取全局context也會出現一樣的error。ide

OneContext解決方案

在嘗試衆多方案都失敗後,咱們仍然在繼續尋找更好的方案,最終找到了OneContext方案,倉庫地址: one_context函數

Flutter全局BuildContext解決方案

OneContext是一個很是新的庫,2020年5月初才發第一個版本,目前還未發1.0版本。不過API的完成度仍是很高的。工具

使用方式

使用OneContext,首先咱們須要在MaterialApp中配置OneContext:性能

MaterialApp(
  builder: (BuildContext context, Widget child) {
    return OneContext().builder(context, child, initialRoute: 'home');
  },
  /// builder: OneContext().builder, /// 若是不須要initialRoute,可使用這種方式
  navigatorKey: OneContext().key,
)
複製代碼

而後,須要使用context的地方,所有經過OneContext來調用:ui

OneContext().pushNamed('calendar');

OneContext().showModalBottomSheet(
  builder: (BuildContext context) {
    return Container();
  },
);
OneContext().showDialog(...);
OneContext().addOverlay(...);
複製代碼

路由跳轉

OneContext().pushNamed('/second');
OneContext().push(MaterialPageRoute(builder: (_) => SecondPage()));
OneContext().pop();
複製代碼

Overlays操做

/// 展現ModalBottomSheet
OneContext().showModalBottomSheet(
  builder: (BuildContext context) {
    return Container();
  },
);

/// 添加移除覆蓋物
OneContext().addOverlay(
    overlayId: myCustomAndAwesomeOverlayId,
    builder: (_) => MyCustomAndAwesomeOverlay()
);

OneContext().removeOverlay(myCustomAndAwesomeOverlayId);

/// 加載提示
OneContext().showProgressIndicator();
OneContext().showProgressIndicator(
    backgroundColor: Colors.blue.withOpacity(.3),
    circularProgressIndicatorColor: Colors.white
);
OneContext().hideProgressIndicator();
複製代碼

主題和媒體查詢

print('Platform: ' + OneContext().theme.platform);
print('Orientation: ' + OneContext().mediaQuery.orientation);
複製代碼

主題模式修改

OneContext().oneTheme.toggleMode();

OneContext().oneTheme.changeDarkThemeData(
  ThemeData(
    primarySwatch: Colors.amber,
    brightness: Brightness.dark
 )
);
複製代碼

Flutter全局BuildContext解決方案

原理分析

從OneContext配置中,能夠看出來,OneContext最關鍵的一句配置是OneContext().builder,咱們點進去看源碼:

Widget builder(BuildContext context, Widget widget,
    {Key key,
    MediaQueryData mediaQueryData,
    String initialRoute,
    Route<dynamic> Function(RouteSettings) onGenerateRoute,
    Route<dynamic> Function(RouteSettings) onUnknownRoute,
    List<NavigatorObserver> observers = const <NavigatorObserver>[]}) =>
ParentContextWidget(
  child: widget,
  mediaQueryData: mediaQueryData,
  initialRoute: initialRoute,
  onGenerateRoute: onGenerateRoute,
  onUnknownRoute: onUnknownRoute,
  observers: observers,
);


class ParentContextWidget extends StatelessWidget {
  /// ...

  @override
  Widget build(BuildContext context) {
    return MediaQuery(
      data: mediaQueryData ?? MediaQuery.of(context),
      child: Navigator(
        initialRoute: initialRoute,
        onUnknownRoute: onUnknownRoute,
        observers: observers,
        onGenerateRoute: onGenerateRoute ??
            (settings) => MaterialPageRoute(
                builder: (context) => OneContextWidget(
                      child: child,
                    )),
      ),
    );
  }
}
複製代碼

從源碼中咱們能夠看到:

  • 在builder函數中,OneContext重寫了Widget結構中的MediaQuery和Navigator的初始化配置,並在每一個頁面的Widget外層包了一層OneContextWidget,而後就能夠在OneContextWidget拿到內層context,這個context能夠用於絕大部分場景。
  • 在OneContextWidget中,提供了Overlay的經常使用方法,並綁定了內部的context對象,從而解決Overlay的context獲取問題。
import 'package:flutter/material.dart';
import 'package:one_context/src/controllers/one_context.dart';

class OneContextWidget extends StatefulWidget {
  final Widget child;
  OneContextWidget({Key key, this.child}) : super(key: key);
  _OneContextWidgetState createState() => _OneContextWidgetState();
}

class _OneContextWidgetState extends State<OneContextWidget> {
  @override
  void initState() {
    super.initState();
    OneContext().registerDialogCallback(
        showDialog: _showDialog,
        showSnackBar: _showSnackBar,
        showModalBottomSheet: _showModalBottomSheet,
        showBottomSheet: _showBottomSheet);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Builder(
        builder: (innerContext) {
          OneContext().context = innerContext;
          return widget.child;
        },
      ),
    );
  }

  Future<T> _showDialog<T>(...){...}

  ScaffoldFeatureController<SnackBar, SnackBarClosedReason> _showSnackBar(...){ ... }

  Future<T> _showModalBottomSheet<T>(...){ ... }

  PersistentBottomSheetController<T> _showBottomSheet<T>(...) { ... }
}
複製代碼
  • OneContextWidget在每次build時,會更新全局context:
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Builder(
      builder: (innerContext) {
        OneContext().context = innerContext;
        return widget.child;
      },
    ),
  );
}
複製代碼

Flutter全局BuildContext解決方案

接入風險

  1. 接入OneContext後,務必對原有業務流程進行完成迴歸,尤爲是頁面返回邏輯(咱們就被坑了一次,Navigator.pop沒法正確關閉Dialog
  2. 頁面返回邏輯,Overlay的場景,須要使用OneContext().popDialog()代替Navigator.pop,切記切記。

總結

到目前咱們解決了Flutter全局BuildContext的問題,但這其實並不該該是最終的方案,OneContext是一個侵入性比較高的方案,Flutter官方應該提供更好的方案來解決這個問題。

講到這裏,還並無完成基礎框架的搭建,後面咱們會講解更多的Flutter架構設計內容,好比:通知、分享、UI設計等等。


持續分享閃點清單在Flutter上的開發經驗。閃點清單,一款懸浮清單軟件:

閃點清單,一款懸浮清單軟件
相關文章
相關標籤/搜索