BlocProvider.of() 在不包含 Bloc 的上下文中调用 - 即使它包含

BlocProvider.of() called with a context that does not contain a Bloc - even that it does

首先,我确实知道 BLoC 应该如何工作,它背后的想法,我知道 BlocProvider()BlocProvider.value() 构造函数之间的区别。

为简单起见,我的应用程序有 3 个页面,其中的小部件树如下:

App() => LoginPage() => HomePage() => UserTokensPage()

我希望我的 LoginPage() 能够访问 UserBloc,因为我需要登录用户等。为此,我将 LoginPage() 构建器包装在 App() 小部件中像这样:

void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: BlocProvider<UserBloc>(
        create: (context) => UserBloc(UserRepository()),
        child: LoginPage(),
      ),
    );
  }
}

这显然工作得很好。然后,如果 User 成功登录,他将被导航到 HomePage。现在,我需要在 HomePage 访问两个不同的区块,所以我使用 MultiBlocProvider 进一步传递现有的 UserBloc 并创建一个名为 DataBloc 的全新区块。我是这样做的:

  @override
  Widget build(BuildContext context) {
    return BlocListener<UserBloc, UserState>(
      listener: (context, state) {
        if (state is UserAuthenticated) {
          Navigator.of(context).push(
            MaterialPageRoute<HomePage>(
              builder: (_) => MultiBlocProvider(
                providers: [
                  BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                  ),
                  BlocProvider<DataBloc>(
                    create: (_) => DataBloc(DataRepository()),
                  ),
                ],
                child: HomePage(),
              ),
            ),
          );
        }
      },
[...]

这也行。从 HomePage 用户 导航到 UserTokensPage 时会出现问题。在 UserTokensPage 我需要我已经存在的 UserBloc 我想用 BlocProvider.value() 构造函数传递。我是这样做的:

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: false,
        title: Text('My App'),
        actions: <Widget>[
          CustomPopupButton(),
        ],
      ),

[...]

class CustomPopupButton extends StatelessWidget {
  const CustomPopupButton({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      icon: Icon(Icons.more_horiz),
      onSelected: (String choice) {
        switch (choice) {
          case PopupState.myTokens:
            {
              Navigator.of(context).push(
                MaterialPageRoute<UserTokensPage>(
                  builder: (_) => BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                    child: UserTokensPage(),
                  ),
                ),
              );
            }
            break;
          case PopupState.signOut:
            {
              BlocProvider.of<UserBloc>(context).add(SignOut());
              Navigator.of(context).pop();
            }
        }
      },
[...]

当我按下按钮导航到 MyTokensPage 时,我收到错误消息:

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Builder(dirty):
        BlocProvider.of() called with a context that does not contain a Bloc of type UserBloc.

        No ancestor could be found starting from the context that was passed to BlocProvider.of<UserBloc>().

        This can happen if:
        1. The context you used comes from a widget above the BlocProvider.
        2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.

        Good: BlocProvider<UserBloc>(create: (context) => UserBloc())
        Bad: BlocProvider(create: (context) => UserBloc()).

        The context used was: CustomPopupButton

我做错了什么?是因为我提取了 PopupMenuButton 小部件以某种方式丢失了 blocs 吗?我不明白我做错了什么。

解决方案

方法 A:直接访问 UserBloc 提供者实例而不传递它

我更喜欢这个解决方案,因为它需要的代码更少。

A.1 使用提供程序 Consumer 包装 CustomPopupButton 实例,因此只要 UserBloc 通知侦听器值更改,它就会自行重建。

改变这个:

actions: <Widget>[
    CustomPopupButton(),
],

收件人:

actions: <Widget>[
    Consumer<UserBloc>(builder: (BuildContext context, UserBloc userBloc, Widget child) {
      return CustomPopupButton(),
    });
],

A.2 更改无状态小部件内的 Provider 实例调用以禁用侦听值更改 -- "listening" 和结果 "rebuilds" 已由 Consumer 完成。

A.2.1 更改:

value: BlocProvider.of<UserBloc>(context),

收件人:

value: BlocProvider.of<UserBloc>(context, listen: false),

A.2.2 并更改为:

BlocProvider.of<UserBloc>(context).add(SignOut());

收件人:

BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());

方法 B:传递 UserBloc 提供商实例

与方法 A 相同,但是:

