web-dev-qa-db-fra.com

BlocProvider.of () appelé avec un contexte qui ne contient pas de bloc - même s'il le fait

Tout d'abord, je sais comment BLoC suppose de fonctionner, l'idée derrière cela et je connais la différence entre les constructeurs BlocProvider() et BlocProvider.value().

Pour plus de simplicité, mon application dispose de 3 pages avec un arbre de widgets comme celui-ci:

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

Je veux que ma LoginPage() ait accès à UserBloc parce que je dois me connecter, etc. Pour ce faire, j'enroule LoginPage() builder à App() widget comme celui-ci:

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(),
      ),
    );
  }
}

Cela fonctionne évidemment très bien. Ensuite, si ser se connecte avec succès, il accède à HomePage. Maintenant, je dois avoir accès à deux blocs différents sur mon HomePage donc j'utilise MultiBlocProvider pour passer plus loin UserBloc et en créer un nouveau nommé DataBloc. Je le fais comme ça:

  @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(),
              ),
            ),
          );
        }
      },
[...]

Cela fonctionne également. Un problème se produit lorsque de HomePagetilisateur accède à UserTokensPage. A UserTokensPage j'ai besoin de mon constructeur UserBloc déjà existant que je veux passer avec le constructeur BlocProvider.value(). Je le fais comme ça:

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();
            }
        }
      },
[...]

Lorsque j'appuie sur le bouton pour accéder à MyTokensPage, j'obtiens une erreur avec le message:

════════ 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

Qu'est-ce que je fais mal? Est-ce parce que j'ai extrait le widget PopupMenuButton qui perd en quelque sorte des blocs? Je ne comprends pas ce que je peux faire de mal.

5
Stahp

Je l'ai corrigé. À l'intérieur de App widget que je crée LoginPage avec

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

À LoginPage j'enroule simplement BlocBuilders l'un dans l'autre

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 navigue ser to TokenPage avec

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

Et cela a résolu tous mes problèmes.

2
Stahp

Solution

Méthode A: accéder directement à l'instance du fournisseur UserBloc sans la passer

Je préfère cette solution car elle nécessite moins de code.

A.1 Enveloppez l'instance CustomPopupButton avec le fournisseur Consommateur afin qu'elle se reconstruise chaque fois que UserBloc notifie aux auditeurs les changements de valeur.

Change ça:

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

À:

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

A.2 Invocation de l'instance du fournisseur de changement à l'intérieur du widget sans état pour désactiver l'écoute des changements de valeur - "l'écoute" et les "reconstructions" résultantes sont déjà effectuées par Consumer.

A.2.1 Modifiez ceci:

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

À:

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

A.2.2 Et changez ceci:

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

À:

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

Méthode B: transmettre l'instance du fournisseur UserBloc

Même chose que la méthode A, mais:

  • Dans A.1, vous passeriez userBloc comme ceci: return CustomPopupButton(userBloc: userBloc),.
  • Vous déclareriez la propriété de membre final UserBloc userBloc; Dans CustomPopupButton.
  • Dans A.2, vous feriez ceci: userBloc.add(SignOut()); au lieu de BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());

Explication

flutter_bloc Utilise Provider , pour être conscient de ce qui se passe, il vaut mieux comprendre Provider. Veuillez vous référer à ma réponse ici pour comprendre ma réponse à votre question et pour mieux comprendre le fournisseur et le drapeau listen.

1
om-ha

Vous pouvez simplement envelopper les Blocs auxquels vous avez besoin d'accéder via l'application en les enveloppant au point d'entrée de l'application comme ceci

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

          ],
          child: App()
      )
  );
}

et vous pouvez accéder à ce bloc à n'importe quel endroit de votre application en

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

0
Amesh Fernando

vous n'avez pas besoin d'utiliser BlocProvider.value () pour naviguer vers un autre écran, vous pouvez simplement envelopper MaterialApp dans BlocProvider en tant qu'enfant

0
Tarek Mahfouz