StreamProvider not rebuilding the widget tree when AsyncValue updates - flutter

I am to listening firebase AuthStateChanges stream and provide the stream with streamProvider to change the view based on the stream value. And I did this:
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream get currentUser => _auth.authStateChanges();
}
final userStream = StreamProvider.autoDispose((ref) => AuthService().currentUser);
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final user = watch(userStream);
print('AutenticationWrapper build method got called');
return user.when(
data: (data) {
if (data?.uid == null) {
print('I am currently Logged out ๐Ÿ˜”');
return LogInPage();
} else {
print('I am logged in user๐Ÿ˜');
return HomePage();
}
},
loading: () => CircularProgressIndicator(),
error: (e, s) => Text('Oops'),
);
}
}
I was expecting to have LogIn page to be rendered when the AsyncValue of the streamProvider gets changed. The above code didn't work as expected; in fact, it prints the message but it doesn't return the Widget it is supposed to return. However, when I hot restart the app it will render the correct Widget based on the stream value.
Why doesn't the build method re-render when this: final user = watch(userStream); receives an update?

I think you should probably watch(userStream.stream) to be notified when the stream itself updates. Haven't played much with StreamProvider though, so I could be wrong.

Related

How to read the data from AsyncData<void> in Riverpod

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.

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.

Flutter GetX state management initial null value

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())) :

Flutter what is the best approach to use navigator and ChangeNotifierProvider together

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.

Flutter: How to make a sequence of http requests on a widget before build method

I have 3 classes: Users, Posts and Comments. User has many Posts and
Posts has many Comments.
I want that all data to be fetched before the widget's build method is called.
I tryed to use initState() to do this:
class FetchDataExample extends StatefulWidget {
final User _user;
FetchDataExample(this._user);
#override
_State createState() => _State(_user);
}
class _State extends State<FetchDataExample> {
final User _user;
_State(this._user);
#override
void initState() {
_user.setPosts();
super.initState();
}
#override
Widget build(BuildContext context) {
print(this._user.posts[0]);
return Container(
);
}
}
In User class I have:
void setPosts() async {
String url = 'https://jsonplaceholder.typicode.com/posts?userId=' + this.id.toString();
var request = Requester.get(url); // Returns a Future<Response>
await request.then((value) => this.posts = Post.jsonToPosts(json.decode(value.body)));
this.posts.forEach((post) => post.setComments());
print(this.posts[0]);
}
The 'setComments()' has the same logic.
I have two prints:
Inside build that returns null;
Inside setPosts the returns Instance of 'Post';
So, by the time that Build method is called in the widget, the initState has not finished yet.
I need it be finished, does anyone know how can I do that?
You can use a FutureBuilder to build a widget by using latest result from a future.
And also you can combile multiple futures into a single one using Future.wait method.
Here is a sample code:
_getPageData() async {
var _combinedFutures = await Future.wait([setPosts, setComments]);
//do stuff with data
}
...
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:_getPageData(),
builder: (context, snapshot) {
return Container();
}),
);
});