Provider: MultiProvider and two or more ChangeNotificationProviders

Created on 13 May 2019  路  10Comments  路  Source: rrousselGit/provider

The app works like this:
It has a Counter with ChangeNotifier and a ThemeSwitch also with ChangeNotifier
if number > 5 it changes to dark.
Consumer2 is used ( the logic for switching is made inside Consumer2).

I have this question too: How can I have access to Counter value inside the ThemeSwitch class to make the logic there.

The error happens when one of them is rebuilding the same descendants when the other is rebuilding at the same time.

Error:
Exception has occurred. FlutterError (setState() or markNeedsBuild() called during build. This ChangeNotifierProvider<ThemeSwitch> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase. The widget on which setState() or markNeedsBuild() was called was: ChangeNotifierProvider<ThemeSwitch>(state: _ListenableProviderState<ThemeSwitch>#e36d8) The widget which was currently being built when the offending call was made was: Consumer2<ThemeSwitch, Counter>(dirty, dependencies: [_Provider<ThemeSwitch>, _Provider<Counter>]))

Files:

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:provider_tutorial/counter.dart';
import 'package:provider_tutorial/theme_switch.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return
        // inject providers
        MultiProvider(
      providers: [
        ChangeNotifierProvider<Counter>(builder: (context) => Counter(0)),
        ChangeNotifierProvider<ThemeSwitch>(
          builder: (context) => ThemeSwitch(ThemeState.light),
        ),
      ],
      // 2 Consumers
      child: Consumer2<ThemeSwitch, Counter>(
        builder: (context, themeSwitch, counter, widget) {
          themeSwitch.themeState =
              (counter.value > 5) ? ThemeState.dark : ThemeState.light;
          return MaterialApp(
            title: 'Flutter Demo',
            theme: themeSwitch.theme,
            home: MyHomePage(),
          );
        },
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterProvider = Provider.of<Counter>(context);
    print(counterProvider.value.toString());

    return Scaffold(
      appBar: AppBar(
        title: Text('Providers Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'If Number > 5 theme is dark:',
            ),
            Consumer<Counter>(
              builder: (context, counter, widget) {
                return Text(
                  '${counter.value}',
                  style: Theme.of(context).textTheme.display1,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: () {
              counterProvider.increment();
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
          SizedBox(
            height: 20,
          ),
          FloatingActionButton(
            onPressed: () {
              counterProvider.decrement();
            },
            tooltip: 'Decrement',
            backgroundColor: Colors.red,
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

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

enum ThemeState { light, dark }

class ThemeSwitch with ChangeNotifier {
  ThemeSwitch(this._themeState);

  ThemeState _themeState;
  ThemeData _theme;

  ThemeState get themeState => _themeState;
  ThemeData get theme => _theme;

  set themeState(ThemeState themeState) {
    _themeState = themeState;

    if (_themeState == ThemeState.light) {
      _theme = ThemeData.light();
    } else {
      _theme = ThemeData.dark();
    }
    notifyListeners();
    print("theme state changed");
  }
}
// counter.dart
import 'package:flutter/foundation.dart';

class Counter with ChangeNotifier {
  Counter(this._value);
  int _value = 0;

  int get value => _value;

  set value(int value) {
    _value = value;
    notifyListeners();
  }

  increment() {
    value++;
  }

  decrement() {
    value--;
  }
}

Most helpful comment

That's not recommended.

But again, the use-case you're describing is a use-case for #46.
You may want to copy the implementation of ProxyProvider(source) from #59

And then do something like:

MultiProvider(
  providers: [
    ChangeNotifierProvider<Counter>(builder: (context) => Counter(0)),
    ProxyProvider<Counter, ThemeSwitch>.custom(
      builder: (context, counter, previous) {
        final theme = previous ?? ThemeSwitch(ThemeState.light);
        theme.themeState =
              (counter.value > 5) ? ThemeState.dark : ThemeState.light;
        return theme;
      },
      dispose: (context, value) => value.dispose(),
      providerBuilder: (_, value, child) =>
          ChangeNotifierProvider.value(notifier: value, child: child),
    ),
  ],
)

All 10 comments

That's another use-case for #46.
You may want to read #56 too.

Basically, you're mutating ThemeSwitch inside the consumer. That's not allowed.

Instead you should have ThemeSwitch listen to Counter.

@rrousselGit Please answer me this question, thank you for your time:

Let's say I instantiate Foo in the provider:

Provider(builder: (context)=>Foo());

If I want to use that initialized Foo() in a class (without context), should I first make it a singleton or static, then pass it to provider? Is there any helpers on the mixin ChangeNotifier that will transform the class in a singleton? I think that will be a nice feature if you want to build service classes.

I don't understand the question

Example:
I have a Dogs() class with ChangeNotifier mixin with provides me some vars.
I instante the class outside the UI because that Dogs() is needed by some other classes... So in order to make that happen I need to make it a Singleton (instantiated only once). This makes the instantiated classes the same. What I am asking is will it be possible to add another mixin that will transform it in a singleton class? So we can instantiate it on main() and get access anywhere?
ref : https://stackoverflow.com/a/55348216/1140807

Don't make singletons/globals. The point is this library is to purposefully avoid them.
Instead, you need some sort of binding between your widget and that non-widget class.
I'll need more information from your use-case to give a proper answer though.

Yes, that's why I got so confused... I don't want to use Singletons but I'm not getting how I can access the provider class outside the builder. The only way I found is to use it on didChangeDependencies() but I want to avoid logic inside the UI

Picking again the basic example above of Counter and SwitchTheme, both with ChangeNotifier.
If I want to add to the Counter class the counter value setter whenever value > 5 change the theme, I can't get the reference for the class that was instantiated in the UI.

The only way I found is to use it on didChangeDependencies() but I want to avoid logic inside the UI

That, or using Consumer and passing the value to a widget constructor.
This is fine.

Widgets are not UI. They are metadata.
It is fine for widgets to contain business logic.

If I pass the context like Counter(context) and use Provider.of inside , will it work?

That's not recommended.

But again, the use-case you're describing is a use-case for #46.
You may want to copy the implementation of ProxyProvider(source) from #59

And then do something like:

MultiProvider(
  providers: [
    ChangeNotifierProvider<Counter>(builder: (context) => Counter(0)),
    ProxyProvider<Counter, ThemeSwitch>.custom(
      builder: (context, counter, previous) {
        final theme = previous ?? ThemeSwitch(ThemeState.light);
        theme.themeState =
              (counter.value > 5) ? ThemeState.dark : ThemeState.light;
        return theme;
      },
      dispose: (context, value) => value.dispose(),
      providerBuilder: (_, value, child) =>
          ChangeNotifierProvider.value(notifier: value, child: child),
    ),
  ],
)

Great ! Thank your for your help/time! I'll appreciate

Was this page helpful?
0 / 5 - 0 ratings