Flutter Firestore Wait untill all data read - flutter

i am trying to build an e commerce app. I got stuck while i was trying to build shopping cart. I read ids of product from cart collections and read products by those ids. Need to wait untill all product models are read and then loading state should turn false. But i can read only ids in cart and state turn false. When it happens, cart looks still empty even there is something in it. How can i solve this issue? I could not grab the idea of asynchronous function i guess. Thanks.
Read data from carts collection;
Future <void> yukleme () async
{
CollectionReference cartReference = FirebaseFirestore.instance.collection("carts");
DocumentSnapshot snapshot = await cartReference.doc("1").get();
cart_model = Cart_Model.fromJson(snapshot.data());
print(cart_model.product_ids);
setState(() {
loading = false; // it turns false when cart model is done but product models havent read
});
return;
}
Cart model which contains id and product models in it.
Cart_Model.fromJson (Map<String, dynamic> snapshot)
{
product_ids = snapshot["product_ids"].cast<String>() ;
if (product_ids.length > 0 && product_ids != null )
{
product_ids.forEach((String id) {
productReference.doc("$id").get().then((DocumentSnapshot snapshot){
ProductModel productModel = ProductModel.fromJson(snapshot.data());
});
});
}
}
Here where i call yukleme() function;
class _CartState extends State {
bool loading = true;
Cart_Model cart_model;
#override
Widget build(BuildContext context) {
if(loading == true)
{
yukleme();
}
...................

Related

Flutter - How can I get the Firestore items that contain their id in an array in another table as snapshots?

How can I get Firestore items containing their id in an array in another table as snapshots in Flutter? I am attaching the code that I have that works perfectly for me doing a "get", but I can't find a way to convert this to Stream and print it on the screen with the StreamBuilder instead of with the FutureBuilder and update it with each change
Future<List<DocumentSnapshot<Map<String, dynamic>>>?> getPools() async {
List<DocumentSnapshot<Map<String, dynamic>>> pools = [];
final user = FirebaseAuth.instance.currentUser;
final DbUser? dbUser = await dbUserAPI.getDbUser(user);
if (dbUser != null) {
for (var pool in dbUser.pools) {
final result = await FirebaseFirestore.instance
.collection('pools')
.doc(pool)
.get();
pools.add(result);
}
return pools;
}
if (kDebugMode) {
print('Error al leer el usuario de FireStore');
}
return null;
}
In the dbUsersAPI.getDbUsers function I retrieve the user data from the "Users" table and then I get the value ".pools", which is an array of Strings with the ids of the items I want to retrieve.
I have tried many ways and to play with Streams but I am always getting a Future or Stream when I only want to get a Stream of the items that I am filtering.
I have tried with the where clause but it does not update the values. I think the problem is that I don't know how to manage the Future returned by the dbUsersAPI.getDbUsers function.
Well for displaying data using a StreamBuilder you need to fetch data in streams by generating a requests that ends with .snapshots() method instead of a .get() method.
A pretty simple scenario will be,
DbUser? dbUser;
getDbUser() async {
final user = FirebaseAuth.instance.currentUser;
final DbUser? _dbUser = await dbUserAPI.getDbUser(user);
if(_dbUser != null){
dbUser = _dbUser;
}
}
#override
void initState(){
getDbUser();
super.initState();
}
#override
void build(BuildContext context){
return ListView.builder(
itemCount: dbUser!.pools.length,
itemBuilder: (context, index){
final pool = dbUser!.pools[index];
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('pools')
.doc(pool)
.snapshots(),
builder: (context, snapshots){
return Container();
}
}
);
);
}

retrieving firebase data in init state

I want to fetch a simple document from firebase and store it in a map as soon as the screen loads,
I'm trying to do this through the init state, i.e. calling the fetching function in the init state, but my map is still null.
Here is some code :
Map<String, int> minDelivery;
void minimumDelivery() async{
await FirebaseFirestore.instance
.collection("minimumDelivery")
.limit(1)
.get()
.then((QuerySnapshot qs) {
qs.docs.forEach((doc) {
int amount = doc["min_amount"];
int charge = doc["charge"];
minDelivery = {
"amount":amount,
"charge":charge
};
});
});
}
#override
void initState() {
// TODO: implement initState
super.initState();
minimumDelivery();
}
But the map is null when I am trying to access its data.
I don't see where the problem is.I just want the map to have the required data at the start of screen.

Does streambuilder from firebase rtdb will update list<User> user data?

currently I understadn with the method streamBuilder I can fetch updated data and add in the List<User> users.
But what if this user which is already added in the List<User> users has updated data, and then it could be double adding this user data in the List<User> users right?
Could you plz show me how to confirm whether for the new coming data List<User> users has already same userId, if yes, the new data / userId will replace this exisiting userId?
If the user is deleted from Firebase rtdb, the stream will be notified, and therefore remove this user from List<User> users?
here is example, my concern is since stream will always add data to the List users, but what if this user is removed from database or disconnect, how to remove this user from this list?
_streamSubscription = availableUserStream.onValue.listen((snap) {
if (snap.snapshot.exists && snap.snapshot.value != null) {
DataSnapshot snapshotData = snap.snapshot;
for (var userSnapshot in snapshotData.children) {
final data = Map<String, dynamic>.from(userSnapshot.value as Map);
List<User> users = [];
User newUser = User.fromJson(data);
users.add(newUser);
firebaseController.setUsers(users: users);
}
}
});
So I thought to do a double confirm here if this user is still exisitng in the database:
User getRandomSenderUser({User asReceiverUser}) {
if (availableSenderUsersList.isNotEmpty) {
final random = Random();
var i = random.nextInt(availableSenderUsersList.length);
User randomUser = availableSenderUsersList[i];
bool thisRandomUserIsAvailable; //TODO
I don't know how to do this check, e.g. if this randomerUser is unavailable, so I need to get next randomUser, so it should be a loop? But it will slow down the response speed.
updateSenderUserAvailableStatus(asReceiverUser:asReceiverUser,connectionUser: randomUser);
return randomUser;
} else {
return null;
}
}
thank you!
Update:
Here is the example code, so now I understand stream will pass user data to List<User> users, but in my way there will always be user who is added in this list before, but was already removed from database, my plan is using while loop for double confirming to remove unavailable user when getting the randomUser, but it sounds not smart and still waste time I guess....
#override
void initState() {
_listenAvailableUsers();
}
_listenAvailableUsers() {
var availableUserStream =
FirebaseDatabase.instance.ref().child('/waitingList');
_streamSubscription = availableUserStream.onValue.listen((snap) {
if (snap.snapshot.exists && snap.snapshot.value != null) {
DataSnapshot snapshotData = snap.snapshot;
for (var userSnapshot in snapshotData.children) {
final data = Map<String, dynamic>.from(userSnapshot.value as Map);
List<User> users = [];
User newUser = User.fromJson(data);
users.add(newUser);
firebaseController.setUsers(users: users);
}
}
});
}
Here is the method I though to confirm if the randomUser is still existing in the database:
Future<User> getRandomSenderUser({User asReceiverUser}) async {
if (availableSenderUsersList.isNotEmpty) {
User randomUser;
while (true) {
final random = Random();
var i = random.nextInt(availableSenderUsersList.length);
randomUser = availableSenderUsersList[i];
DatabaseEvent event = await databaseReference
.child('/waitingList/${randomUser.userId}')
.once();
print('randomUser is ${randomUser.toString()}');
if (event.snapshot.value != null) {
break;
}
}
await updateSenderUserAvailableStatus(
asReceiverUser: asReceiverUser, connectionUser: randomUser);
print('connectionUserId is $connectionUserId');
return randomUser;
} else {
return null;
}
}
Since you're listening to the onValue of a path in the database, the DataSnapshot you get will contain the entire data at that path. When there was only a small change in the data, the server will only send that update to the client, but the SDK will then merge that with the existing data and still fire an event with a snapshot of all the data at the path.
Since you're starting with an empty list (List<User> users = [];) each time you get an event from the stream, that means you're rebuilding the entire lit of users each time, which seems correct to me.

Riverpod StateNotifierProvider depend on a FutureProvider

I have a StateNotifierProvider that depends on a FutureProvider. Currently they look like below.
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider.future); // future provider
return CatalogNotifier(network: network);
});
this makes my CatalogNotifier accept a Future<NetworkProvider> instead of NetworkProvider and requires me to do things like below.
await (await network).doGet(...)
What's the best way to avoid having to await multiple and allow CatalogNotifier to accept a bare NetworkProvider so I can write like await network.doGet(...) ?
for completeness as requested, below is the other related providers
final networkProvider = FutureProvider<Network>((ref) async {
final cache = await ref.watch(cacheProvider.future);
return Network(cacheManager: cache);
});
final cacheProvider = FutureProvider<CacheManager>((ref) async {
final info = await ref.watch(packageInfoProvider.future);
final key = 'cache-${info.buildNumber}';
return CacheManager(Config(
key,
stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 100,
));
I'm sure I can take my cache provider as a future into the network provider, so it doesn't have to be a FutureProvider, but I'm interested in how to solve the issue above, since in another scenario, if I depend on say 3 or 4 FutureProviders, this may not be an option.
this makes my CatalogNotifier accept a Future instead of >NetworkProvider and requires me to do things like below.
I can't think of a way to get your desired result.
Could you not just accept an AsyncValue and handle it in the statenotifier?
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider); // future provider
return CatalogNotifier(network: network);
});
Then you can:
void someFunction() async {
network.maybeWhen(
data: (network) => AsyncData(await network.doGet(...)),
orElse: () => state = AsyncLoading(),
);
}
with riverpod v2 and its codegen features this has become much easier since you no longer have to decide the type of the provider. (unless you want to)
StateNotifier in riverpod 2
#riverpod
Future<CatalogController> catalog(CatalogRef ref) async {
final network = await ref.watch(networkProvider.future);
return CatalogController(network: network);
}
Alternative approch in Riverpod 2
Quite often you want to have a value calculated and have a way to explicitely redo that calculation from UI. Like a list from network, but with a refresh button in UI. This can be modelled as below in riverpod 2.
#riverpod
Future<CatalogState> myFeed(MyFeedRef ref) async {
final json = await loadData('url');
return CatalogState(json);
}
// and when you want to refresh this from your UI, or from another provider
ref.invalidate(myFeedProvider);
// if you want to also get the new value in that location right after refreshing
final newValue = await ref.refresh(myFeedProvider);
Riverpod 2 also has loading and error properties for the providers. You can use these to show the UI accordingly. Though if you want to show the last result from the provider while your feed is loading or in an error state, you have to model this yourself with a provider that returns a stream/BehaviorSubject, caches the last value .etc.
you can make AsyncValue a subtype of StateNotifier, I use the Todo list as an example.
as follows:
class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
TodoNotifier(this._ref) : super(const AsyncValue.loading()) {
_fetchData();
}
final Ref _ref;
Future<void> _fetchData() async {
state = const AsyncValue.loading();
// todoListProvider is of type FutureProvider
_ref.read(todoListProvider).when(data: (data) {
state = AsyncValue.data(data);
}, error: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace: stackTrace);
}, loading: () {
state = const AsyncValue.loading();
});
}
void addTodo(Todo todo) {
if (state.hasValue) {
final todoList = state.value ?? [];
state = AsyncValue.data(List.from(todoList)..add(todo));
}
}
....
}

