passing data to provider - setState() or markNeedsBuild() called during build - flutter

I'm fetching data from the local database using SQflite in my main.dart and passing it to ProvideRecords widget using FutureProvider :
Future<List<WeightRecord>> _getRecords() async {
List<WeightRecord> records = await RecordsDatabase.instance.getRecords();
return records;
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureProvider<List<WeightRecord>?>(
create: (context) {
return _getRecords();
},
initialData: [],
catchError: (_, error) => [
WeightRecord(
date: DateTime.now(), weight: 00.0, note: 'hasError: $error')
],
child: ProvideRecords(),
),
);
}
}
then in the ProvideRecords widget I pass the data again to another provider :
class ProvideRecords extends StatelessWidget {
const ProvideRecords({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<List<WeightRecord>?>(builder: (context, list, child) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => RecordsListModel()),
ChangeNotifierProvider(create: (context) => ButtonMode())
],
builder: (context, child) {
Provider.of<RecordsListModel>(context, listen: true)
.updateRecordsList(list);
return Home(list: list);
});
}
});
}
}
the code works but I'm getting setState() or markNeedsBuild() called during build. because I'm using Provider.of<RecordsListModel>(context, listen: true).updateRecordsList(list); in the builder function. However I couldn't find another way of passing the data from FutureProvider to the RecordListModel, what can I do?

Its mainly an issue of how you have structured your code
Its recommended to have your multiproviders at the apps entry point..
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
//my providers here
ChangeNotifierProvider(create: (context) => ButtonMode())
... material app
then when adding data no need to set listen to true
Provider.of<RecordsListModel>(context, listen: false)
.updateRecordsList(list);
since home depends on data from the above provider use a consumer
Home(list: list);
// consume your provider
Consumer<RecordsListModel>(
builder:
(context, RecordsListModel recordsP, child) {
return Home(list:recordsP.list);
},
)

Related

My changenotifierprovider is not updating. Not sure why

Here is my change Notifier class.
class UserChamaNotifier with ChangeNotifier {
final List<UserChama> _userChamaList = [];
UnmodifiableListView<UserChama> get userchamaListy =>
UnmodifiableListView(_userChamaList);
void addUserChama(UserChama userchama) {
_userChamaList.add(userchama);
notifyListeners();
}
}
I have created the provider in main.dart:
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => _appStateManger,
),
ChangeNotifierProvider(
create: (context) => _profileManager,
),
ChangeNotifierProvider(
create: (context) => UserChamaNotifier(),
)
],
Then I proceed to add a chama object to my list:
UserChama userChama =
UserChama(id: s['Id'], phone: s['Phone'], name: s['Name']);
print(userChama.phone);
Provider.of<UserChamaNotifier>(context).addUserChama(userChama);
Here i try to access the list through the provider:
class ChamaList extends StatelessWidget {
const ChamaList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
UserChamaNotifier userChamaNotifier =
Provider.of<UserChamaNotifier>(context, listen: true);
return Text(userChamaNotifier.userchamaListy.length.toString());
}
}
At this point, i have experimented alot and i still don't have the correct way of implementation.
While adding data, set listen:false
Provider.of<UserChamaNotifier>(context,listen:false)
.addUserChama(userChama);
Check more how listen: false works when used with Provider.of(context, listen: false).

StreamProvider: Error: Could not find the correct Provider<User> above this App Widget

