stream_listener_widget 1.0.0 copy "stream_listener_widget: ^1.0.0" to clipboard
stream_listener_widget: ^1.0.0 copied to clipboard

A simple widget that listen streams without rebuilding its child (ex. to show a dialog)

stream_listener_widget #

A simple widget that listen streams without rebuilding its child (ex. to show a dialog)

Keep it simple #

  • A simple logic class (ViewModel, Controller, etc) should not make use of BuildContext
  • Actually it should not trigger any popup or navigation because it's View's responsibility

The StreamListener widget is as simple as Flutter's StreamBuilder widget.

  • It allows you to react to the given streams' events without rebuilding any widget
  • And it will clean up memory for you by cancelling all the given streams' subscriptions

Here's what you should avoid to do (handling navigation, popup, etc in your logic class):

    class MyController {
        BuildContext context;
        
        Future<void> submitForm(String email, String password) {
            try {
                await authenticationApi.login(email, password);
                Navigator.of(context).pushNamed('/homePage');
            } catch(e) {
                showDialog(
                  context: context,
                  barrierDismissible: true,
                  builder: (context) => Dialog(child: Text('An unknown error occured')),
                );
            }
        }
    }
    
    
    class MyView extends StatefulWidget {
      const MyView({super.key});
    
      @override
      State<MyView> createState() => _MyViewState();
    }
    
    class _MyViewState extends State<MyView> {
      final logic = MyController();
    
      @override
      Widget build(BuildContext context) {
        logic.context = context; // That's really ugly
        return Scaffold(
            floatingActionButton: FloatingActionButton(
              onPressed: () => logic.submitForm('my_email', 'my_password'),
              child: const Text('Submit'),
            ),
          );
      }
    }

But wait ?! The example above is shorter !

  • Yes ! But we haven't separated View logic & Business logic
  • Our controller is doing everything while the View is doing nothing
  • Responsibilities and dependencies are mixed up
  • Your controller has been designed for this specific View
  • BuildContext is used after a Future/async gap which is forbidden because it can be unstable

Instead your logic could just trigger some events handled by the View:

  • Our view handle View's logic
  • Our logic class handle Business logic
  • Responsibilities & dependencies are well separated
  • Our controller has been Domain Driven Designed and can be used elsewhere :)
  • By defining a Type to logic's events the Business logic is clear
  • The code is stable and testable
    /// Here's some Type classes that defines possible domain/logic's events
    sealed class MyEvent {}
    class LoginSuccess extends MyEvent {}
    class LoginError extends MyEvent {}
    
    class MyController {
        final controller = StreamController<MyEvent>.broadcast();
        
        Future<void> submitForm(String email, String password) {
            try {
                await authenticationApi.login(email, password);
                controller.add(LoginSuccess());
            } catch(e) {
                controller.add(LoginError());
            }
        }
        
        void dispose() => controller.close();
    }
    
    class MyView extends StatefulWidget {
      /// We let the possibility to override the default controller for testability
      final MyController? controller;
      
      const MyView({super.key, this.controller});
    
      @override
      State<MyView> createState() => _MyViewState();
    }
    
    class _MyViewState extends State<MyView> {
      late final logic = widget.controller ?? MyController();
    
      void _onError(LoginError e) {
        showDialog(
          context: context,
          barrierDismissible: true,
          builder: (context) => Dialog(child: Text('An unknown error occured')),
        );
      }
    
      void _onLoginSuccess(LoginSuccess e) {
        Navigator.of(context).pushNamed('/homePage');
      }
    
      @override
      void dispose() {
        logic.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return StreamListener(
          listeners: [
            (context) => logic.controller.stream.whereType<LoginSuccess>().listen(_onMaxReached),
            (context) => logic.controller.stream.whereType<LoginError>().listen(_onError),
          ],
          child: Scaffold(
            floatingActionButton: FloatingActionButton(
              onPressed: () => logic.submitForm('my_email', 'my_password'),
              child: const Text('Submit'),
            ),
          ),
        );
      }
    }
5
likes
160
points
24
downloads

Publisher

unverified uploader

Weekly Downloads

A simple widget that listen streams without rebuilding its child (ex. to show a dialog)

Repository (GitLab)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on stream_listener_widget