Using Riverpod in Flutter to implement session user data management - flutter

I am using Riverpod in Flutter to handle Firebase authentication, and it seems to be working fine. Additionally, I'd like to use Riverpod to handle core user data (i.e. "session" data). The problem is that if a user logs out and another logs in, the user session data is not fetched/refreshed for that new user.
Authentication (and the semi-working session data handling) is handled the following way:
return authState.when(
data: (authData) {
if (authData != null) {
// Get user data here:
final userState = ref.watch(appUserProvider);
return userState.when(
data: (appUser) {
if (appUser.isFirstRun) {
return const OnboardingPage();
} else {
return const AppRoot();
}
},
loading: () => const LoadingScreen(),
error: (e, stackTrace) => ErrorDisplay(e, stackTrace));
}
return const LoginPage();
},
loading: () => const LoadingScreen(),
error: (e, stackTrace) => ErrorScreen(e, stackTrace));
As seen above, I'd like the appUserProvider to be provided once the auth state has been provided, but am having trouble getting this nested approach to work properly. When one user logs out and another logs in, the data is not automatically refreshed without an app restart. Trying to refresh the appUserProvider explicitly (using ref.read()) does not work either.
The appUserProvider looks like this:
final appUserProvider = StateNotifierProvider<AppUserNotifier, AsyncValue<AppUser>>((ref) {
return AppUserNotifier(ref);
});
class AppUserNotifier extends StateNotifier<AsyncValue<AppUser>> {
final StateNotifierProviderRef ref;
late final UserService service;
AppUserNotifier(this.ref) : super(AsyncValue.data(AppUser())) {
service = ref.watch(userService);
get();
}
// get method omitted
How can I get this to work properly? Is it even a good approach?
Thankful for any input here!

You can use this for your StateNotifierProvider:
final appUserProvider = StateNotifierProvider.autoDispose<AppUserNotifier, AsyncValue<AppUser>>((ref) {
return AppUserNotifier(ref);
});
Then the provider will be disposed of as soon as it is no longer used. When a new user logs in, the provider is initialized again with the correct data.

Related

Riverpod FutureProvider - passing result between screens

I'm learning Riverpod provider and stuck on a topic regarding passing values between screens.
As I learnt from Riverpod docs - it delivers a provider that enables values to be accessed globally... and here is my case.
I'm creating a service repo, that contains some methods delivering futures (e.g. network request to verify user):
class VerifyUser {
Future<User> verifyUser(String input) async {
await Future.delayed(const Duration(seconds: 2));
print(input);
if (input == 'Foo') {
print('Foo is fine - VERIFIED');
return User(userVerified: true);
} else {
print('$input is wrong - NOT VERIFIED');
return User(userVerified: false);
}
}
}
Next step is to create providers - I'm using Riverpod autogenerate providers for this, so here are my providers:
part 'providers.g.dart';
#riverpod
VerifyUser verifyUserRepo(VerifyUserRepoRef ref) {
return VerifyUser();
}
#riverpod
Future<User> user(
UserRef ref,
String input
) {
return ref
.watch(verifyUserRepoProvider)
.verifyUser(input);
}
and there is a simple User model for this:
class User {
bool userVerified;
User({required this.userVerified});
}
I'm creating a wrapper, that should take the user to Homescreen, when a user is verified, or take the user to authenticate screen when a user is not verified.
class Wrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
String user = '';
final userFromProvider = ref.watch(userProvider(user));
if (user == 'verified') {
print(userFromProvider);
return MyHomePage();
} else {
return Authenticate();
}
}
}
App opens on Authenticate screen because there is no info about the user.
On Authenticate screen I'm getting input and passing it to FutureProvider for verification.
final vUser = ref.watch(userProvider(userInput.value.text));
When I'm pushing to Wrapper and calling provider - I'm not getting the value I initially got from future.
ElevatedButton(
onPressed: () async {
vUser;
if (vUser.value?.userVerified == true) {
print('going2wrapper');
Navigator.of(context).push(MaterialPageRoute(builder: (context) => Wrapper()));
}},
child: const Text('Verify User'))
Inside Wrapper it seems that this is only thing that I can do:
String user = '';
final userFromProvider = ref.watch(userProvider(user));
But it makes me call the provider with a new value... and causing unsuccessful verification and I cannot proceed to homescreen.
As a workaround, I see that I can pass the named argument to Wrapper, but I want to use the provider for it... is it possible?
I hope that there is a solution to this.
Thx!
David

Flutter redux store.dispatch(...) resetting the value of another redux state variable

