Provider listening to StreamProvider not rebuilding - flutter

I have the following code using riverpod:
///
/// Data dependencies
///
final authRepositoryProvider = Provider(
name: 'authRepositoryProvider',
(ref) {
return AuthRepositoryImpl(
authClient: ref.watch(firebaseAuthClientProvider),
);
},
);
///
/// Application dependencies
///
final authStateStreamProvider = StreamProvider<User?>(
name: 'authStateStreamProvider',
(ref) {
return ref.watch(authRepositoryProvider).authStateChange;
},
);
final authCheckerProvider = Provider(
name: 'authCheckerProvider',
(ref) {
var authStateStreamData = ref.watch(authStateStreamProvider);
authStateStreamData.when(
data: (User? data) {
debugPrint('data');
},
error: (Object error, StackTrace stackTrace) {
debugPrint('error');
},
loading: () {
debugPrint('loading');
},
);
},
);
The authCheckerProvider is not getting rebuilt when new data is received by the authStateStreamProvider. Shouldnt it rebuild? ref.read(authRepositoryProvider).authStateChange returns the Firebase userChanges() stream.
I tried ref.watch(authStateStreamProvider.stream) and added a listener to it and the new data is effectively coming through but I don't know why its not working by simply listening to the StreamProvider.

Related

riverpod state not updating

(Update at the end of the post) I want to add my normal firebase auth with additional user information. In this example, name and goal calories. For that, I created this register function:
Future<void> signUpWithEmailAndPassword(String email, String password, BuildContext context, WidgetRef ref, widget) async {
FocusManager.instance.primaryFocus?.unfocus();
try {
await auth.createUserWithEmailAndPassword(email: email, password: password);
ref.read(isUp.notifier).state = false;
ref.read(writeItemViewModelProvider).setInitValue();
} on FirebaseAuthException catch (e) {
the function setInitValue() looks like this:
class FirestoreDb extends ChangeNotifier {
Future<void> setInitValue() async {
await firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').doc(auth.currentUser!.uid).set({
'name': null,
'calories': null,
});
}
}
Here seems to work everything fine. Inside firestore a file gets created and my user also. Without this additional user infos my auth works also fine. So I think there is a problem with my stream of the user information. Because: I have to check if the registert user has already added information or not.
I do this with a second .when function:
#override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
final watcher = ref.watch(itemsProvider);
return authState.when(
data: (data) {
if (data != null) {
return watcher.when(data: (calo) {
if (calo.first.calories != null) {
return const RootPage();
} else {
return UserInformation();
}
}, error: (e, trace) => ErrorScreen(e, trace), loading: () => const LoadingScreen());
the first .when function is for the auth, here seems to be no problem, but the secons is strange. When I login first time, it says bad state. From now on, every time I register with a different account, I only get the old data from the previous account until I hot restart.
After the user information, you get to this page:
#override
Widget build(BuildContext context, WidgetRef ref) {
final streamData = ref.watch(itemsProvider);
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar(toolbarHeight: 0, backgroundColor: Colors.transparent),
resizeToAvoidBottomInset: false,
body: streamData.when( data: (calo) {
return Text(calo.first.calories.toString());
}, error: (e, trace) => ErrorScreen(e, trace), loading: () => const LoadingScreen())
);
}
where I can see that s old information until hot restart.
So something with my stream is not updating the state correctly.
When I wrap delete the .when function and use a Streambuilder listening to the stream directly everything works.
Here is my itemsProvider:
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) => ref.read(itemRepositoryProvider).itemsStream,
);
final itemRepositoryProvider = Provider((ref) => ReadData());
class ReadData{
Stream<List<UsersModel>> get itemsStream {
return firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').snapshots().map((QuerySnapshot query) {
List<UsersModel> user = [];
for (var usersIter in query.docs) {
final usersModel = UsersModel.fromDocumentSnapshot(documentSnapshot: usersIter);
user.add(usersModel);
}
return user;
});
}
}
I check with debugging and "print points" the way of the compiler and recognised the problem but have no answer why the compiler do this:
#override
Widget build(BuildContext context, WidgetRef ref) {
print("inside UserInfoBuild");
final watcher = ref.watch(itemsProvider);
return watcher.when(data: (userInfoData) {
print("inside AsyncValue<List<UsersModel>>");
if (userInfoData.first.calories != null) {
return const RootPage();
} else {
return UserInformation(); [...]
declare provider:
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) {
print("inside stream provider");
return ref.read(itemRepositoryProvider).itemsStream;
},
);
so, my guess was that the print order should be:
I/flutter: inside UserInfoBuild
I/flutter: inside stream provider
I/flutter: inside AsyncValue<List<UsersModel>>
but its actually just:
I/flutter: inside UserInfoBuild
I/flutter: inside AsyncValue<List<UsersModel>>
so the compiler skips the final itemsProvider = StreamProvider.
Just after a hot restart it executes the line of code
I think the key point is 'get' itemsStream. You have two ways to try.
// 1.
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) => firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').snapshots().map((QuerySnapshot query) {
List<UsersModel> user = [];
for (var usersIter in query.docs) {
final usersModel = UsersModel.fromDocumentSnapshot(documentSnapshot: usersIter);
user.add(usersModel);
}
return user;
}),
);
// 2.
You can use StreamController to get data from firebaseFirestore.collection in ReadData class, and use a Stream variable to sync that value. Update StreamProvider to the Stream variable.

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: Access SharedPreferences Provider / ChangeNotifier in a Stream Class

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.

