從0開(kāi)始設(shè)計(jì)Flutter獨(dú)立APP | 第三篇: 一勞永逸解決全局BuildContext問(wèn)題

鑒于Flutter的高性能渲染、跨平臺(tái)、多端一致性等優(yōu)勢(shì),閃點(diǎn)清單在移動(dòng)端APP上,使用了完整的Flutter框架來(lái)開(kāi)發(fā)。既然是完整APP,架構(gòu)搭建完全不受歷史Native APP的影響,沒(méi)有歷史包袱的沉淀,設(shè)計(jì)也能更靈活和健壯。

全局BuildContext,幾乎是所有Flutter開(kāi)發(fā)者的一個(gè)痛點(diǎn)。這個(gè)痛點(diǎn)有多痛呢?我們來(lái)列舉一下場(chǎng)景:

  1. 路由跳轉(zhuǎn)、彈窗、媒體查詢,全部依賴于BuildContext,如果在Service層(或其他非UI層)做這些操作,必須要逐層傳遞正確的BuildContext實(shí)例。
  2. 依賴于BuildContext的邏輯,必須寫(xiě)在某一個(gè)頁(yè)面的Widget初始化中,否則無(wú)法拿到正確的BuildContext;而一些全局初始化的邏輯必須要寫(xiě)在某一個(gè)頁(yè)面里,而如果首次喚起的不是這個(gè)頁(yè)面,需要手動(dòng)保證初始化邏輯不出問(wèn)題。
  3. 獲取當(dāng)前前臺(tái)頁(yè)面的路由,可以用ModalRoute對(duì)象,但必須拿到目標(biāo)頁(yè)面的BuildContext才可以,Navigator的BuildContext是拿不到的。
  4. MediaQuery、Navigator、Overlays的BuildContext不是一個(gè),不能用錯(cuò)。
  5. Flutter絕大部分第三方UI庫(kù)是依賴于BuildContext,意味著你必須要在APP初始化后才能使用這些庫(kù),即使是toast這樣的工具UI。
  6. 等等等等......
Flutter全局BuildContext解決方案

社區(qū)推薦方案

在Android中,我們可以用getApplicationContext解決全局context問(wèn)題,F(xiàn)lutter官方并沒(méi)有提供建議的方案,不過(guò)社區(qū)有一些推薦的解決方案,比如使用GlobalKey的方案:

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

globalNavigatorKey.currentState.push(
  MaterialPageRoute(builder: (context) => SomePage()),
);

首先我們定義一個(gè)GlobalKey,然后在初始化MaterialApp的時(shí)候傳入navigatorKey,然后我們?cè)谛枰褂寐酚商D(zhuǎn)的地方,不使用原始的方式,而使用navigatorKey來(lái)調(diào)用:

globalNavigatorKey.currentState.push(...)

社區(qū)推薦方案的問(wèn)題

看起來(lái)上述方案好像可以解決問(wèn)題,但是目前只能解決頁(yè)面路由跳轉(zhuǎn)問(wèn)題,而如果使用Overlays(比如Dialog)、MediaQuery等就會(huì)出現(xiàn)問(wèn)題了,error提示context不合法:

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也會(huì)出現(xiàn)同樣的error。

OneContext解決方案

在嘗試眾多方案都失敗后,我們?nèi)匀辉诶^續(xù)尋找更好的方案,最終找到了OneContext方案,倉(cāng)庫(kù)地址: one_context。

Flutter全局BuildContext解決方案

OneContext是一個(gè)非常新的庫(kù),2020年5月初才發(fā)第一個(gè)版本,目前還未發(fā)1.0版本。不過(guò)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的地方,全部通過(guò)OneContext來(lái)調(diào)用:

OneContext().pushNamed('calendar');

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

路由跳轉(zhuǎn)

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配置中,可以看出來(lái),OneContext最關(guān)鍵的一句配置是OneContext().builder,我們點(diǎn)進(jìn)去看源碼:

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函數(shù)中,OneContext重寫(xiě)了Widget結(jié)構(gòu)中的MediaQuery和Navigator的初始化配置,并在每個(gè)頁(yè)面的Widget外層包了一層OneContextWidget,然后就可以在OneContextWidget拿到內(nèi)層context,這個(gè)context可以用于絕大部分場(chǎng)景。
  • 在OneContextWidget中,提供了Overlay的常用方法,并綁定了內(nèi)部的context對(duì)象,從而解決Overlay的context獲取問(wèn)題。
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時(shí),會(huì)更新全局context:
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Builder(
      builder: (innerContext) {
        OneContext().context = innerContext;
        return widget.child;
      },
    ),
  );
}
Flutter全局BuildContext解決方案

接入風(fēng)險(xiǎn)

  1. 接入OneContext后,務(wù)必對(duì)原有業(yè)務(wù)流程進(jìn)行完成回歸,尤其是頁(yè)面返回邏輯(我們就被坑了一次,Navigator.pop無(wú)法正確關(guān)閉Dialog
  2. 頁(yè)面返回邏輯,Overlay的場(chǎng)景,需要使用OneContext().popDialog()代替Navigator.pop,切記切記。

總結(jié)

到目前我們解決了Flutter全局BuildContext的問(wèn)題,但這其實(shí)并不應(yīng)該是最終的方案,OneContext是一個(gè)侵入性比較高的方案,F(xiàn)lutter官方應(yīng)該提供更好的方案來(lái)解決這個(gè)問(wèn)題。

講到這里,還并沒(méi)有完成基礎(chǔ)框架的搭建,后面我們會(huì)講解更多的Flutter架構(gòu)設(shè)計(jì)內(nèi)容,比如:通知、分享、UI設(shè)計(jì)等等。


持續(xù)分享閃點(diǎn)清單在Flutter上的開(kāi)發(fā)經(jīng)驗(yàn)。閃點(diǎn)清單,一款懸浮清單軟件:

閃點(diǎn)清單,一款懸浮清單軟件
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容