从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上的开发经验。闪点清单,一款悬浮清单软件:

闪点清单,一款悬浮清单软件
相关文章
相关标签/搜索