Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MultiProvider and two or more ChangeNotificationProviders #63

Closed
peekpt opened this issue May 13, 2019 · 10 comments
Closed

MultiProvider and two or more ChangeNotificationProviders #63

peekpt opened this issue May 13, 2019 · 10 comments

Comments

@peekpt
Copy link

peekpt commented May 13, 2019

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--;
  }
}
@rrousselGit
Copy link
Owner

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.

@peekpt
Copy link
Author

peekpt commented May 16, 2019

@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.

@rrousselGit
Copy link
Owner

I don't understand the question

@peekpt
Copy link
Author

peekpt commented May 16, 2019

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

@rrousselGit
Copy link
Owner

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.

@peekpt
Copy link
Author

peekpt commented May 16, 2019

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.

@rrousselGit
Copy link
Owner

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.

@peekpt
Copy link
Author

peekpt commented May 16, 2019

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

@rrousselGit
Copy link
Owner

rrousselGit commented May 16, 2019

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),
    ),
  ],
)

@peekpt
Copy link
Author

peekpt commented May 16, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants