how to add multiple ChangeNotifierProvider in same type in Flutter using Provider State Mangaement Technique - flutter-provider

Is it possible to add the same type multiple ChangeNotifierProvider? Because I'm not getting the expected results.
I am unable to deal with a list of model classes while using Provider.
return MultiProvider(
providers: [
ChangeNotifierProvider<ValueNotifier<double>>(
create: (_) => ValueNotifier<double>(0.0),
),
ChangeNotifierProvider<ValueNotifier<double>>(
create: (_) => ValueNotifier<double>(0.0),
),
],
In my build method
#override
Widget build(BuildContext context) {
ValueNotifier<double> firstNotifier = Provider.of(context, listen: true);
ValueNotifier<double> secondNotifier = Provider.of(context, listen: true);
print('First value ${firstNotifier.value} Second value ${secondNotifier.value}');
...
onTap:(){
firstNotifier.value = 20.0;
secondNotifier.value = 30.0;
}
both printed values are the `enter code here same First value is 20 Second value is 20

Flutter uses the type of ChangeNotifierProvider to find it in the widget tree. So you can't have the same types in your MultiProvider, because Provider.of will always find one of these, as it happens in your code.
To solve this, you can define different classes for the providers, and use the class members to store values. For example, if you want to have two int type providers, you can do something like this:
class MyFirstProvider with ChangeNotifier {
int _myValue = 0;
int get myValue => _myValue;
set myValue(int newValue) {
_myValue = newValue;
notifyListeners();
}
}
class MySecondProvider with ChangeNotifier {
int _myValue = 0;
int get myValue => _myValue;
set myValue(int newValue) {
_myValue = newValue;
notifyListeners();
}
}
Create MultiProvider:
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MyFirstProvider()),
ChangeNotifierProvider(create: (context) => MySecondProvider()),
],
Different ways to access these providers:
context.read<MyFirstProvider>()
context.watch<MySecondProvider>()
Provider.of<MyFirstProvider>(context, listen: false)
Provider.of<MySecondProvider>(context, listen: true)
You can adopt this to your case, you can use any type.
(Keep in mind, that ValueNotifier is a ChangeNotifier holding a single value, so it might not be the best approach to use a ValueNotifier as the type of a ChangeNotifierProvider.)

Related

Refresh/Reset StreamProvider - Flutter Firebase

In my app, I have the following providers.
#override
Widget build(BuildContext context) {
return OverlaySupport.global(
child: MultiProvider(
providers: [userLoggedIn, currentUserData],
child: MaterialApp(...)))
}
var userLoggedIn = StreamProvider<User?>.value(
value: FirebaseAuth.instance.authStateChanges(), initialData: null);
var currentUserData = StreamProvider<FrediUser>.value(
updateShouldNotify: (_, __) => true,
initialData: FrediUser(
loginProvider: '',
email: '',
admin: false,
profileSettings: [],
profileChips: [],
profileStats: [],
id: 'loading',
imageUrl: 'loading',
bio: 'loading',
username: 'loading'),
value: currentUserID != null ? currentUserDataStream() : null,
);
PROBLEM
When the user logs out (or logs in for the first time), the provider is either:
Containing old user data (until a hot restart is done, when the providers are called again and reloaded)
Null or empty, because there was no user before.
What I want to do is to refresh or call the Stream Providers again once I have a new user, or delete all the data once a user logs off.
Thank you!
You can listen to the changes of auth state like this.
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
I've been facing a similar problem as you are, I've come up with a work-around although not sure how "valid" it is according to the Provider architecture
The Problem
I've got a DatabaseService class which has a stream function of type Stream<CustomUser> function, and I used it like this:
//--- main.dart ---//
runApp(MultiProvider(
providers: [
// ..some other providers.. //
// data provider
Provider<DatabaseService?>(
create: (_) => databaseService,
),
// data provider
StreamProvider<CustomUser?>(
create: (context) => databaseService.getCurrUserFromDb(),
lazy: false,
initialData: null,
updateShouldNotify: (_, __) => true,
),
],
child: MyApp(
initPage: initPage,
)
));
Stream Function:
//--- database_service.dart ---//
// gets the user from database and
// assigns it to the variable _user.
Stream<CustomUser?> getCurrUserFromDB() async* {
try {
CustomUser? currUser;
if (_user != null) {
await for (DocumentSnapshot<Object?> event
in users.doc(user.uid).snapshots()) {
final jsonMap = event.data() as Map<String, dynamic>;
currUser = CustomUser.fromJson(jsonMap);
_user = currUser;
CustomPreferences.setCurrUser(_user);
yield currUser;
}
}
} catch (e) {
rethrow;
}
}
databaseService is the DatabaseService class with named constructors.
This was not causing the widgets to rebuild at the start nor when the stream has a new value
Solution:
Created a StreamController in the DatabaseService class, and when the user signs in I add the stream function:getCurrUserFromDB() to the StreamController like this
//--- authentication_screen.dart ---//
...
ElevatedButton(
child: const Text("Sign In"),
onPressed: () async {
final user = await AuthService().googleSignIn();
if (user != null) {
final dbProvider = context.read<DatabaseService?>();
await dbProvider?.setInitUser(user, context);
await dbProvider?.cusUserController
.addStream(dbProvider.getCurrUserFromDB());
}
}),
...
setInitUser(CustomUser? user) is used set the value of the _user variable in DatabaseService and user is used to get this variable.
Reasoning
I am creating a StreamProvider at the start of the app, and its source the StreamController needs to have a stream to listen so I give it when I am trying to sign in.
Or even cleaner solution would be to do it in the constructor of DatabaseService Class like this:
//--- database_service.dart ---//
// method to add the stream to controller //
Future<void> addStream() async {
if (!_cusUserController.isClosed && !_cusUserController.isPaused) {
await _cusUserController.addStream(getCurrUserFromDB());
}
}
// constructor //
DatabaseService._init(CustomUser cusUser) {
addStream();
_user = cusUser;
}
And one last thing to note is that I don't make the declare the Controller as final. When I had it declared as final the streams weren't updating, so it looks like this now:
//--- database_service.dart ---//
StreamController<CustomUser?> _cusUserController = StreamController();
TL;DR
I created a StreamProvider which returns a StreamController in its create property and later down the life cycle of the app I gave the controller a Stream using the addStream method.
Sorry for the wall of text I just wanted to come out as clear as possible.

Flutter Web Firestore Running Out of Memory - Provider

I have a very tricky situation, which I've reproduced in a demo.
I have a Provider of a user, with this method of updating the listeners:
class User extends ChangeNotifier {
...
User({required this.uid}) {
Database().getUser(uid).listen(
(user) async {
displayName = user?.displayName;
email = user?.email;
phoneNumber = user?.phoneNumber;
photoURL = user?.photoURL;
did = user?.did;
interests = user?.interests;
notifyListeners();
},
onError: (e) => print(e),
);
}
...
}
My main.dart starts like this:
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthState>.value(value: _authState),
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel())
],
child: Consumer<AuthState>(
builder: (context, auth, child) {
var user =
auth.authUser == null ? null : User(uid: auth.authUser!.uid);
return MultiProvider(
providers: [
ChangeNotifierProvider<ZUser?>.value(
value: zuser,
),
],
child: MaterialApp.router(...
This has been sufficient for my use case thus far.
Now, I wish to make an update to the interests field;
I have a DB widget that does:
Future updateUser(String uid, Map<String, Object?> data) async {
return userCollection.doc(uid).update(data);
}
Where the userCollection is my collection in Firestore.
I call this class from my view widget, as:
ZWideButton(
text: "Save",
onPressed: () async {
setState(() {
_localEdit = false;
_loading = true;
});
await user.saveInterests(_interests());
setState(() => _loading = false);
},
),
Where saveInterests is:
Future saveInterests(List<String> interests) async {
return _db.updateUser(uid, {"interests": interests});
}
None of this presents any problem at first -- I can update the interests and it works fine. That is, until I keep updating the interests, and it gets slower and slower each time (the browser says the download time gets longer and longer) and seemingly my computer is eating up more and more memory until the webpage ultimately crashes.
Something of a memory leak appears to be happening, but I'm unsure what about flutter web and firebase could be causing it. I believe it may have to do with the Provider package not disposing appropriately. It does not seem to be the provider as I don't see the Widget being rebuilt over and over. Looking for some thoughts.
For anyone looking; My issue is that my json deserializer was causing an infinite loop with the firebase listener

How to update boolean value in Firestore from a switch tile?

My goal is to have a userNotifications collection in Firestore that is used to track user notification preferences. The user can toggle between true and false based on tapping a switch tile. With the code below, Firestore is immediately and correctly updating the boolean value based on user interaction and the user interface is updating and reflecting the changes. If the user logs out and logs back in, the boolean value is accurately reflected in the user interface.
Before I apply this approach more broadly in the code I was hoping that someone can comment and let me know if my approach to updating the boolean value in Firestore is valid or point me in a better direction so I can improve my code. Links to SO posts or documentation are fine as I am more than willing to read and learn. Thanks in advance for any help.
class NotificationsMessagesTile extends StatefulWidget {
const NotificationsMessagesTile({
Key? key,
}) : super(key: key);
#override
State<NotificationsMessagesTile> createState() =>
_NotificationsMessagesTileState();
}
class _NotificationsMessagesTileState extends State<NotificationsMessagesTile> {
bool notificationsActive = false;
final String? currentSignedInUserID = Auth().currentUser?.uid;
Future<void> updateNotifications() async {
if (!notificationsActive) {
notificationsActive = true;
FirebaseFirestore.instance
.collection('userNotifications')
.doc(currentSignedInUserID)
.update({
'messages': false,
});
} else {
notificationsActive = false;
FirebaseFirestore.instance
.collection('userNotifications')
.doc(currentSignedInUserID)
.update({
'messages': true,
});
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return SwitchListTileSliver(
icon: Provider.of<NotificationsPageProvider>(context).areMessagesTurnedOn
? Icons.notifications_active
: Icons.notifications_off,
onChanged: (bool value) {
final provider = Provider.of<NotificationsPageProvider>(
context,
listen: false,
);
provider.updateMessagesSettings(isOn: value);
updateNotifications();
},
subTitle:
Provider.of<NotificationsPageProvider>(context).areMessagesTurnedOn
? const Text(
SettingsPageString.messagesOn,
)
: const Text(
SettingsPageString.messagesOff,
),
title: SettingsPageString.messages,
value:
Provider.of<NotificationsPageProvider>(context).areMessagesTurnedOn,
);
}
}
You could improve your updateNotifications() function to not have duplicate code:
Future<void> updateNotifications() async {
await FirebaseFirestore.instance
.collection('userNotifications')
.doc(currentSignedInUserID)
.update({
'messages': notificationsActive,
});
setState(() {
notificationsActive = !notificationsActive;
});
}
And also I would suggest to you to listen to your Firestore collection and update UI on change. You can look up on how to do that here.

The getter 'create' isn't defined for the type 'flutter class'

I'm using Flutter and I've been used the the class Routes to navigate between views but a the IDE throw the next error:
error: The getter 'create' isn't defined for the type 'UserLogin'.
here is the code:
class Routes{
static const login = '/';
static const registra = '/registra';
static const home = '/home';
static const info = '/info';
static const incons = '/incons';
static Route routes(RouteSettings routeSettings){
switch(routeSettings.name){
case login:
return _buildRoute(UserLogin.create);
case registra:
return _buildRoute(EmailCreate.create);
case home:
return _buildRoute(HomeMenu.create);
case info:
return _buildRoute(UserInfoMenu.create);
case incons:
return _buildRoute(IssuesContainer.create);
default:
throw Exception('Route does not exists');
}
}
static MaterialPageRoute _buildRoute(Function build)=> MaterialPageRoute(builder: (context)=>build(context));
}
Im importing all the classes that im using, but the only that doesn't throw any error is the EmailCreate one
I attach the implementation of the class Routes:
final _navigatorkey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
static Widget create(){
return BlocListener<AuthCubit, AuthState>(
listener: (context, state){
if (state is AuthSignedOut){
_navigatorkey.currentState?.pushNamedAndRemoveUntil(Routes.login, (route) => false);
}else if (state is AuthSignedIn){
_navigatorkey.currentState?.pushNamedAndRemoveUntil(Routes.home, (route) => false);
}
},
child: MyApp(),
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorkey,
title: 'Flutter Demo',
onGenerateRoute: Routes.routes,
);
}
}
I don't know if is it a better option use Navigator.push and Navigator.pop instead of Routes.
The getter 'create' isn't defined for the type 'UserLogin'
This means you do not have a create method/getter for the UserLogin class.
The error will go away when you add a static method create to the class.

How do I print a list from a Firebase Database query using Flutter?

I'm trying to print a list of attributes from my firebase database. The database is currently structured like this:
I would first like to print a list of show names to the console so I can see that it works and then add it to a ListView later. Any help is appreciated!
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// This is the model class
class Mod {
final String name;
final String nextEpisode;
final String prevEpisode;
Mod(this.name, this.nextEpisode, this.prevEpisode);
Mod.fromJson(Map<String, dynamic> json)
: name = json['name'],
nextEpisode = json['nextEpisode'],
prevEpisode = json['prevEpisode'];
}
// This is the screen class
class FTest2 extends StatefulWidget {
#override
_FTest2State createState() => _FTest2State();
}
class _FTest2State extends State<FTest2> {
List<Mod> list = List();
MakeCall() {
final mainReference = FirebaseDatabase.instance.reference();
mainReference.child('-M5Uol7Xldnc8wvNXnNg').once().then((DataSnapshot dataSnapshot){
this.setState(() {
for(var value in dataSnapshot.value){
list.add(Mod.fromJson(value));
}
});
});
print(list);
}
void getData() {
MakeCall();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('This is App Bar for the FB Test')),
body: Column(
children: <Widget>[
RaisedButton(
child: Text('Press for data'),
onPressed: () {
getData();
},
),
],
));
}
}
You're looping over the nodes of one specific show, which means that your value is actually one of the child properties under that: name, nextEpisode, prevEpisode. What you're probably looking for is to listen to onChildAdded for all shows, and then get the name property for each:
mainReference.child('shows')
.onChildAdded
.forEach((event) => {
print(event.snapshot.key+": "+event.snapshot.value.toString());
this.setState(() {
list.add(Mod.fromJson(event.snapshot.value["name"]));
});
});
Also see my answer form a few weeks ago here: Flutter: Firebase Real-Time database orderByChild has no impact on query result
Your reference is wrong, you need to traverse the database from top to the node you want to retrieve, therefore use the following:
final mainReference = FirebaseDatabase.instance.reference("shows");
mainReference.child('-M5Uol7Xldnc8wvNXnNg').once().then((DataSnapshot dataSnapshot){
pass the argument shows to the reference() method.

Resources