How should logic in a BLoC trigger a one-time UI alert, like a dialog or snackbar?
Asked Answered
M

3

8

When using the BLoC pattern to isolate business logic from the view (without the bloc library), how can we trigger a one-time UI call (like showing a snackbar or an alert dialog) when some asynchronous logic has finished in a BLoC? For example, if a network request fails, we want to show a dialog informing the user that some data cannot be fetched.

The options I've explored are:

  1. Use a StreamBuilder in the widget's build method to listen for an event from the bloc, and show the dialog in the StreamBuilder's build method. The problem with this approach is that the StreamBuilder's build method would not actually be building anything here. We'd just be asking Flutter to show a dialog. We could return some invisible "fake" widget just to make Flutter happy, but this is really hacky.

  2. Make the widget stateful. In initState, subscribe to a stream of errors from the BLoC, and show the dialog in the listener. Here we maintain the decoupling of logic/view, but as far as I can tell, this option is not even viable because we do not have the BuildContext needed to show the dialog.

  3. When the widget invokes some logic on the BLoC which might result in a change that should trigger a dialog, pass in a callback which shows the dialog. Of the options listed here, I prefer this the one. Logic and view are still pretty separate, but it feels a little odd. The flow of data in the BLoC pattern is typically purely "widget -> BLoC via sinks or functions" and "BLoC -> widget via streams". It's a violation of this pattern to have the widget ask the BLoC to invoke a callback (passing data back with it) after some logic has executed.

I've been avoiding using the bloc library (to keep it simple, among other reasons). However, it does seem to offer a solution to this problem in the form of its BlocListener. Is there a simple way to solve this problem while maintaining the core principles of BLoC?

Mazzola answered 30/1, 2020 at 18:28 Comment(0)
M
1

After looking into StatefulWidget some more, I see that we actually do have access to BuildContext context in initState. Option #2 is therefore viable after all. It allows us to keep the logic and view separate, and maintains the pattern that data only flows from the BLoC to the widget using streams.

To reiterate the solution: the BLoC can expose a stream of data (e.g. an error code) which the stateful widget can subscribe to in the State's initState. The listener can use the context property to show the dialog when the stream emits some data.

Mazzola answered 30/1, 2020 at 18:52 Comment(0)
J
2

I was just working on this exact problem. This was my solution - it falls into category #2:

class CurrencyStreamListener extends StatefulWidget {
  final Stream<CurrencyNotification> notificationStream;
  final Widget child;
  CurrencyStreamListener({this.notificationStream, this.child});

  @override
  _CurrencyStreamListenerState createState() => _CurrencyStreamListenerState();
}

class _CurrencyStreamListenerState extends State<CurrencyStreamListener> {
  @override
  void initState() {
    widget.notificationStream.listen((CurrencyNotification data) {
      if (data is NotificationOfIncrease) {
        Scaffold.of(context).showSnackBar(
          SnackBar(
            duration: Duration(seconds: 1),
            content: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Earned Currency'),
              ],
            ),
          ),
        );
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

This way I can nest this widget wherever I have access to the Bloc's stream, so under some Provider or Consumer class probably.

Jarlathus answered 30/1, 2020 at 19:56 Comment(0)
M
1

After looking into StatefulWidget some more, I see that we actually do have access to BuildContext context in initState. Option #2 is therefore viable after all. It allows us to keep the logic and view separate, and maintains the pattern that data only flows from the BLoC to the widget using streams.

To reiterate the solution: the BLoC can expose a stream of data (e.g. an error code) which the stateful widget can subscribe to in the State's initState. The listener can use the context property to show the dialog when the stream emits some data.

Mazzola answered 30/1, 2020 at 18:52 Comment(0)
D
1

What I do is: In my bloc state I have a property that holds a Snackbar object that can be null.

If I need to show some message, I set it to a value, otherwise, I set it to null.

In my bloc listener, I check if it's not null and display it.

To ensure it'll be triggered only once, the first line in my mapEventToState is to yield a new state where the message property is null.

Duston answered 16/9, 2020 at 14:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.