why the state could not get the newest in flutter after change the state in reducer

I am get the flutter state in buildView function like this:
Widget buildView(ArticleListState state, Dispatch dispatch, ViewService viewService) {
final articles = state.articles;
}
but the state could not get the newest, I am dispath to set articles in initial function in effect.dart like this:
Future _onInit(Action action, Context<ArticleListState> ctx) async {
ArticleListState articleListState = ctx.state;
List<int> ids = articleListState.articleIds;
List<Item> articles = new List();
for(final i in ids){
Item article = await Repo.fetchArticleItem(i);
if (article != null) {
articles.add(article);
}
}
if (articles != null) {
ctx.dispatch(ArticleListActionCreator.onSetArticles(articles));
}
}
and in reducer change the state:
ArticleListState _onSetArticles(ArticleListState state, Action action) {
ArticleListState newState = state.clone();
List<Item> articles = (action.payload as List<Item>);
newState.articles = articles;
newState.articleRequest.latestTime = 1;
return newState;
}
I think when return the newstate, the UI buildView should trigger the rerender,and the state may be the newest with article info. but the article info aways get null, and did not trigger rerender actuallly. what should I do to fix it?
you should implement the connector set function like this:
#override
void set(HomeListDefaultState state, ArticleListState subState) {
state.articleListState.articles = subState.articles;
}