Search code examples
flutterdartflutter-layoutflutter-providerprovider-model

How to show errors from ChangeNotifier using Provider in Flutter


I'm trying to find the best way to show errors from a Change Notifier Model with Provider through a Snackbar.

Is there any built-in way or any advice you could help me with?

I found this way that is working but I don't know if it's correct.

Suppose I have a simple Page where I want to display a list of objects and a Model where I retrieve those objects from api. In case of error I notify an error String and I would like to display that error with a SnackBar.

page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Page extends StatefulWidget {
  Page({Key key}) : super(key: key);

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

class _PageState extends State< Page > {

  @override
  void initState(){
    super.initState();
    Provider.of<Model>(context, listen: false).load();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    Provider.of< Model >(context, listen: false).addListener(_listenForErrors);
  }

  @override
  Widget build(BuildContext context){
    super.build(context);
    return Scaffold(
      appBar: AppBar(),
      body: Consumer<Model>(
          builder: (context, model, child){
  
            if(model.elements != null){
              ...list
            }
            else return LoadingWidget();
          }
        )
      )
    );
  }



  void _listenForErrors(){
    final error = Provider.of<Model>(context, listen: false).error;
    if (error != null) {
      Scaffold.of(context)
        ..hideCurrentSnackBar()
        ..showSnackBar(
          SnackBar(
            backgroundColor: Colors.red[600],
            content: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Icon(Icons.error),
                Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(error) )),
              ],
            ),
          ),
        );
    }
  }

  @override
  void dispose() { 
        Provider.of<PushNotificationModel>(context, listen: false).removeListener(_listenForErrors);
    super.dispose();
  }

}

page_model.dart

import 'package:flutter/foundation.dart';

class BrickModel extends ChangeNotifier {

  List<String> _elements;
  List<String> get elements => _elements;

  String _error;
  String get error => _error;

  Future<void> load() async {
    try{
      final elements = await someApiCall();
      _elements = [..._elements, ...elements];
    }
    catch(e) {
      _error = e.toString();
    }
    finally {
      notifyListeners();
    }
  }

}

Thank you


Solution

  • Edit 2022

    I ported (and reworked) this package also for river pod if anyone is interested

    https://pub.dev/packages/riverpod_messages/versions/1.0.0

    EDIT 2020-06-05

    I developed a slightly better approach to afford this kink of situations.

    It can be found at This repo on github so you can see the implementation there, or use this package putting in your pubspec.yaml

     provider_utilities:
        git:
          url: https://github.com/quantosapplications/flutter_provider_utilities.git
    

    So when you need to present messages to the view you can:

    1. extend your ChangeNotifier with MessageNotifierMixin that gives your ChangeNotifier two properties, error and info, and two methods, notifyError() and notifyInfo().

    2. Wrap your Scaffold with a MessageListener that will present a Snackbar when it gets called notifyError() or NotifyInfo()

    I'll give you an example:

    ChangeNotifier

    import 'package:flutter/material.dart';
    import 'package:provider_utilities/provider_utilities.dart';
    
    class MyNotifier extends ChangeNotifier with MessageNotifierMixin {
    
      List<String> _properties = [];
      List<String> get properties => _properties;
    
      Future<void> load() async {
    
        try {
          /// Do some network calls or something else
          await Future.delayed(Duration(seconds: 1), (){
    
            _properties = ["Item 1", "Item 2", "Item 3"];
            notifyInfo('Successfully called load() method');
    
          });
        }
        catch(e) {
          notifyError('Error calling load() method');
        }
    
      }
    
    }
    

    View

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:provider_utilities/provider_utilities.dart';
    
    import 'notifier.dart';
    
    class View extends StatefulWidget {
      View({Key key}) : super(key: key);
    
      @override
      _ViewState createState() => _ViewState();
    }
    
    class _ViewState extends State<View> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: MessageListener<MyNotifier>(
            child: Selector<MyNotifier, List<String>>(
              selector: (ctx, model) => model.properties,
              builder: (ctx, properties, child) => ListView.builder(
                itemCount: properties.length,
                itemBuilder: (ctx, index) => ListTile(
                  title: Text(properties[index])
                ),
              ),
            )
          )
        );
      }
    }
    

    OLD ANSWER

    thank you.

    Maybe I found a simpler way to handle this, using the powerful property "child" of Consumer.

    With a custom stateless widget (I called it ErrorListener but it can be changed :))

    class ErrorListener<T extends ErrorNotifierMixin> extends StatelessWidget {
    
      final Widget child;
    
      const ErrorListener({Key key, @required this.child}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Consumer<T>(
          builder: (context, model, child){
            
            //here we listen for errors
            if (model.error != null) { 
              WidgetsBinding.instance.addPostFrameCallback((_){
                 _handleError(context, model); });
            }
    
            // here we return child!
            return child;
          },
          child: child
        );
      }
    
    
      // this method will be called anytime an error occurs
      // it shows a snackbar but it could do anything you want
      void _handleError(BuildContext context, T model) {
        Scaffold.of(context)
        ..hideCurrentSnackBar()
        ..showSnackBar(
          SnackBar(
            backgroundColor: Colors.red[600],
            content: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Icon(Icons.error),
                Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(model.error) )),
              ],
            ),
          ),
        );
    
        // this will clear the error on model because it has been handled
        model.clearError();
      }
    }
    

    This widget must be put under a scaffold if you want to use a snackbar.

    I use a mixin here to be sure that model has a error property and a clarError() method.

    mixin ErrorNotifierMixin on ChangeNotifier {
      String _error;
      String get error => _error;
    
      void notifyError(dynamic error) {
        _error = error.toString();
        notifyListeners();
      }
    
      void clearError() {
        _error = null;
      }
    }
    

    So for example we can use this way

    class _PageState extends State<Page> {
    
       // ...
    
    @override 
      Widget build(BuildContext context) =>
        ChangeNotifierProvider(
          builder: (context) => MyModel(),
          child: Scaffold(
            body: ErrorListener<MyModel>(
              child: MyBody()
            )
          )
        );
    
    }