I'm using StreamProvider from the provider package for auth functionality in my flutter-firebase app, just like it is explained in this tutorial https://www.youtube.com/watch?v=j_SJ7XmT2MM&list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC&index=9.
When trying to run my app, I get an error message, with a suggestion how to do it correctly, but my code IS written in the way that is suggested.
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(FirebaseWrapper());
runApp(App());
}
class FirebaseWrapper extends StatelessWidget {
// Create the initialization Future outside of build():
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
// final Future<void> _initSharedPrefs = SharedPrefsHelper().initSharedPrefsInstance();
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return FutureBuilder(
// from: https://firebase.flutter.dev/docs/overview/#initializing-flutterfire
future: _initialization,
// future: Future.wait([_initialization, _initSharedPrefs]),
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorPage(); //TODO better error pages
if (snapshot.connectionState == ConnectionState.done) return FirebaseAuthWrapper();
return Loading(); //waiting
},
);
}
}
class FirebaseAuthWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: Auth().userStream,
initialData: null,
child: App(),
);
}
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print('yeet');
return MaterialApp(
key: UniqueKey(),
title: 'Wanderapp',
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: (user == null) ? '/signIn' : '/',
routes: (user == null)
? {
'/signIn': (context) => SignIn(),
'/register': (context) => Register(),
// '/forgotPassword': (context) => ForgotPassword(),
}
: {
'/': (context) => Home(),
//...
},
);
}
}
the error message:
Error: Could not find the correct Provider<User> above this App Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that App is under your MultiProvider/Provider<User>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
```
consider using `builder` like so:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
```
I'm user the same "User" class from Firebase for StreamProvider and Provider.of, the hierarchy/scope also seems to be correct in my code, but it doesn't work.
Does anyone know what my mistake is? Thank you very much.
In this link about runApp it says:
Calling runApp again will detach the previous root widget from the
screen and attach the given widget in its place.
So, you just need to remove the second runApp, as App is being called anyway from the StreamProvider: child: App(),.
Solution:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(FirebaseWrapper());
runApp(App()); //*** Remove this line ***///
}

Flutter. Could not find the correct Provider<ThemeChanger> above this Home Widget

I wanted to add theme with provider to my code. I adapted it from this source. https://github.com/lohanidamodar/flutter_theme_provider/blob/master/lib/main.dart .
Even it is same code, I got this error:
"The following ProviderNotFoundException was thrown building Home(dirty, state: _HomeState#c900c):
Error: Could not find the correct Provider above this Home Widget"
This happens because you used a BuildContext that does not include the provider
of your choice.
void main() async {
setPathUrlStrategy();
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MaterialAppWithTheme());
}
class MaterialAppWithTheme extends StatefulWidget {
#override
_MaterialAppWithThemeState createState() => _MaterialAppWithThemeState();
}
class _MaterialAppWithThemeState extends State<MaterialAppWithTheme> {
#override
void initState() {
super.initState();
AppRouter appRouter = AppRouter(
routes: AppRoutes.routes,
notFoundHandler: AppRoutes.routeNotFoundHandler,
);
appRouter.setupRoutes();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ThemeNotifier(),
child: Consumer<ThemeNotifier>(
builder: (context, ThemeNotifier notifier, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: notifier.darkTheme ? dark : light,
onGenerateRoute: AppRouter.router.generator,
);
},
),
);
}
}
Change this:
create: (_) => ThemeNotifier(),
To this:
create: (context) => ThemeNotifier(),

How can I stop my change notifier provider from rebuilding my parent material app when I am rendering my child material app?