The scenario is, when the app opens, we need to do two REST API calls,
Get User Function List API call
Get Chat Bubble List API call
We have two redux state variable
userFunctionList
chatBubbleList
state.dart
class AppState {
final List userFunctionList;
final List chatBubbleList;
const AppState({
required this.userFunctionList,
required this.chatBubbleList,
});
AppState.initialState()
: userFunctionList = [],
chatBubbleList = [];
}
model.dart
class AddUserFunctionList {
late final List userFunctionList;
AddUserFunctionList({
required this.userFunctionList,
});
}
class AddChatBubbleList {
late final List chatBubbleList;
AddChatBubbleList({
required this.chatBubbleList,
});
}
store.dart
final store = new Store(
appReducer,
initialState: new AppState.initialState(),
);
reducer.dart
List userFunctionsListReducer(List existingData, dynamic action) {
if (action is AddUserFunctionList) {
return action.userFunctionList;
}
return [];
}
List chatBubbleListReducer(List existingData, dynamic action) {
if (action is AddChatBubbleList) {
return action.chatBubbleList;
}
return [];
}
AppState appReducer(AppState state, dynamic action) {
return new AppState(
chatBubbleList: chatBubbleListReducer(state.chatBubbleList, action),
userFunctionList: userFunctionsListReducer(state.userFunctionList, action),
);
}
On the homepage of the app, initState() function, we are doing two API calls,
getUserFunctionList()
getChatBubbleList()
In every function after receiving response, we have store.dispatch() method, like below,
At the end of function 1,
store.dispatch(AddUserFunctionList(userFunctionList: response['data']));
At the end of function 2,
store.dispatch(AddChatBubbleList(chatBubbleList: response['data]));
And the StoreConnector inside the widget builder like,
....
....
StoreConnector<AppState, List>(
converter: (store) => store.state.userFunctionList,
builder: (context, userFunctionList) {
return UserFunctionListView(
userFunctionList: userFunctionList,
);
}
....
....
If I comment out the second function and call only the first API (getUserFunctionList()), the data updates happening on the redux variable, I am able to see the UI.
But If the second function also doing the store.dispatch... action, the first redux variable gets replaced with the initial value ([]).
Not able to do two store.dispatch action continuously.
Moreover, currently not using any middleware.
How to do two different store.dispatch calls while opening the app?

why posting data to firebase using flutter bloc is not emitting?

I'm creating an app with firebase as a database. After sending data to firebase, app screen should pop out for that I had bloclistener on the screen but after sending the data to firestore database, nothing is happening, flow is stopped after coming to loaded state in bloc file why? check my code so that you will know. I can see my data in firebase but it is not popping out because flow is not coming to listener.
state:
class SampletestInitial extends SampletestState {
#override
List<Object> get props => [];
}
class SampletestLoaded extends SampletestState {
SampletestLoaded();
#override
List<Object> get props => [];
}
class SampletestError extends SampletestState {
final error;
SampletestError({required this.error});
#override
List<Object> get props => [error];
}
bloc:
class SampletestBloc extends Bloc<SampletestEvent, SampletestState> {
SampletestBloc() : super(SampletestInitial()) {
on<SampletestPostData>((event, emit) async {
emit(SampletestInitial());
try {
await Repo().sampleTesting(event.des);
emit(SampletestLoaded());
} catch (e) {
emit(SampletestError(error: e.toString()));
print(e);
}
});
}
}
Repo: ---- Firebase post data
Future<void> sampleTesting(String des) async {
final docTicket = FirebaseFirestore.instance.collection('sample').doc();
final json = {'Same': des};
await docTicket.set(json);
}
TicketScreen:
//After clicking the button ---
BlocProvider<SampletestBloc>.value(
value: BlocProvider.of<SampletestBloc>(context, listen: false)
..add(SampletestPostData(description.text)),
child: BlocListener<SampletestBloc, SampletestState>(
listener: (context, state) {
if (state is SampletestLoaded) {
Navigator.pop(context);
print("Popped out");
}
},
),
);
im not sure but i think that you have the same hash of:
AllData? data;
try to remove AllData? data; and create new data variable so you can be sure that you has a new hash code every time you call createTicket method;
final AllData data = await repo.createTicket(AllData(
Check your AllData class properties.
BLoC will not show a new state if it not unique.
You need to check whether all fields of the AllData class are specified in the props field.
And check your BlocProvider. For what you set listen: false ?
BlocProvider.of<SampletestBloc>(context, listen: false)

how to make more than one asynchronous calls when open a page using Riverpod?

when the user of my app open the Home page, I need to make two asynchronous calls to the server
first, I need to get current user data.
and then based on the current user data, I need fetch his favorite restaurants.
I have two separate methods to get those data from server
class MyAPI {
Future<User> getUserData() async {}
Future<List<Restaurant>> getUserData() async {}
}
then how do I construct those 2 asynchronous methods in my HomePage using Riverpod?
show circular loading indicator
make those 2 asynchronous calls
hide circular loading indicator and load lisView
I know about FutureProvider from Riverpod, but FutureProvider is only for one asynchronous service right?
do I need to somehow combine those two into a single method first and then use FutureBuilder? or is it another way that more common to use? I am not sure
how to solve this issue. sorry I am a beginner in Flutter
This look like a use case of a stateNotifier:
first in your data model define a UserData class :
class UserData{
final User user;
final List<Restaurant> restaurants;
UserData(this.user, this.restaurants)
}
next define a state and it's associated stateNotifierProvider :
final userDataProvider = StateNotifierProvider<UserDataNotifier, AsyncValue<UserData>>((ref) => UserDataNotifier());
class UserDataNotifier extends StateNotifier<AsyncValue<UserData>> {
UserDataNotifier() : super(AsyncValue.loading()){
init();
}
final _api = MyAPI();
void init() async {
state = AsyncValue.loading();
try {
final user = await _api.getUser;
final List<Restaurant> restaurants = await _api.getFavoriteRestaurant(user);
state = AsyncValue.data(UserData(user,restaurants));
} catch (e) {
state = AsyncValue.error(e);
}
}
}
finally in you UI use a consumer :
Consumer(builder: (context, watch, child) {
return watch(userDataProvider).when(
loading: ()=> CircularProgressIndicator(),
error: (error,_) =>
Center(child: Text(error.toString())),
data: (data) => Center(child: Text(data.toString())));
})

Why can't I get last document from cloud firestore in flutter using provider package?

I am banging my head with this and I need your help guys. Please help me with this.
I am currently getting streams from firestore and it's working fine, but the problem is I want to implement pagination now and currently, I can't get the value of the last document which is why I can't use startAfter feature. Have a look into my code
Code on parent page i.e. homepage.dart
StreamProvider<List<Cars>>.value(
value: DatabaseService().getCars(),
catchError: (ctx, err) => null,
child: ChangeNotifierProvider(
create: (context) => LastDocumentTracker(),
child: Scaffold()
Code on database Service page:
getCars({bool getMore = false}) {
var collection = carsCollection.orderBy('dueDate').limit(15);
if(!getMore ) {
return collection.snapshots().map((event) {
LastDocumentTracker().changeLastDocument(event.docs.last);
return _carsListFromSnapshot(event);
});
}
}
Now I got a class with ChangeNotifier
class LastDocumentTracker with ChangeNotifier{
List <QueryDocumentSnapshot> _snapshot = [];
QueryDocumentSnapshot get getLastDocument {
return _snapshot.last;
}
void changeLastDocument (QueryDocumentSnapshot doc){
print('Snapshot $_snapshot'); // here I can see the snapshot on console but on other pages where I am listinig its null.
_snapshot.add(doc);
notifyListeners();
}
}
I was thinking to get the value of the last document from the getter getLastDocument however I am unable to get it because it's always null.
Please help me to implement pagination because I don't want a whole bunch of data to be accessed by users at once.
Every time you do LastDocumentTracker(), you are creating a new instance of LastDocumentTracker with _snapshot = []. Hence, you are getting the last element as null. Convert LastDocumentTracker into a singleton:
class LastDocumentTracker with ChangeNotifier{
static LastDocumentTracker _instance;
List <QueryDocumentSnapshot> _snapshot;
LastDocumentTracker._construct() {
_snapshot = [];
}
factory LastDocumentTracker() {
if(_instance == null) _instance = LastDocumentTracker._construct();
return _instance;
}
QueryDocumentSnapshot get getLastDocument {
return _snapshot.last;
}
void changeLastDocument (QueryDocumentSnapshot doc) {
_snapshot.add(doc);
notifyListeners();
}
}
Edit
As you mentioned about the providers, it is better not to go with the singleton answer I provided. Instead, you can replace this:
LastDocumentTracker().changeLastDocument(event.docs.last);
with
final tracker = Provider.of<LastDocumentTracker>(context, listen: false);
tracker.changeLastDocument(event.docs.last);
This way, you are accessing the tracker instance that your provider holds. This is better than the singleton pattern I mentioned as it makes the class reusable using the provider.
Note:
You need context to access provider of that context so pass the context to the getCars method from wherever you are calling it.
set listen to false otherwise, you won't be able to access getCars from methods like buttonPress callbacks or initState etc.