Where to handle Firebase Dynamic Links in Flutter?

I use Firebase dynamic links and also named routes. What I want is to install a global listener for the dynamic link events and forward to register page if a token is provided. In the code below I got the exception The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget. which means I have to put navigation code below the home: property of MaterialApp. But when doing this I had to implement the dynamic links event handler for earch route.
class MyApp extends StatelessWidget {
String title = "Framr";
#override
Widget build(BuildContext context) {
FirebaseDynamicLinks.instance.onLink(
onSuccess: (linkData) {
if (linkData != null) {
try {
Navigator.pushNamed(context, '/register', arguments: linkData);
// throws: The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
} catch(e) {
print(e);
}
}
return null;
}
);
return MaterialApp(
title: "...",
home: LoginPage(),
routes: {
'/createEvent': (context) => CreateEventPage(),
'/showEvent': (context) => ShowEventPage(),
'/register': (context) => RegisterPage(),
},
);
}
}
I was able to get this work by following the example provided from the dynamic link README with the use of the no_context_navigation package or GlobalKey to workaround around the lack of context to call Navigator.pushNamed(...). Note: You don't have to use no_context_navigation. You can implement the no context routing yourself. Here's an example.
// Add this
import 'package:no_context_navigation/no_context_navigation.dart';
void main() {
runApp(MaterialApp(
title: 'Dynamic Links Example',
// Add this
navigatorKey: NavigationService.navigationKey,
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => MyHomeWidget(), // Default home route
'/helloworld': (BuildContext context) => MyHelloWorldWidget(),
},
));
}
class MyHomeWidgetState extends State<MyHomeWidget> {
.
.
.
#override
void initState() {
super.initState();
this.initDynamicLinks();
}
void initDynamicLinks() async {
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
// Add this.
final NavigationService navService = NavigationService();
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
// This doesn't work due to lack of context
// Navigator.pushNamed(context, deepLink.path);
// Use this instead
navService.pushNamed('/helloworld', args: dynamicLink);
}
},
onError: (OnLinkErrorException e) async {
print('onLinkError');
print(e.message);
}
);
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null) {
// This doesn't work due to lack of context
// Navigator.pushNamed(context, deepLink.path);
// Use this instead
navService.pushNamed('/helloworld', args: dynamicLink);
}
}
.
.
.
}
// pubspec.yaml
no_context_navigation: ^1.0.4

In Flutter How to use Providers with AMQP?

in Flutter -which I just recently begin to use-, I am trying to use an AMQP stream using dart_amqp: ^0.1.4 and use providers provider: ^3.1.0+1 to make the data available throughout the app.
Only after logging in I start the AMQP service.
The AMQP part works without any issues, I get the data but I never manage to use it with Providers.
main.dart
class BigBrother extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<EventsModel>(create: (_) => EventsModel()),
ChangeNotifierProxyProvider<EventsModel, DeviceState>(
create: (_) => new DeviceState(),
update: (context, eModel, deviceState) {
deviceState.updateFromEvent(eModel.value);
},
),
],
My models in models.dart
(As seen in below code, I also tried to used StreamProvider and commented it out)
// Global data model
class DeviceState with ChangeNotifier {
Map<String, Map<String, dynamic>> state = {};
DeviceState() {
this.state['xxx'] = {};
this.state['yyy'] = {};
}
updateFromEvent(EventsItemModel event) {
if (event != null && event.type != null) {
switch (event.type) {
case 'heartbeat':
this.state[event.device][event.type] = event.createdAt;
break;
case 'metrics':
this.state[event.device][event.type] = {}
..addAll(this.state[event.device][event.type])
..addAll(event.message);
break;
}
notifyListeners();
}
}
}
class EventsModel with ChangeNotifier {
EventsItemModel value;
bool isSubscribed = false;
AMQPModel _amqp = new AMQPModel();
// final _controller = StreamController<EventsItemModel>();
EventsModel();
// Stream<EventsItemModel> get stream => _controller.stream;
_set(String jsonString) {
value = new EventsItemModel.fromJson(jsonString);
// _controller.sink.add(value); // send data to stream
// Provider.of<DeviceState>(context, listen: false).updateFromEvent(value);
notifyListeners();
}
subscribe() {
if (!this.isSubscribed) {
this.isSubscribed = true;
this._amqp.subscribeEvents(_set); // start AMQP service after login
}
}
}
So on the login.dart view, on button pressed and validating the login, I start the AMQP stream:
onPressed: () {
if (_formKey.currentState.validate()) {
print("Login button onPressed");
Provider.of<EventsModel>(context, listen: false)
.subscribe();
Navigator.pushReplacementNamed(context, Routes.live);
}
And lastly the view after successful login:
class _LivePageState extends State<LivePage> {
#override
Widget build(BuildContext context) {
DeviceState deviceState = Provider.of<DeviceState>(context);
print('#### Device state updated');
print(deviceState.state['xxx']);
In the above code, deviceState is always null.
So after trying many combination of various Providers, I am still unable to make this work.
Would be glad to have someone's insight on this.
Best regards!