I have a app class that returns a MaterialApp() which has it's home set to TheSplashPage(). This app listens to the preferences notifier if any preferences are changed.
Then in TheSplashPage() I wait for some conditionals to be true and if they are I show them my nested material app.
Side Note: I use a material app here because it seems more logical since it has routes that the parent material app shouldn't have. And also once the user is unauthenticated or gets disconnected I want the entire nested app to shut down and show another page. This works great!
But my problem is the following. Both apps listen to ThePreferencesProvider() so when the theme changes they both get notified and rebuild. But this is a problem because whenever the parent material app rebuilds, it returns the splash page. So now I am back on TheSplashPage() whenever I change a setting on TheSettingsPage().
So my question is how can I stop my application from going back to the TheSplashPage() whenever I change a setting?
Main.dart
void main() {
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MultiProvider(
providers: [
ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
ChangeNotifierProvider<ConnectionProvider>(
create: (_) => ConnectionProvider(),
),
ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
],
child: Consumer<PreferencesProvider>(builder: (context, preferences, _) {
return MaterialApp(
home: TheSplashPage(),
theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
debugShowCheckedModeBanner: false,
);
}),
);
}
}
TheSplashPage.dart
class TheSplashPage extends StatelessWidget {
static const int fakeDelayInSeconds = 2;
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.delayed(new Duration(seconds: fakeDelayInSeconds)),
builder: (context, delaySnapshot) {
return Consumer<ConnectionProvider>(
builder: (BuildContext context, ConnectionProvider connectionProvider, _) {
if (delaySnapshot.connectionState != ConnectionState.done ||
connectionProvider.state == ConnectionStatus.uninitialized) return _buildTheSplashPage(context);
if (connectionProvider.state == ConnectionStatus.none) return TheDisconnectedPage();
return Consumer<AuthenticationProvider>(
builder: (BuildContext context, AuthenticationProvider authenticationProvider, _) {
switch (authenticationProvider.status) {
case AuthenticationStatus.unauthenticated:
return TheRegisterPage();
case AuthenticationStatus.authenticating:
return TheLoadingPage();
case AuthenticationStatus.authenticated:
return MultiProvider(
providers: [
Provider<DatabaseProvider>(create: (_) => DatabaseProvider()),
],
child: Consumer<PreferencesProvider>(
builder: (context, preferences, _) => MaterialApp(
home: TheGroupManagementPage(),
routes: <String, WidgetBuilder>{
TheGroupManagementPage.routeName: (BuildContext context) => TheGroupManagementPage(),
TheGroupCreationPage.routeName: (BuildContext context) => TheGroupCreationPage(),
TheGroupPage.routeName: (BuildContext context) => TheGroupPage(),
TheSettingsPage.routeName: (BuildContext context) => TheSettingsPage(),
TheProfilePage.routeName: (BuildContext context) => TheProfilePage(),
TheContactsPage.routeName: (BuildContext context) => TheContactsPage(),
},
theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
debugShowCheckedModeBanner: false,
)),
);
}
});
});
});
}
TheSettingsPage.dart
Switch(
value: preferences.isDarkMode,
onChanged: (isDarkmode) => preferences.isDarkMode = isDarkmode,
),
You fell for the XY problem
The real problem here is not "my widget rebuilds too often", but "when my widget rebuild, my app returns to the splash page".
The solution is not to prevent rebuilds, but instead to change your build method such that it fixes the issue, which is something that I detailed previously here: How to deal with unwanted widget build?
You fell for the same issue as in the cross-linked question: You mis-used FutureBuilder.
DON'T:
#override
Widget build(BuildContext context) {
return FutureBuilder(
// BAD: will recreate the future when the widget rebuild
future: Future.delayed(new Duration(seconds: fakeDelayInSeconds)),
...
);
}
DO:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
// Cache the future in a StatefulWidget so that it is created only once
final fakeDelayInSeconds = Future<void>.delayed(const Duration(seconds: 2));
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Rebuilding the widget no longer recreates the future
future: fakeDelayInSeconds,
...
);
}
}
When using Consumer, you are forcing the widget to rebuild every time you notify listeners.
To avoid such behaviour, you can use Provider.of as stated in ian villamia's answer, as it can be used wherever you need it, and only where you need it.
The changes in your code to use Provider.of would be removing the consumer and adding Provider.of when resolving the theme as follows:
theme: Provider.of<PreferencesProvider>(context).isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
HOWEVER if you want to keep using Consumer, you can do something else:
The child property on the Consumer widget is a child that is not rebuilt. You can use this to set the TheSpashScreen there, and pass it to the materialApp through the builder.
TL:DR
Use Provider.of if you need only to tap into one variable for simplicity.
Use Consumer with its child property as the child doesn't rebuild. <= Better performance
Using Provider.of
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MultiProvider(
providers: [
ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
ChangeNotifierProvider<ConnectionProvider>(
create: (_) => ConnectionProvider(),
),
ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
],
child: Builder(
builder: (ctx) {
return MaterialApp(
home: TheSpashPage(),
theme: Provider.of<PreferencesProvider>(ctx).isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
);
}),
);
}
}
Using Consumer
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MultiProvider(
providers: [
ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
ChangeNotifierProvider<ConnectionProvider>(
create: (_) => ConnectionProvider(),
),
ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
],
child: Consumer<PreferencesProvider>(
child: TheSpashPage(),
builder: (context, preferences, child) {
return MaterialApp(
home: child,
theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
debugShowCheckedModeBanner: false,
);
}),
);
}
}
I hope this is helpful for you!
basically there's 2 ways in using a provider
one it the current one you're using which is the consumer type,
is using the instance of a provider
final _preferencesProvider= Provider.of<PreferencesProvider>(context, listen: false);
you can toggle the "listen:true" if you want the widget to rebuild when notifyListeners() are called... false if otherwise
also just use _preferencesProvider.someValue like any other instance

