【问题标题】:BlocProvider.of() called with a context that does not contain a Bloc - even that it doesBlocProvider.of() 使用不包含 Bloc 的上下文调用 - 即使它包含
【发布时间】:2020-05-11 03:07:03
【问题描述】:

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

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

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

我希望我的LoginPage() 能够访问UserBloc,因为我需要登录用户等。为此,我将LoginPage() builder 包装在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 user 导航到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 小部件,它以某种方式丢失了块吗?我不明白我做错了什么。

【问题讨论】:

  • 为什么需要 BlocListener 来导航到下一页?我认为这可以使用BlocBuilder以更简单的方式完成,并在UserAuthenticated时返回您需要返回的页面
  • @StefanoSaitta 我这样做是因为这是官方 flutter_bloc documentation 为 BlocListeners 推荐的做法。
  • @Stahp 试试我的答案,让我知道结果。

标签: flutter navigation bloc flutter-bloc


【解决方案1】:

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

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

          ],
          child: App()
      )
  );
}

并且您可以通过

在应用程序的任何位置访问此块

BlocProvider.of&lt;UserBloc&gt;(context).add(event of user bloc());

【讨论】:

  • 是否必须在 main.dart 文件中以这种方式实现每个 Bloc?你能解释一下我们为什么要这样做吗?你的回答救了我。谢谢
  • 否,但您不能在当前页面(您当前所在的页面)上提供它。您可以在导航到特定页面之前或在 main.dart 文件中提供它。
  • 其简单而真实的解决方案。谢谢!
  • 该死的简单解决方案。与这些上下文一起工作真是太糟糕了。
【解决方案2】:

我修好了。在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(),
                ),
              ),
            ),
          );
        }
      },
[...]

PopupMenuButtonUser 导航到 TokenPage

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

这解决了我所有的问题。

【讨论】:

    【解决方案3】:

    解决方案

    方法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 在无状态小部件内调用 Change Provider 实例以禁用监听值更改——“监听”和由此产生的“重建”已经由 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&lt;UserBloc&gt;(context, listen: false).add(SignOut());

    说明

    flutter_bloc 正在使用Provider,要知道发生了什么,最好了解 Provider。请参考我的回答here了解我对你的问题的回答,更好的理解Provider和listenflag。

    【讨论】:

    • 试试这个解决方案,让我知道它是否有效。无法测试它。如果您提供了您的 Provider 类型实现,那就太好了。
    • 我尝试实施您的解决方案。不幸的是,flutter_bloc Provider.of(context) 没有 listen 参数,我可以将其设置为 false,如 A.2.1 所述。此外,在使用 ConsumerBloc 包装 CustomPopupButton(不更改 Provider.of() )之后,CustomPopup 小部件崩溃并且不呈现,因为“使用不包含 UserBloc 类型的 Bloc 的上下文调用 BlocProvider.of()。”。似乎在 LoginPage 处创建 UserBloc 已经失败,但由于 MultiBlocProvider 它不会使应用程序崩溃。
    • @Stahp 你很幸运。 flutter_bloc 已经是 doing that。所以你不需要指定listen: false,因为它已经为你完成了。当我找到一些可能的修复时,我会更新我的答案,并在完成后通知你。
    【解决方案4】:

    在构建器中更改上下文名称,无论是在 bottomSheet 还是 materialPageRoute 中。

    这样 bloc 就可以通过上下文访问父上下文 除非它将从构建器(底部表)中获取上下文。这可能导致 到您无法访问 bloc 实例的错误。

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

    【讨论】:

    • 这很有帮助。谢谢!
    【解决方案5】:

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

    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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        猜你喜欢
        • 2021-02-08
        • 2020-10-25
        • 2021-05-02
        • 2020-12-26
        • 2020-10-16
        • 2020-05-20
        • 2020-08-21
        • 2020-11-15
        • 2021-06-10
        相关资源
        最近更新 更多