How to know if the writes in Firebase are on the server or only cached? - flutter

I built an Agro App, where the majority of users are Offline when they register the data and when they return to the central site they obtain an Internet connection and the data goes up.
However, it is not clear to the user whether or not his record was uploaded to the cloud, so I would like to implement a tick system similar to that used by WhatsApp:
Gray tick when the data is written and is only in cache
Blue tick when the data uploads to the cloud and therefore is available to other users
What I imagine is something like this:
The procedure with which I display the list is as follows:
Widget _crearListado(BuildContext context, AlimentarBloc alimentarBloc) {
return Column(
children: <Widget>[
Container(
child: Padding(
child: StreamBuilder(
stream: alimentarBloc.alimentarStream,
builder: (BuildContext context, AsyncSnapshot<List<AlimentarModel>> snapshot){
if (snapshot.hasData) {
final alimentarList = snapshot.data;
if (alimentarList.length == 0) return _imagenInicial(context);
return Container(
child: Stack(
children: <Widget>[
ListView.builder(
itemCount: alimentarList.length,
itemBuilder: (context, i) =>_crearItem(context, alimentarBloc, alimentarList[i]),
],
),
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('assets/Preloader.gif'), height: 200.0,));
},
),
),
),
],
);
}
Widget _crearItem(BuildContext context, AlimentarBloc alimentarBloc, AlimentarModel alimentar) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
Container(
child: Card(
child: ListTile(
leading: Container(,
child: Text(" ${alimentar.nombreRefEstanque}"),
),
title: Text('${alimentar.nombreAlimentoFull}'),
subtitle: Container(
child: Container(
child: Text ('$_fechaAlimentar)
),
),
onTap: () => null,
trailing: Container(
child: Text("${alimentar.consumo),
),
)
),
],
);
}
What options do you see to mark the data when they have already uploaded to the Internet? Can I do that?

Unfortunately the Firebase Realtime Database does not have a built-in marker on a data snapshot to indicate whether it's been synchronized to the server.
The simplest approach to implement something like this is to add a completion listener to the write operation, and mark the write as completed when this listener is invoked. This only works while the app remains active however. If the app is restarted, your data will be synchronized later, but no completion handler will be invoked.
If you also want to handle that case, you could write a marker value into the database when the app starts, and add a completion listener for that too. Once the completion listener for the marker value is written, you know that all writes that were queued up before that were also processed by the server.
You could combine the two approaches and:
Keep a set of outstanding write operations in local storage.
Add the key of each item that you write.
Remove the key for an item when its completion handler is called.
Clear the entire list when he app is restarted and your marker value is confirmed.
By the way: this is one area where Cloud Firestore (the other database that is part of Firebase) has a much better API, as it has a hasPendingWrites property that indicates if there are pending writes on the snapshot.

Related

Flutter: Is it possible to use single JSON file in bodies of more expansion panels?

In my app, I have one JSON file with some static data which I use to produce List of a widgets. Then, I have one screen with ExpansionPanelRadio showing few items and each of them, when expanded, (in their bodies) are containing that list of a widgets made using JSON file.
I am using provider and I am able to display that list of widgets inside body of expansionpanel but the lists are somehow repeating.
For example, I expand one panel and in its body list is displayed few times, like in a loop. I guess, that the problem is in provider but I don't understand it quite well.
I am pretty new to flutter and would appreciate if someone could explain me why is this happening and what approach should I use to solve it.
here is part of a code where i make those expansion panels with provided JSON in a body:
SingleChildScrollView(
child: ExpansionPanelList.radio(
elevation: 0,
children: MyList.map<ExpansionPanelRadio>((Item item) {
return ExpansionPanelRadio(
value: MyList.indexOf(item),
headerBuilder: (BuildContext context, bool isExpanded) {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SvgPicture.asset(
"assets/images/some_image.svg"
),
),
Text('some label'),
],
);
},
body: ChangeNotifierProvider(
create: (context) => FeaturesProvider(),
builder: (context, child) {
Provider.of<FeaturesProvider>(context, listen: false)
.readFeatures();
return SingleChildScrollView(
child: Container(
child: Features(item.objectId.toString()),
),
);
}));
}).toList(),
))

pointless Api requests occuring in future builder flutter

