引言:什么是 context
flutter 中的概念 '万物皆 widget '。flutter 的UI布局由一个个 widget 叠加组合而成,而每一个 widget 都会对应 一个 Element, Element 类内部会实现 BuildContext 接口。编码中,我们使用的 context 指向 widget 树中的具体 UI 节点。
context 在项目中的使用场景
在 flutter 编程中,我们常用 context 来实现如下功能:
- navigator 导航进行页面的跳转。
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PageA(),
),
)
- 弹窗 Dialog
showDialog(context: context, builder: (context) {
return Dialog(
child: Page(),
);
})
- 获取主题配置
final themeData = Theme.of(context);
- 添加 Overlay 图层 (常用于自定义 loading,toast 等能力支持)
Overlay.of(context)?.insert(? extends OverlayEntry);
如何维护一个全局 context
利用 MaterialApp 节点的 navigatorKey 属性,维护一个适用于全局的 BuildContext。在需要使用 context 的地方可直接使用全局维护的 context 进行页面跳转,showLoading,toast 等功能实现。
创建一个维护 GlobalContext 的工具类 NavigatorProvider
(可直接copy使用)
import 'package:flutter/material.dart';
/// 用于提供全局的 navigatorContext
class NavigatorProvider {
final GlobalKey _navigatorKey = new GlobalKey(debugLabel: 'Rex');
static final NavigatorProvider _instance = NavigatorProvider._();
NavigatorProvider._();
/// 赋值给根布局的 materialApp 上
/// navigatorKey.currentState.pushName('url') 可直接用于跳转
static GlobalKey get navigatorKey => _instance._navigatorKey;
/// 可用于 跳转,overlay-insert(toast,loading) 使用
static BuildContext? get navigatorContext => _instance._navigatorKey.currentState?.context;
}
在 MaterialApp 根节点上进行应用注册 navigatorKey
void main() {
runApp(
MaterialApp( //为什么这里会嵌套两层 MaterialApp,我们在下面进行解说
home: MaterialApp(
navigatorKey: NavigatorProvider.navigatorKey,
routes: {
'/pageA': (BuildContext context) => PageA(),
},
home: Scaffold(
body: TestNavigatorWidget(),
),
),
),
);
}
TestNavigatorWidget
是小编写的一个测试页面,页面内包含两个功能按键(1. 跳转页面。 2. toast )
class TestNavigatorWidget extends StatelessWidget {
const TestNavigatorWidget({Key? key}) : super(key: key);
///跳转页面
void _jumpPageA() {
NavigatorProvider.navigatorKey.currentState?.pushNamed('/pageA');
}
///toast
void _overLayToast() {
final globalContext = NavigatorProvider.navigatorContext;
if (globalContext != null) {
ToastUtils.toast('首页的 toast', globalContext);
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _overLayToast,
child: Text('toast'),
),
TextButton(
onPressed: _jumpPageA,
child: Text('跳转下一个页面'),
),
],
),
),
);
}
}
class PageA extends StatelessWidget {
const PageA({Key? key}) : super(key: key);
///toast
void _overLayToast() {
final globalContext = NavigatorProvider.navigatorContext;
if (globalContext != null) {
ToastUtils.toast('PageA的 toast', globalContext);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('PageA')),
body: Center(
child: TextButton(
onPressed: _overLayToast,
child: Text('PageA toast'),
),
),
);
}
}
效果图如下:
代码分析
-
- 可直接使用全局 context 进行页面跳转
NavigatorProvider.navigatorKey.currentState?.pushNamed('/pageA')
- 可直接使用全局 context 进行页面跳转
-
-
ToastUtils
是小编封装的 toast 工具类,借助三方库fluttertoast: ^8.0.9
使用 overlay 图层添加的原理实现。工具类中使用的 context 正是在根节点中注册的NavigatorProvider
提供的全局 context。
-
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class ToastUtils {
late FToast _fToast;
static ToastUtils _instance = ToastUtils._();
ToastUtils._() {
_fToast = FToast();
}
static void toast(String message, BuildContext context) {
_instance._fToast.init(context);
_instance._fToast.showToast(
child: _ToastEntry(message),
gravity: ToastGravity.BOTTOM,
toastDuration: Duration(seconds: 2),
);
}
///toast使用的UI
class _ToastEntry extends StatelessWidget {
final String message;
const _ToastEntry(this.message, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 12.0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
color: Colors.greenAccent,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check),
SizedBox(width: 12.0),
Text(message),
],
),
);
}
}
使用这段代码的同学别忘了在 pubspec.yaml 文件中添加三方库依赖哟 ~
fluttertoast: ^8.0.9
-
- 细心的同学已经发现了,小编在 main方法中,app 的根节点使用了两层 MaterialApp 进行嵌套,这是由于 Overlay 的添加特性造成的。
overlay 的 insert 操作最终会转换成 Stack 布局,而实际上 insert 添加的图层是在所提供context 对应节点的父级节点上进行操作。
因为这个特性,如果我们要使用一个全局的 context 用于操作 overlay ,那么就要求这个全局的 context 需要拥有一个父节点。
如果仅仅是使用全局context进行导航操作(跳转、dialog),则无需使用两层 MaterialApp 进行嵌套。