Snapshot has not have Data in Bloc - flutter

I am trying to learn Bloc with creating dynamic simple theme manager. I create a class called theme_bloc :
class DefaultApi {
final String name;
final ThemeData theme;
DefaultApi(this.name, this.theme);
}
class ThemeBloc {
DefaultApi _defualt;
ThemeBloc() {}
final _themeManager = StreamController<DefaultApi>.broadcast();
Stream<DefaultApi> get themeManager => _themeManager.stream;
Function(DefaultApi) get changeTheme => _themeManager.sink.add;
DefaultApi initialTheme() {
_defualt = DefaultApi("light", ThemeManager.instance.lightTheme);
return _defualt;
}
void dispose() {
_themeManager.close();
}
}
to inject bloc class i use provider like this:
class ThemeProvider with ChangeNotifier{
ThemeBloc _bloc;
ThemeProvider(){
_bloc = ThemeBloc();
}
ThemeBloc get bloc => _bloc;
}
I use StringBuilder in main class to set theme like this:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: ThemeProvider(),
child: Consumer<ThemeProvider>(
builder: (crx, provider, child) {
return StreamBuilder(
initialData: provider.bloc.initialTheme(),
stream: provider.bloc.themeManager,
builder: (context, AsyncSnapshot<DefaultApi>snapshot) {
return snapshot.hasData? MaterialApp(
title: 'Flutter Demo',
theme: snapshot.data.theme,
home: HomePage(),
):Container();
});
},
),
);
}
In HomePage page i have switch to change theme between light and dark theme.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of<ThemeProvider>(context).bloc;
return Scaffold(
appBar: AppBar(
title: Text("Theme manager"),
),
body: StreamBuilder<DefaultApi>(
stream: bloc.themeManager,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Switch(
value: true,
onChanged: (bool value) {
},
);
} else if (!snapshot.hasData) {
return Text("loading");
}
return Text("!!!!");
}),
);
}
But after running just loading is printed into screen.
Someone know what is my problem?

Your problem will solve if the StreamBuilder widget inside HomePage have initialData. like this:
...
body: StreamBuilder<DefaultApi>(
initialData: bloc.initialTheme(), // <<< new line
stream: bloc.themeManager,
builder: (context, snapshot) {
...
This input is not required. Yet I don't have any clue why it's absence cause problem here.
There are few deeper considerations:
1. Using ChangeNotifierProvider
As the documentation recommends, use ChangeNotifierProvider instead of ChangeNotifierProvider.value. Obviously because you're creating a new instance
...
return ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: Consumer<ThemeProvider>(
...
2. Prevent useless listening
Based on this guide, if you are using provider to only call actions, you have to use listen: false to prevent useless rebuilds.

Related

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 ***///
}

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

Flutter provider loosing value when navigating to another screen

I'm new to Flutter and provider package so any assistance would be great, so the issue I have my main.dart file which is as follows:
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: ChangeNotifierProvider(
create: (_) => InterestingMomentProvider(),
child: Home(),
),
),
);
This builds my Home widget, I won't post it all as It's extremely large, however, what happens is I click a button and it passes in a string to the provider class an adds it to the list which is outlined as follows:
import 'package:flutter/widgets.dart';
class InterestingMomentProvider extends ChangeNotifier {
List<String> _moments = [];
List<String> get moments => _moments;
void addMoment(String time){
_moments.add(time);
}
int momentsTotal(){
return _moments.length;
}
}
Adding a breakpoint on the addMoment method I can confirm _moments has all the strings.
I then press a button which navigates to another screen, the navigation code is as follows:
Navigator.push(context, MaterialPageRoute(builder: (context) => MomentsRecorded()),);
MomentsRecorded widget is as follows:
class MomentsRecorded extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Moments'),
centerTitle: true,
),
body: Center(
child: MomentsList()
),
);
}
}
the first error was:
Could not find the correct Provider<InterestingMomentProvider> above this Consumer<InterestingMomentProvider> Widget
To fix, please:
* Ensure the Provider<InterestingMomentProvider> is an ancestor to this Consumer<InterestingMomentProvider> Widget
* Provide types to Provider<InterestingMomentProvider>
* Provide types to Consumer<InterestingMomentProvider>
* Provide types to Provider.of<InterestingMomentProvider>()
* Always use package imports. Ex: `import 'package:my_app/my_code.dart';
* Ensure the correct `context` is being used.
I then tweaked the body to look like the following and the error dissappeared:
body: Center(
child: ChangeNotifierProvider(
create: (_) => InterestingMomentProvider(),
child: MomentsList())
),
However inside MomentLists widget, I try to loop through the list of moments from the provider class, however when debugging _moments is 0 ?
MomentsList widget:
class MomentsList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<InterestingMomentProvider>(
builder: (context, momentData, child){
return momentData.momentsTotal() > 0 ? ListView.builder(
itemCount: momentData.momentsTotal(),
itemBuilder: (BuildContext context, int index) {
final moment = momentData.moments[index];
return ListTile(
title: Text(moment),
);
}
) : Center(child: Text('no moments recorded'),);
}
);
}
}
Can someone please explain why this maybe?
This is happening, because your provider is defined in home property of MaterialApp, so when you change the route the provider will be removed too.
Solution: move the provider above the MaterialApp like this:
void main() => runApp(
ChangeNotifierProvider(
create: (_) => InterestingMomentProvider(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
home: Home()
),
),
);
If you are afraid that this isn't right - checkout the docs, they are doing the same

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

Persist Provider data across multiple pages not working

I'm using Provider in my flutter app, and when I go to a new page, the data provided to the Provider at page 1 is not accessible in page 2.
The way I understood the way Provider works, was that there is a central place where one stores all the data, and one can access that data anywhere in the application. So in my application, which is shown below, ToDoListManager is the place where all the data is stored. And if I set the data in Page 1, then I will be able to access that data in Page 2, and vice versa.
If this is not correct, then what part is wrong? And why isn't it working in my application?
Here's the code
Page 1
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => ToDoListManager(),
child: Scaffold(
appBar: AppBar(
title: Text('Cool Project'),
),
body:e ToDoList(),
),
);
}
}
class ToDoList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final toDoListManager = Provider.of<ToDoListManager>(context);
return ListView.builder(
itemCount: toDoListManager.toDoList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => Details(index)));
},
child: Text(toDoListManager.toDoList[index]),
);
},
);
}
}
Page 2
class Details extends StatelessWidget {
final int index;
Details(this.index);
#override
build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => ToDoListManager(),
child: Scaffold(
appBar: AppBar(
title: Text('Details Bro'),
),
body: AppBody(index)),
);
}
}
class AppBody extends StatelessWidget {
final int index;
AppBody(this.index);
#override
Widget build(BuildContext context) {
final toDoListManager = Provider.of<ToDoListManager>(context);
print(toDoListManager.toDoList);
return Text(toDoListManager.toDoList[1]);
}
}
ToDoListProvider
class ToDoListManager with ChangeNotifier {
List<String> _toDoList = ['yo', 'bro'];
List<String> get toDoList => _toDoList;
set toDoList(List<String> newToDoList) {
_toDoList = newToDoList;
notifyListeners();
}
}
You have 2 options:
Place your ChangeNotifierProvider above your MaterialApp so that is accesible from any of you Navigator routes.
Keep your Home widget as is but when pushing the new widget with the Navigator provide the original Manager.
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return Provider<ToDoListManager>.value(
value: toDoListManager,
child: Details(index),
);
},
),
);
},
With both approaches you don't need to create a new ChangeNotifierProvider in your details screen.