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
Related
I am trying to implement simple authentication using Riverpod package in flutter. State of my controller after calling the sign in function prints this: "AsyncData(value: Instance of 'Response')". How can I extract the body of response from this kind of data type?
You can do it like this:
/// A provider that asynchronously exposes the current user
final userProvider = StreamProvider<User>((_) async* {
// fetch the user
});
/// or example
final userProvider = FutureProvider<User>((_) async {
// fetch the user
});
class UserAuthenticationWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Oops, something unexpected happened'),
data: (user) => Text('Hello ${user.name}'),
);
}
}
Note AsyncValueX. You can use any method that suits you.
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.
This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :
I have a problem like this :
In Splash Page , i check in sharedpreference to get saved token when login successfully .If i have token , i request Api to get account information and move to next page like this:
Future check() async {
String _getToken = await splashBloc.getTokenFormSharedPref();
if (_getToken=='0') {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LoginMain()));
} else {
splashBloc.getAccountInfo();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomeScreenMain()));
}
}
and this is BLoC class:
class SplashBloc extends BlocBase{
String _getToken = '';
Future<String> getTokenFormSharedPref() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
_getToken = (prefs.getString('token') ?? '0');
return _getToken;
}
final accountInfoController = new StreamController<Account>();
Sink<Account> get accountInfoSink => accountInfoController.sink;
Stream<Account> get accountInfoStream => accountInfoController.stream;
Future getAccountInfo() async{
Account account = await NetworkService().getAccountInfo2(_getToken);
accountInfoSink.add(account);
print('from splash: '+account.fullName);
}
#override
void dispose() {
accountInfoController.close();
}
}
When i check log , it totally request successfully and the problem is how can i acesss data in streambuilder in next page that is HomeScreenMain()?
Thanks for help!!
You can declare a variable in HomeScreenMain() and send the data you received before to the class constructor like this:
HomeScreenMain() {
final data;
HomeScreenMain(this.data)
//....
}
and when you want to call this widget you can pass that data from block to this widget
You appear to use a very basic approach with BLoC. Not sure if my answer helps there.
But if you use the library flutter_bloc, then you can use on the next page
ˋfinal bloc = context.read;
This looks for a provider of this bloc type upstream in the Widget tree and assigns it to ˋbloc
I'm new to flutter, this question may be a very basic one.
I have a firebase phone auth login page to implement this,
if the user is logged in, then navigate to home page
else if the user is a new user, then navigate to the sign-up page
The problem is, whenever the values are changed at the provider, the consumer will get notified and rebuild the build method. I won't be able to listen to them within the build method and return a Navigator.of(context).pushNamed(). Any idea what is the right way to use ChangeNotifierProvider along with listeners and corresponding page navigation?
I have Login class and provider class as below,
class LoginPage extends StatefulWidget {
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LoginProvider(),
child: Consumer<LoginProvider>(builder: (context, loginState, child) {
return Scaffold(
...
body: RaisedButton(
onPressed: **loginState.doLogin(_textController.text, context);**
...
)
}),
);
}
}
class LoginProvider with ChangeNotifier {
bool _navigateToSignup = false;
bool get navigateToSignup => _navigateToSignup;
Future doLogin(String mobile, BuildContext context) async {
FirebaseAuth _auth = FirebaseAuth.instance;
_auth.verifyPhoneNumber(
...
verificationCompleted: (AuthCredential credential) async {
UserCredential result = await _auth.signInWithCredential(credential);
User user = result.user;
// if user is new user navigate to signup
// do not want to use Navigator.of(context).pushNamed('/signupPage'); here, instead would like to notify listeners at login page view and then use navigator.
if (user.metadata.creationTime == user.metadata.lastSignInTime) {
_navigateToSignup = true;
} else {
if (result.user != null) {
_navigateToHome = true;
//Navigator.of(context).pushNamedAndRemoveUntil('/homePage', ModalRoute.withName('/'));
}
}
notifyListeners();
},
...
);
}
}
Thanks in advance.
There are several approaches, you choose the one that suits you best.
Pass the context to the ChangeNotifier as you are already doing. I don't like this as well, but some people do it.
Pass a callback to your ChangeNotifier that will get called when you need to navigate. This callback will be executed by your UI code.
Same as 2, but instead of a callback export a Stream and emit an event indicating you need to Navigate. Then you just listen to that Stream on your UI and navigate from there.
Use a GlobalKey for your Navigator and pass it to your MaterialApp, than you can use this key everywhere. More details here.