vlambda博客
学习文章列表

从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局BuildContext问题

鉴于Flutter的高性能渲染、跨平台、多端一致性等优势,闪点清单在移动端APP上,使用了完整的Flutter框架来开发。既然是完整APP,架构搭建完全不受历史Native APP的影响,没有历史包袱的沉淀,设计也能更灵活和健壮。


全局BuildContext,几乎是所有Flutter开发者的一个痛点。这个痛点有多痛呢?我们来列举一下场景:


  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. 等等等等......



社区推荐方案


在Android中,我们可以用`getApplicationContext`解决全局context问题,Flutter官方并没有提供建议的方案,不过社区有一些推荐的解决方案,比如使用GlobalKey的方案:

@overrideWidget 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不合法:

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。


OneContext解决方案


在尝试众多方案都失败后,我们仍然在继续寻找更好的方案,最终找到了OneContext方案,pub.dev搜索one_context。


从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局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来调用:

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操作


/// 展示ModalBottomSheetOneContext().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(    primarySwatchColors.amber,    brightnessBrightness.dark ));


从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局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:

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

从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局BuildContext问题


接入风险

  1. 接入OneContext后,务必对原有业务流程进行完成回归,尤其是页面返回逻辑(我们就被坑了一次,`Navigator.pop`无法正确关闭`Dialog`)

  2. 页面返回逻辑,Overlay的场景,需要使用`OneContext().popDialog()`代替`Navigator.pop`,切记切记。

总结


到目前我们解决了Flutter全局BuildContext的问题,但这其实并不应该是最终的方案,`OneContext`是一个侵入性比较高的方案,Flutter官方应该提供更好的方案来解决这个问题。


讲到这里,还并没有完成基础框架的搭建,后面我们会讲解更多的Flutter架构设计内容,比如:通知、分享、UI设计等等。





持续分享闪点清单在Flutter上的开发经验。闪点清单,一款悬浮清单软件:


从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局BuildContext问题







End






从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局BuildContext问题
从0开始设计Flutter独立APP | 第三篇: 一劳永逸解决全局BuildContext问题

关注“闪点君”
随时与我们交流