Flutter: Access SharedPreferences Provider / ChangeNotifier in a Stream Class - flutter

I've looked around in StackoverFlow and was not able to find myself a solution to this.
Scenario:
I have a Flutter SharedPreferences Provider with ChangeNotifier Class, that will get updated with the current Logged In User info.
Simplified content:
class SharedPreferences {
final String userId;
final String userName;
SharedPreferences({
#required this.userId,
#required this.userName,
});
}
class SharedPreferencesData with ChangeNotifier {
var _sharedPreferencesData = SharedPreferences(
userId: 'testUserId',
userName: 'testUserName',
);}
And a database.dart file with Class containing DataBaseServices to get FireStore Streams from Snapshots:
class DatabaseService {
final CollectionReference companiesProfilesCollection =
Firestore.instance.collection('companiesProfiles');
List<CompanyProfile> _companiesProfilesFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return CompanyProfile(
docId: doc.documentID,
companyName: doc.data['companyName'] ?? '',
maxLocationsNumber: doc.data['maxLocationsNumber'] ?? 0,
maxUsersNumber: doc.data['maxUsersNumber'] ?? 0,
);
}).toList();
}
Stream<List<CompanyProfile>> get getCompaniesProfiles {
return companiesProfilesCollection
.where('userId', isEqualTo: _userIdFromProvider)
// My problem is above -----
.snapshots()
.map(_companiesProfilesFromSnapshot);
}
}
I Don't want to fetch the entire Stream data as it could be massive for other Streams, I just want to pass the userID under .where('userId', isEqualTo:_userIdFromProvider).
I couldn't access the context in this class to get the data from the Provider
Couldn't send the userId to getCompaniesProfiles getter, as getter don't take parameters
And if I convert this getter to a regular method, I wont be able to send the userID to it, as this has to run under void main() {runApp(MyApp());} / return MultiProvider(providers: [ and By then I cannot call fetch the sharedPreferences with a context that does not contain the provider info ...
Couldn't figure out how to receive the context as a constructor in this class, when I did, I got the following Only static members can accessed in initializers in class DatabaseService.
I'm still a beginner, so I would appreciate if you could share with me the best approach to handle this.
Thank you!
*********** Re-Edited by adding the below: **************
I'm trying to implement the same scenario, here is my code:
Main file:
return MultiProvider(
providers: [
ChangeNotifierProvider<SpData>(
create: (context) => SpData(),
),
ProxyProvider<SpData, DS>(
create: (context) => DS(),
update: (ctx, spData, previousDS) {
print('ChangeNotifierProxyProvider RAN');
previousDS.dbData = spData;
return previousDS;
},
),
],
SP File:
class SP {
final String companyId;
SP({
#required this.companyId,
});
}
class SpData with ChangeNotifier {
var _sPData = SP(
companyId: '',
);
void setCompanyId(String cpID) {
final newSharedPreferences = SP(
companyId: cpID,
);
_sPData = newSharedPreferences;
print('_spData companyId:${_sPData.companyId}');
notifyListeners();
}
String get getCompanyId {
return _sPData.companyId;
}
}
DS file:
class DS with ChangeNotifier {
SpData dbData;
void printCompanyId() {
var companyId = dbData.getCompanyId;
print('companyId from DataBase: $companyId');
}
}
The SpData dbData; inside Class DS does not update. I've added the prints to figure out what is running and what is not. When I run my code, the print function in main.dart file print('ChangeNotifierProxyProvider RAN'); does not run.
What am I missing? Why ChangeNotifierProxyProvider is not being triggered, to update dbData inside DS file? Thanks!

You can use ProxyProvider for this purpose.
ProxyProvider is a provider that builds a value based on other providers.
You said you have a MultiProvider, so I guess you have SharedPreferencesData provider in this MultiProvider and then DatabaseService provider. What you need to do is use ProxyProvider for DatabaseService instead of a regular provider and base it on the SharedPreferencesData provider.
Here is an example:
MultiProvider(
providers: [
ChangeNotifierProvider<SharedPreferencesData>(
create: (context) => SharedPreferencesData(),
),
ProxyProvider<SharedPreferencesData, DatabaseService>(
create: (context) => DatabaseService(),
update: (context, sharedPreferencesData, databaseService) {
databaseService.sharedPreferencesData = sharedPreferencesData;
return databaseService;
},
dispose: (context, databaseService) => databaseService.dispose(),
),
],
child: ...
Here is what happens in the code snippet above:
ProxyProvider calls update everytime SharedPreferencesData changes.
DatabaseService gets its sharedPreferencesData variable set inside update.
Now that you have access to sharedPreferencesData inside the DatabaseService instance, you can do what you want easily.

Related

Riverpod StateNotifier and a Stream

I've got a Stream<UserProfile> being returned form a firebase service.
I'm using MVVM architecture and have a ProfileViewModel which is extended by a freezed state class:
class ProfileModel extends StateNotifier<ProfileState> {
ProfileModel({
required this.authService,
required this.databaseService,
}) : super(const ProfileState.loading());
late AuthService authService;
late FirestoreDatabase databaseService;
Stream<UserProfile?> get userProfile {
return databaseService.profileStream();
}
}
The above results in the following view:
final profileModelProvider =
StateNotifierProvider.autoDispose<ProfileModel, ProfileState>((ref) {
final authService = ref.watch(authServiceProvider);
final databaseService = ref.watch(databaseProvider)!;
return ProfileModel(
authService: authService, databaseService: databaseService);
});
class ProfilePageBuilder extends ConsumerWidget {
const ProfilePageBuilder({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(profileModelProvider);
final model = ref.read(profileModelProvider.notifier);
final up = ref.read(userProfileProvider);
return ProfilePage(
onSubmit: () => {},
name: up.value?.uid ?? "Empty",
canSubmit: state.maybeWhen(
canSubmit: () => true,
success: () => true,
orElse: () => false,
),
isLoading: state.maybeWhen(
loading: () => true,
orElse: () => false,
),
errorText: state.maybeWhen(
error: (error) => error,
orElse: () => null,
),
);
}
}
I would like to know the correct way (using riverpod) to pass the firebase stream to the UI without mixing up UI/BL without loosing functionality of real time data.
I was able to create a StreamProvider which referenced the profile model but it doesnt feel right.
final userProfileProvider = StreamProvider.autoDispose<UserProfile?>((ref) {
return ref.watch(profileModelProvider.notifier).userProfile;
});
My alternative is to convert streams to futures within the view model and then update the state as the function runs.
I'm really quite stuck here, any help would be appreciated
My guess is you want to
listen to a stream from Firebase
When the latest value changes, you want any dependencies to update
You only want the latest value of the stream.
INTRODUCING BehaviorSubject!
You'll need package:rxdart though you may already have it installed.
import 'package:rxdart/rxdart.dart';
#riverpod
BehaviorSubject<ProfileState> userProfileSubject(
UserProfileSubjectRef ref) {
final stream = ....;
// Get the stream and wrap it in a BehaviorSubject
return BehaviorSubject()..addStream(stream);
}
#riverpod
UserProfile? userProfile(
UserProfileRef ref) {
final behaviorSubject = ref.watch(userProfileSubjectProvider);
// when the underlying stream updates,
// invalidate self so we read the new value
behaviorSubject.doOnData((newProfileState) { ref.invalidateSelf(); });
// note that value could be null while stream
// emits a value. You can wait for that
// and convert this provider to return a Future<UserProfile>
// or in the UI handle the null.
// note that firebase could also have a null value.
return behaviorSubject.value?.userProfile;
}

Flutter StreamProvider does not make realtime exchanes

I'm using streamprovider to get firestore document's realtime changing data.
(like a Streambuilder)
However when i change the data in firestroe consol while app is running, the widget does not reflect the changes.
I searched it in stackoverflow whole day and i tried several answers in here, but it doesn't go on my code.
I'm wondering what is the problem!
class MainPage extends StatelessWidget {
DataProvider db = DataProvider();
#override
Widget build(BuildContext context) {
var userData = Provider.of<User?>(context);
return MultiProvider(
providers: [
StreamProvider<TUser>.value(
value: db.getUser(userData!.uid),
initialData: TUser(email: '', uid: '', name:'', registerdate: '', recentlogindate: '', istag: false, taglist:['index']),
updateShouldNotify: (_, __) => true,
),
StreamProvider<List<Note>>.value(
value: db.getNotes(userData.uid),
initialData: [],
),
ChangeNotifierProvider(
create: (BuildContext context) => SideBarProvider()),
],
child: MainPageSideBar()
);
}
}
class DataProvider extends ChangeNotifier {
final FirebaseFirestore _db = FirebaseFirestore.instance;
Stream<TUser> getUser(String uid) async* {
var snap = await _db.collection('users').doc(uid).get();
Map<String, dynamic>? user_data = snap.data();
yield TUser.fromMap(user_data);
}
Stream<List<Note>> getNotes(String uid) {
return FirebaseFirestore.instance.collection('users').doc(uid).collection('tags').snapshots()
.map((list) =>
list.docs.map((doc) => Note.fromMap(doc.data())).toList());
}
}
//usage
var noteDataList = Provider.of<List<Note>>(context);
I tried several things and then I found the solution. So, I answer myself.
The problem was in DataProvider's getUser Method.
Because of async function, I guess, Stream can't reflect the changes in database.
Therefore, I changed method like this.
Stream<TUser> getUser(String uid) {
return FirebaseFirestore.instance.collection('users').doc(uid).snapshots().map((doc) => TUser.fromMap(doc.data()));
}
And now, my widget changes in realtime!

Flutter initiating a provider and changing its value / NoSuchMethodError

I struggle changing the values of my providers.
They are at the top of my widget tree :
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => User(),
),
ChangeNotifierProvider(
create: (context) => DisplayModel(),
),
],
child: MyApp(),
),
);
Here is the class where I try to build function to change the provider value. DisplayChange() should allow user to set his own values for DisplayModel.
class DisplayModel extends ChangeNotifier {
String blogId;
String title;
String subtitle;
String desc;
String author;
String publisherId;
String imageUrl;
DisplayModel(
{this.blogId,
this.title,
this.subtitle,
this.desc,
this.author,
this.publisherId,
this.imageUrl});
factory DisplayModel.fromDocument(doc) {
return DisplayModel(
blogId: doc['blogId'],
title: doc['title'],
subtitle: doc['subtitle'],
desc: doc['desc'],
author: doc['author'],
publisherId: doc['publisherId'],
imageUrl: doc['imageUrl'],
);
}
DisplayChange(DisplayModel) {
DisplayModel (DisplayModel);
notifyListeners();
return DisplayModel();
}
}
I call the provider in MyApp() and I want the user to be able to change the instance of DisplayModel with the values of loadebBlog (it's built with the same constructors as DisplayModel) and be redirected to home of the app
child: GestureDetector(
onTap: () {
Provider.of<DisplayModel>(
context,
listen: false,
).DisplayChange(loadedBlog);
print ("ok");
Navigator.of(this.context).pushReplacementNamed('home');
},
child: Icon(Icons.ios_share),),
But when the button is tapped, it returns an error
The following NoSuchMethodError was thrown while handling a gesture:
Class 'BlogModel' has no instance method 'call'.
Receiver: Instance of 'BlogModel'
Tried calling: call(Instance of 'BlogModel')
Is the logic good ? What am I missing ?
Thank you for your time.
Best regards,
Julien
Just came across this, so I don't know if it's helpful after 4 months but..
This syntax seems weird:
DisplayChange(DisplayModel) {
DisplayModel (DisplayModel);
notifyListeners();
return DisplayModel();
}
Should be something like:
DisplayChange(DisplayModel model) {
// I'm not sure what this is supposed to do
// but it's no-op code
// DisplayModel (DisplayModel);
notifyListeners();
return DisplayModel();
}

ProxyProvider - how to call proxy from its sub-providers?

What would be the correct way to call (and pass values to) ProxyProvider from its "sub"providers?
Currently I'm passing a callback function to "sub"provider as a parameter, storing it as a Function and then I can call it when needed.
It works in a sense that ProxyProvider is called (and value is passed), but at the same time it breaks notifyListeners(), which is called next - searches getter in "sub"provider (and can't find it) despite that Consumer is used just for ProxyProvider.
This is the error I receive:
error: org-dartlang-debug:synthetic_debug_expression:1:1: Error: The
getter 'audInd' isn't defined for the class 'AudioModel'.
'AudioModel' is from 'package:quiz_game_new/models/audioModel.dart' ('lib/models/audioModel.dart'). Try correcting the name to the name of
an existing getter, or defining a getter or field named 'audInd'.
audInd ^^^^^^
Code
Provider (audioModel.dart):
class AudioModel extends ChangeNotifier {
int _audioIndex = -1;
Function? audioIndexChanged;
void setCallbacks(Function _audioPlaybackCompleted, Function _audioIndexChanged) {
audioPlaybackCompleted = _audioPlaybackCompleted;
audioIndexChanged = _audioIndexChanged;
}
//Some code that changes _audioIndex and afterwards calls audioIndexChanged!(_audioIndex)
}
ProxyProvider (commonModel.dart)
class CommonModel extends ChangeNotifier {
CommonModel(this.audioModel);
final AudioModel audioModel;
int _audioIndex = -1;
int get audioIndex => _audioIndex;
void setCallbacksForAudioPlayback() {
audioModel.setCallbacks(audioPlaybackCompleted, audioIndexChanged);
}
void audioIndexChanged(int audInd) {
_audioIndex = audInd;
notifyListeners();
}
}
Initialization:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<STTModel>(create: (context) => STTModel()),
ChangeNotifierProvider<QuestionModel>(
create: (context) => QuestionModel()),
ChangeNotifierProvider<AudioModel>(create: (context) => AudioModel()),
ChangeNotifierProxyProvider3<STTModel, QuestionModel, AudioModel,
CommonModel>(
create: (BuildContext context) => CommonModel(
Provider.of<STTModel>(context, listen: false),
Provider.of<QuestionModel>(context, listen: false),
Provider.of<AudioModel>(context, listen: false)),
update:
(context, sttModel, questionModel, audioModel, commonModel) =>
CommonModel(sttModel, questionModel, audioModel))
],
child: MaterialApp(
title: 'Flutter Demo',
initialRoute: '/',
routes: {
'/': (context) => ScreenMainMenu(),
'/game': (context) => ScreenGame(),
}));
}
}
What would be the correct way to call (and pass values to)
ProxyProvider from its "sub"providers?
I'm not a big fan of "nested" Providers : it often leads to this kind of issues and doesn't ease the readability.
In my projects, I usually use a Provider for each Feature, which I declare and Consume at the lowest level possible.
In your case, I guess I'd juste have used your STTModel, QuestionModel and AudioModel and would have forgotten the idea of a CommonModel (whom only job is is to merge all your Providers I guess?).
You can still keep your logic, but you should take in consideration the following :
In your AudioModel class, update the method where the _audioIndex and add a notifyListeners()
class AudioModel extends ChangeNotifier {
//...
int get audioIndex => _audioIndex;
void updateIndex(int index) {
_audioIndex = index;
//The rest of your code
notifyListeners();
}
//...
}
The creation of your Providers looks alright, but consider updating the update method of your ChangeNotifierProxyProvider for something like that :
update: (_, sttModel, questionModel, audioModel) =>
commonModel!..update(sttModel, questionModel, audioModel),
and in your CommonModel
void update(SttModel sttModelUpdate, QuestionModel questionModelUpdate, AudioModel audioModelUpdate) {
audioModel = audioModelUpdate;
questionModel = questionModelUpdate;
sttModel = sttModelUpdate;
//Retrieve the index here from your audioModel
_audioIndex = audioModel.audioIndex;
notifyListeners();
}
This way, whenever you call your updateIndex method in your AudioModel class, the notifyListeners() will update the CommonModel and you'll have the _audioIndex up to date.
And then it should work fine, no need for your callback methods anymore !