  • 在 A.1 中,您将像这样传递 userBlocreturn CustomPopupButton(userBloc: userBloc),.
  • 您将在 CustomPopupButton.
  • 中声明 final UserBloc userBloc; 成员 属性
  • 在 A.2 中你会这样做:userBloc.add(SignOut()); 而不是 BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());

说明

flutter_bloc 正在使用 Provider, to be aware what's going on it's better understand Provider. Please refer to my answer 来理解我对你问题的回答,并更好地理解 Provider 和 listen 标志。

编辑 2022 年 3 月 10 日

既然这个帖子很受欢迎,我觉得我需要添加一些评论。

如果您的目标是使用 不是 MaterialApp 小部件上方提供的块,而是通过包装在小部件树下的某处声明,则这是有效的解决方案您的小部件(例如某些页面)BlocProvider 使该小部件可以访问该集团。

通过在小部件树上方的 MultiBlocProvider 某处声明所有 bloc 可以更容易地避免问题(就像我之前说的那样),但是创建本主题时并没有考虑到这一点。随意投票并使用 Amesh Fernando 回复中描述的这种方法,但要知道其中的区别。


我修好了。在 App 小部件中,我用

创建了 LoginPage
home: BlocProvider<UserBloc>(
        create: (context) => UserBloc(UserRepository()),
        child: LoginPage(),

LoginPage 我只是将 BlocBuilders 一个包裹到另一个

Widget build(BuildContext context) {
    return BlocListener<UserBloc, UserState>(
      listener: (context, state) {
        if (state is UserAuthenticated) {
          Navigator.of(context).push(
            MaterialPageRoute<HomePage>(
              builder: (_) => BlocProvider.value(
                value: BlocProvider.of<UserBloc>(context),
                child: BlocProvider<NewRelicBloc>(
                  create: (_) => NewRelicBloc(NewRelicRepository()),
                  child: HomePage(),
                ),
              ),
            ),
          );
        }
      },
[...]

PopupMenuButton 使用

User 导航到 TokenPage
              Navigator.of(context).push(
                MaterialPageRoute<UserTokensPage>(
                  builder: (_) => BlocProvider.value(
                    value: BlocProvider.of<UserBloc>(context),
                    child: UserTokensPage(),
                  ),
                ),
              );

这解决了我所有的问题。

您不必使用 BlocProvider.value() 导航到另一个屏幕,您可以将 MaterialApp 作为它的子项包装到 BlocProvider 中

您可以像这样在应用程序的入口点包装您需要通过应用程序访问的 Blocs

  runApp(
      MultiBlocProvider(
          providers: [
            BlocProvider<UserBloc>(
              create: (context) =>
                  UserBloc(UserRepository()),
            ),

          ],
          child: App()
      )
  );
}

并且您可以通过

在您的应用程序的任何位置访问此集团

BlocProvider.of<UserBloc>(context).add(event of user bloc());

在 bottomSheet 或 materialPageRoute 中更改构建器中上下文的名称。

这样bloc就可以通过上下文访问父上下文 除非它要从构建器中获取上下文(底部 sheet)。这可以导致 一个你无法到达 bloc 实例的错误。

showModalBottomSheet(
     context: context,
     builder: (context2) {  ===> change here to context2
     BlocProvider.value(
          value: BlocProvider.of<BlocA>(context),
          child: widgetA(),
                       ),
     }

您需要将小部件分解为两个小部件(出于可测试性原因我建议这样做)或使用构建器小部件来获取子上下文。

class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (_) => TestCubit(), child: MyHomeView(), ); } } class MyHomeView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: RaisedButton(onPressed: () => BlocProvider.of<TestCubit>(context)...) ), ); } }

来源:由 Felix Angelov 解决,https://github.com/felangel/bloc/issues/2064