How to show errors from ChangeNotifier using Provider in Flutter
Asked Answered
P

2

16

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

Pounds answered 3/11, 2019 at 22:11 Comment(1)
I was facing this exact same problem yesterday. I couldn't come up with a clean way to display snackbars when using provider.Lakesha
P
12

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

}
Pounds answered 7/11, 2019 at 7:36 Comment(4)
That's a nice approach! Thanks for sharing :)Lakesha
I like this approach, but what about if I use MultiProvider? That would changes a lot or not?Workable
@IgnacioGiagante I currently use this with MultiProvider and there's no problem about that. You can listen to one or more changenotifiers wrapping your scaffold with the listener. I developed a slightly better version of this approach that you can find in github.com/quantosapplications/flutter_provider_utilities. You can extend ChangeNotifier with MessageNotifierMixin that gives you notifyError and notifyInfo methods. Than you can wrap a Scaffold with MessageListener that will listen and present a snacker when you notify error or infoPounds
Have you considered publishing this as a package on pub.devTartaric
L
7

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),
          ),
        ],
      ),
    );
  }
}
Lakesha answered 6/11, 2019 at 8:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.