Flutter中BlocProvider.value是否多余?为何无需它也能传递Cubit?
我正在学习Flutter中用于状态管理和路由的Bloc模式,线上课程和官方文档都提到,在使用Navigator.push类方法导航时,必须用BlocProvider.value向新创建的子树提供已有的Bloc或Cubit。但我实际测试发现,不用BlocProvider.value也能把Cubit传递给新页面,想知道这是哪里的原因,是我哪里操作错了,还是无意中符合了某种逻辑?
代码示例
main.dart 路由配置
routes: { "/": (context) => const MyHomePage(title: 'Flutter Demo Home Page'), "/second": (context) => const SecondScreen(title: "Second screen"), "/third": (context) => const ThirdScreen(title: "Third screen") },
HomeScreen 代码
class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: BlocConsumer<CounterCubit, CounterState>( listener: (context, state) { if (state.isIncremented) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text("Incremented"))); } else { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text("Decremented"))); } }, builder: (context, state) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), BlocBuilder<CounterCubit, CounterState>( builder: (context, state) { return Text( '${state.counterValue}', style: Theme.of(context).textTheme.headlineMedium, ); }, ), Row( children: [ FloatingActionButton( heroTag: UniqueKey(), onPressed: () { BlocProvider.of<CounterCubit>(context).decrement(); }, tooltip: 'Increment', child: const Icon(Icons.text_decrease), ), FloatingActionButton( heroTag: UniqueKey(), onPressed: () { BlocProvider.of<CounterCubit>(context).increment(); }, tooltip: 'Increment', child: const Icon(Icons.add), ), ], ), MaterialButton( onPressed: () { Navigator.pushNamed(context, '/second'); }, color: Colors.blue, child: const Text("Navigate to Second Screen"), ), MaterialButton( onPressed: () { Navigator.pushNamed(context, '/third'); }, color: Colors.blue, child: const Text("Navigate to Third Screen"), ), ], ), ); }, ), ); } }
SecondScreen 代码
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../cubit/counter_cubit.dart'; class SecondScreen extends StatefulWidget { const SecondScreen({super.key, required this.title}); final String title; @override State<SecondScreen> createState() => _SecondScreenState(); } class _SecondScreenState extends State<SecondScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: BlocConsumer<CounterCubit, CounterState>( listener: (context, state) { if (state.isIncremented) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text("Incremented"))); } else { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text("Decremented"))); } }, builder: (context, state) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), BlocBuilder<CounterCubit, CounterState>( builder: (context, state) { return Text( '${state.counterValue}', style: Theme.of(context).textTheme.headlineMedium, ); }, ), Row( children: [ FloatingActionButton( heroTag: UniqueKey(), onPressed: () { BlocProvider.of<CounterCubit>(context).decrement(); }, tooltip: 'Increment', child: const Icon(Icons.text_decrease), ), FloatingActionButton( heroTag: UniqueKey(), onPressed: () { BlocProvider.of<CounterCubit>(context).increment(); }, tooltip: 'Increment', child: const Icon(Icons.add), ), ], ) ], ), ); }, ), ); } }
ThirdScreen 代码
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../cubit/counter_cubit.dart'; class ThirdScreen extends StatefulWidget { const ThirdScreen({super.key, required this.title}); final String title; @override State<ThirdScreen> createState() => _ThirdScreenState(); } class _ThirdScreenState extends State<ThirdScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: BlocConsumer<CounterCubit, CounterState>( listener: (context, state) { if (state.isIncremented) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text("Incremented"))); } else { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text("Decremented"))); } }, builder: (context, state) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), BlocBuilder<CounterCubit, CounterState>( builder: (context, state) { return Text( '${state.counterValue}', style: Theme.of(context).textTheme.headlineMedium, ); }, ), Row( children: [ FloatingActionButton( heroTag: UniqueKey(), onPressed: () { BlocProvider.of<CounterCubit>(context).decrement(); }, tooltip: 'Increment', child: const Icon(Icons.text_decrease), ), FloatingActionButton( heroTag: UniqueKey(), onPressed: () { BlocProvider.of<CounterCubit>(context).increment(); }, tooltip: 'Increment', child: const Icon(Icons.add), ), ], ) ], ), ); }, ), ); } }
原因分析
你之所以不用BlocProvider.value也能在新页面获取到Cubit,核心原因是你的CounterCubit是在整个应用的根节点(比如MaterialApp上方)提供的。
Flutter的BuildContext是树形结构,BlocProvider会把Bloc/Cubit注入到当前上下文的子树中。如果CounterCubit是在MaterialApp之外或者直接包裹MaterialApp的BlocProvider中创建的,那么MaterialApp下的所有页面(包括通过Navigator打开的路由页面)都属于这个BlocProvider的子树,自然就能通过BlocProvider.of<CounterCubit>(context)获取到同一个实例。
为什么官方文档推荐用BlocProvider.value?
官方建议的场景是:当Bloc/Cubit是在某个页面(比如MyHomePage)内部提供的,而非全局根节点时。此时新页面的上下文不属于该页面的子树,无法直接获取Bloc/Cubit,这时候才需要用BlocProvider.value把已有实例传递给新路由的子树。
比如如果你的CounterCubit是在MyHomePage的build方法中用BlocProvider(create: (_) => CounterCubit())提供的,那么SecondScreen和ThirdScreen作为独立路由页面,它们的上下文不在MyHomePage的子树里,不用BlocProvider.value就会报错。
你的代码隐含逻辑
从你的代码能推断出,你应该是在main.dart最外层做了全局的BlocProvider配置,类似这样:
void main() { runApp( BlocProvider( create: (context) => CounterCubit(), child: const MyApp(), ), ); }
这种情况下,所有路由页面都在全局BlocProvider的子树范围内,所以不用额外传递就能获取到Cubit。
注意事项
虽然当前方式能正常工作,但如果Cubit不需要全局共享,建议遵循官方推荐的BlocProvider.value方式,避免不必要的全局状态污染。只有确实需要全局共享的状态(比如用户登录状态),才适合放在根节点提供。
内容的提问来源于stack exchange,提问作者Demir