I have a Future Builder in my flutter app and it displays --
Error : if there's an error in json parsing
Data : if everything goes smooth
Loader : if its taking time
Everything works. the Future is calling a 'future' function thats doing a get request of some student data and the 'builder' is displaying it. I have an edit dialog box on the same page. I can edit the student information through the put request. The problem is that when I click on the form fields in the edit dialog box, I notice that get request is automatically happening approx 10 times. When I save the edits, a confirmation dialog box appears that data is updated. While this happens again get requests happens upto 10 times. And then it pops. So there are round about 20 useless requests happening on the server.
I think it happens because when I click the form fields the keyboard appears and the underlying displaying widget rebuilds, calling the api. When data is edited keyboards goes back into its place again widget rebuilds, calling the api. How can I resolve this issue ?
this is the code if it helps :
child: FutureBuilder(
future: APIs().getStudentDetails(),
builder: (context, data) {
if (data.hasError) {
return Padding(
padding: const EdgeInsets.all(8),
child: Center(child: Text("${data.error}")));
} else if (data.hasData) {
var studentData = data.data as List<StudentDetails>;
return Padding(
padding: const EdgeInsets.fromLTRB(0, 15, 0, 0),
child: SingleChildScrollView(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.9,
child: ListView.builder(
itemCount: studentData.length,
itemBuilder: ((context, index) {
final student = studentData[index];
final id = student.studentId;
final father = student.fatherName;
final mother = student.motherName;
final cg = student.cg;
final cityName = student.city;
final studentName = student.studentName;
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: SingleChildScrollView(
child: GestureDetector(
onDoubleTap: () {
edit(context, id!, studentName!, father,
mother, cg, cityName!);
},
child: Column(children: [
CustomReadOnlyField(
hintText: id.toString()),
CustomReadOnlyField(hintText: studentName),
CustomReadOnlyField(hintText: father),
CustomReadOnlyField(hintText: mother),
CustomReadOnlyField(
hintText: cg.toString()),
CustomReadOnlyField(hintText: cityName),
]),
),
),
),
);
}),
scrollDirection: Axis.vertical,
),
),
),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
I followed this answer and it worke. Flutter FutureBuilder gets constantly called
Apparantly I had to 'Lazily initializing my Future' and 'Initializing my Future in initState:'
Create a state variable for future like
late final future = APIs().getStudentDetails();
and use
FutureBuilder(
future: future ,
You can check Fixing a common FutureBuilder and StreamBuilder problem
class _YourWidgetState extends State<YourWidget> with AutomaticKeepAliveClientMixin<YourWidget> {
#override
bool get wantKeepAlive => true;
So extend your Widget with AutomaticKeepAliveClientMixin so items inside Listview will not be reproduced

Flutter Firebase Retrieve Different Users Data

My Firebase Form like this;
Users
User1 ID
- Posts
User2 ID
- Posts
The number of users changes according to the number of users in the application.
What I am trying to do is to show the posts of the users I have selected on my home screen.
So first of all I created a list like this(Users I want to show their posts);
List<dynamic> userIDs = [
"User1ID",
"User2ID"
];
Then I tried to use these elements in the list in a reference inside a for loop, Because I tried to show more than one user's post.
This is CollectionReference in for loop;
final firestore = FirebaseFirestore.instance;
var userPostsRef;
for (int i = 0; i < userIDs.length; i++) {
userPostsRef = userPostsRef.firestore.collection('users/${userIDs[i]}/Posts');
}
But it didn't work.
When I use CollectionReference like this;
var userPostsRef = firestore.collection('users/${userIDs[0]}/Posts');
It worked but I dont want to show single user posts, I want to show multiple users posts.
How can I show multiple users' posts on my home screen with this method or a different method?
Added StreamBuilder part;
StreamBuilder<QuerySnapshot>(
stream: userPostsRef,
builder:
(BuildContext context, AsyncSnapshot asyncsnapshot) {
if (asyncsnapshot.hasError) {
return Center(
child: Text("Error"),
);
} else {
if (asyncsnapshot.hasData) {
List<DocumentSnapshot> listOfDocumentSnapshot =
asyncsnapshot.data.docs;
return ListView.builder(
physics: ScrollPhysics(),
shrinkWrap: true,
itemCount: listOfDocumentSnapshot.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 12.0),
child: Container(
child: Column(
children: <Widget>[
Stack(
children: <Widget>[
Align(
alignment: Alignment.topCenter,
child: ClipRRect(
borderRadius:
BorderRadius.circular(24),
child: GestureDetector(
onTap: () => navigateToDetail(
listOfDocumentSnapshot[
index]),
child: Image(
height: 320,
width: 320,
fit: BoxFit.cover,
image: NetworkImage(
listOfDocumentSnapshot[
index]["photo"]),
),
),
),
),
],
),
],
),
),
);
},
);
}
else {
return Center(
child: CircularProgressIndicator(
color: Colors.orangeAccent[400],
),
);
}
}
},
),
There is no way to load from the Posts subcollections from a list of UIDs and not from others.
The closest that Firestore supports is loading from all Posts collections with a collection group query, or from all Posts subcollections under a specific path with the trick samthecodingman showed here: CollectionGroupQuery but limit search to subcollections under a particular document
If you can't change your data model to allow getting the data from the relevant subcollections with a single query, you will have to execute multiple queries and merge the results in your application code.
You cant use query collection reference like this
var userPostsRef = firestore.collection('users/${userIDs[0]}/Posts');
my trick I used for my app
if you want to load all post under user id
I recommend to wrap future builder (post list) under future builder (user list)
the other way is set user id with something you can track like (I use firebase.auth.email for document id reference so query will be easier)
or You can get all user id reference at the start of app and put that in memory variable so you can reduce redundancy get user id reference for the rest of functionality (mind about delete/changed user etc)
the other way around is make a dynamic query based on user action (this need more work around base on ur app)