Error while serializing Built<Object> using built_value combined with FlutterFire

I would like to combine built_value with cloud_firestore in my Flutter project. I believe it makes sense and that is how it should do it in a clean world? (see https://firebase.flutter.dev/docs/firestore/usage/#typing-collectionreference-and-documentreference).
This is where I am so far.
The Place model:
// ../models/place.dart
import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:sitback/models/serializers.dart';
part 'place.g.dart';
abstract class Place implements Built<Place, PlaceBuilder> {
// Fields
String? get id;
String get name;
String get address;
BuiltList<String> get methods;
Place._();
factory Place([void Function(PlaceBuilder) updates]) = _$Place;
String toJson() {
return json
.encode(standardSerializers.serializeWith(Place.serializer, this));
}
// https://github.com/google/built_value.dart/issues/964#issuecomment-850419921
static Place? fromJson(String jsonString) {
return standardSerializers.deserializeWith(
Place.serializer, json.decode(jsonString));
}
Map<String, Object?> toFirestore() {
return serializers.serializeWith(Place.serializer, this);
}
// TODO: check if something can be improved
// https://github.com/google/built_value.dart/issues/964#issuecomment-850419921
static Place? fromFirestore(Map<String, dynamic> json) {
return serializers.deserializeWith(Place.serializer, json);
}
static Serializer<Place> get serializer => _$placeSerializer;
}
In the above code, I have two issues:
A value of type 'Object?' can't be returned from the method 'toFirestore' because it has a return type of 'Map<String, Object?>'
However, this is a solution that is proposed at multiple locartions (SO questions, github issues), so I don't understand why I get that error.
In the same reference tutorials/videos, they use Place instead of Place?, as a return value for fromJson but then I have the following error:
A value of type 'Place?' can't be returned from the method 'fromJson' because it has a return type of 'Place'
Could it be related to the null-safety recent changes?
Below are the serializers (following the doc/tutorials):
// ../models/serializers.dart
library serializers;
import 'package:built_collection/built_collection.dart';
import 'package:built_value/json_object.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:sitback/models/place.dart';
part 'serializers.g.dart';
#SerializersFor([
Place,
])
final Serializers serializers = _$serializers;
final Serializers standardSerializers =
(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();
Finally, here is the place where I want to consume a Firestore collection while using the Place "data class" defined via the built_value package:
class IndexPage extends StatelessWidget {
IndexPage({
Key? key,
}) : super(key: key);
final placesRef = FirebaseFirestore.instance
.collection('places')
.withConverter<Place>(
// TODO: ! at the end due to Place? vs Place in class definition
// would require a try-catch or something else?
fromFirestore: (snapshot, _) => Place.fromFirestore(snapshot.data()!)!,
toFirestore: (place, _) => place.toFirestore(),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: StreamBuilder<QuerySnapshot<Place>>(
stream: placesRef.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final itemData = snapshot.data!.docs[index];
return PlaceCard(itemData: itemData);
},
childCount: snapshot.data!.docs.length,
),
),
],
);
}
},
),
),
);
}
}
It is not working currently because of this line:
toFirestore: (place, _) => place.toFirestore(),. If I return {} from toFirestore(), then it compiles and works
BUT I suspect fromFirestore() is not even called: if I place assert(false); inside the function, nothing happen.. I'd except an exception.
You can't use a serialiser for Firestore. You need to manually create a map from your built value object to upload it. Once you want to fetch from Firebase you need to create a built object from a map. Haven't done it recently but it would be something like it:
static Place _fromFirestore(Map<dynamic, dynamic> placeMap) {
final List<dynamic> methods = placeMap['methods'] ?? [];
final place = Place((b) => b
..id = placeMap['id']
..name = placeMap['name']
..address = placeMap['address']
..methods.addAll(List<String>.from(methods.cast<String>()));
return place;
}