Combine two Streams into one StreamProvider

I have two streams:
Stream<FirebaseUser> FirebaseAuth.instance.onAuthStateChanged
Stream<User> userService.streamUser(String uid)
My userService requires the uid of the authenticated FirebaseUser as a parameter.
Since I will probably need to access the streamUser() stream in multiple parts of my app, I would like it to be a provider at the root of my project.
This is what my main.dart looks like:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
var auth = FirebaseAuth.instance;
var userService = new UserService();
return MultiProvider(
providers: [
Provider<UserService>.value(
value: userService,
),
],
child: MaterialApp(
home: StreamBuilder<FirebaseUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) return LoginPage();
return StreamProvider<User>.value(
value: userService.streamUser(snapshot.data.uid),
child: HomePage(),
);
}),
),
);
}
}
The issue is that when I navigate to a different page, everything below the MaterialApp is changed out and I lose the context with the StreamProvider.
Is there a way to add the StreamProvider to the MultiProvider providers-list?
Because when I try, I also have to create another onAuthStateChanged stream for the FirebaseUser and I don't know how to combine them into one Provider.
So this seems to work fine:
StreamProvider<User>.value(
value: auth.onAuthStateChanged.transform(
FlatMapStreamTransformer<FirebaseUser, User>(
(firebaseUser) => userService.streamUser(firebaseUser.uid),
),
),
),
If anybody has doubts about this in certain edge cases, please let me know.
Thanks to pskink for the hint about flatMap.
Maybe you can try this approach:
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<FirebaseUser>(
builder: (_) => FirebaseUser(),
),
],
child: AuthWidgetBuilder(builder: (context, userSnapshot) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.indigo),
home: AuthWidget(userSnapshot: userSnapshot),
);
}),
);
}
}
AuthWidgetBuilder.dart
Used to create user-dependant objects that need to be accessible by
all widgets. This widget should live above the [MaterialApp]. See
[AuthWidget], a descendant widget that consumes the snapshot generated
by this builder.
class AuthWidgetBuilder extends StatelessWidget {
const AuthWidgetBuilder({Key key, #required this.builder}) : super(key: key);
final Widget Function(BuildContext, AsyncSnapshot<User>) builder;
#override
Widget build(BuildContext context) {
final authService =
Provider.of<FirebaseUser>(context, listen: false);
return StreamBuilder<User>(
stream: authService.onAuthStateChanged,
builder: (context, snapshot) {
final User user = snapshot.data;
if (user != null) {
return MultiProvider(
providers: [
Provider<User>.value(value: user),
Provider<UserService>(
builder: (_) => UserService(uid: user.uid),
),
],
child: builder(context, snapshot),
);
}
return builder(context, snapshot);
},
);
}
}
AuthWidget.dart
Builds the signed-in or non signed-in UI, depending on the user
snapshot. This widget should be below the [MaterialApp]. An
[AuthWidgetBuilder] ancestor is required for this widget to work.
class AuthWidget extends StatelessWidget {
const AuthWidget({Key key, #required this.userSnapshot}) : super(key: key);
final AsyncSnapshot<User> userSnapshot;
#override
Widget build(BuildContext context) {
if (userSnapshot.connectionState == ConnectionState.active) {
return userSnapshot.hasData ? HomePage() : SignInPage();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
This is originally from the tutorial of advance provider from Andrea Bizotto.
But I tailored some the code according to your your code above.
Hope this works, good luck!
Reference:
https://www.youtube.com/watch?v=B0QX2woHxaU&list=PLNnAcB93JKV-IarNvMKJv85nmr5nyZis8&index=5