A dismissed Dismissible widget is still part of the tree

There seem to be many questions regarding this error but I'm yet to find an answer that will work in my situation.
The behaviour I'm seeing is that the Dismissible works, it fires and deletes the item, but for a moment it shows an error in the ListView. I'm guessing it's waiting for the tree to update based on the Stream<List>, which in turn is removing the record from Firebase.
My StreamBuilder...
return StreamBuilder<List<Person>>(
stream: personBloc.personsByUserId(userId),
builder: (context, snapshot) {
...
}
My ListView.builder()
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
var person = snapshot.data[index];
return GestureDetector(
onTap: () {
Navigator.of(context)
.pushNamed('/reading/${person.personId}');
},
child: Dismissible(
key: Key(person.personId),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
personBloc.deletePerson(person.personId);
},
background: Container(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(
FontAwesomeIcons.trash,
color: Colors.white,
),
Text(
'Delete',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.right,
),
],
),
),
color: Colors.red,
),
child: AppCard(
//Bunch of properties get set here
),
),
);
},
My deletePerson
deletePerson(String personId) async {
fetchPersonId(personId).then((value) {
if (value.imageUrl.isNotEmpty) {
removeImage();
}
db.deletePerson(personId);
});
}
I've tried changing the onDismissed to a confirmDismiss with no luck.
Any suggestions?
This happens when you dismiss with a Dismissible widget but haven't removed the item from the list being used by the ListView.builder. If your list was being stored locally, with latency not being an issue, you might never see this issue, but because you are using Firestore (I assume, based on your mention ofFirebase) then there is going to be some latency between asking the item to be removed from the DB and the list getting updated on the app. To avoid this issue, you can manage the local list separately from the list coming from the Stream. Updating the state as the stream changes, but allowing you to delete items locally from the local list and avoiding these kind of view errors.
I ended up making a couple of changes to my code to address this.
I added a BehaviourSubject in my bloc to monitor whether the delete was taking place or not. At the beginning of the firestore delete I set this to true and then added a .then to the delete to set it back to false.
I then added a Streambuilder around the ListView on the screen to monitor the value of this and show a CircularProgressIndicator when true.
It now looks like this:
Thanks for your help.

UI doesn't fully update when receive bloc informations

I have this weird problem: I want to update a grid of items when I click on it. I use a BLoC pattern to manage the changement so the view just receive a list and have to display it. My problem is that the view doesn't fully update.
Before I go further in the explanation, here my code
body: BlocEventStateBuilder<ShopEvent, ShopState>(
bloc: bloc,
builder: (BuildContext context, ShopState state) {
staggeredTile.clear();
cards.clear();
staggeredTile.add(StaggeredTile.count(4, 0.1));
cards.add(Container());
if (state.products != null) {
state.products.forEach((item) {
staggeredTile.add(StaggeredTile.count(2, 2));
cards.add(
Card(
child: InkWell(
child: Column(
children: <Widget>[
Image.network(
item.picture,
height: 140,
),
Container(margin: EdgeInsets.only(top: 8.0)),
Text(item.title)
],
),
onTap: () {
bloc.emitEvent(ClickShopEvent(item.id));
},
),
),
);
});
}
return StaggeredGridView.count(
crossAxisCount: 4,
staggeredTiles: staggeredTile,
children: cards,
);
}),
So, I have two items. When I click on the first one, I'm suppose to have one item with a different name and picture. But when I click, I have one item as expected, but with the same text and image. When I print thoses values, it's correctly updated but the view doesn't show it.
Do you have any clues of my problem?
For a reason that I can't explain, when I replaced
staggeredTile.clear();
cards.clear();
by
staggeredTile = new List<StaggeredTile>();
cards = new List<Widget>();
It works fine.
If someone can explain me the reason, I'd be gratefull.