How to show errors from ChangeNotifier using Provider in Flutter

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

}

You can create a custom StatelessWidget to launch the snackbar when the view model changes. For example:

class SnackBarLauncher extends StatelessWidget {
  final String error;

  const SnackBarLauncher(
      {Key key, @required this.error})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (error != null) {
      WidgetsBinding.instance.addPostFrameCallback(
          (_) => _displaySnackBar(context, error: error));
    }
    // Placeholder container widget
    return Container();
  }

  void _displaySnackBar(BuildContext context, {@required String error}) {
    final snackBar = SnackBar(content: Text(error));
    Scaffold.of(context).hideCurrentSnackBar();
    Scaffold.of(context).showSnackBar(snackBar);
  }
}

We can only display the snackbar once all widgets are built, that's why we have the WidgetsBinding.instance.addPostFrameCallback() call above.

Now we can add SnackBarLauncher to our screen:

class SomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Title',
        ),
      ),
      body: Stack(
        children: [
          // Other widgets here...

          Consumer<EmailLoginScreenModel>(
            builder: (context, model, child) =>
                SnackBarLauncher(error: model.error),
          ),
        ],
      ),
    );
  }
}