Animated nested lists fetching real time data from Firebase with StreamBuilder - Flutter - flutter

I'm trying to create a program where teachers can create classes and other users can request an invitation for those classes. This is the document structure in Firebase:
classes_data (collection)
{class_id} (document)
invitations (collection)
{uid} (document)
users_data (collection)
{uid} (document)
classes_hosting (collection)
{class_id} (document)
I want a screen with a list that displays all of the classes the user is hosting, and inside each class I want a nested list to display all the pending invitations for that class. However, I want the list of invitations to be animated so that the list changes in real-time with an animation every time someone requests an invitation for that class. People can request invitations and cancel requests, so the list has to be able to have the elements added and removed.
The code that I wrote works fine with nested ListViews. However, as soon as I change the inner ListView to an AnimatedList, it stops working properly.
import 'package:classes_app/models/classHosting.dart';
import 'package:classes_app/models/pending_invitations.dart';
import 'package:classes_app/models/user.dart';
import 'package:classes_app/services/database.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Hosting5 extends StatefulWidget {
const Hosting5({Key? key}) : super(key: key);
#override
State<Hosting5> createState() => _Hosting5State();
}
class _Hosting5State extends State<Hosting5> with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
// Stream with all classes that user is hosting and will happen in the future (after today):
var stream1 = DatabaseService(uid: hiphenUserSnapshot!.uid!).futureClassesHosting;
return SingleChildScrollView(
child: StreamBuilder<List<ClassHosting>>(
stream: stream1,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Text("Loading");
}
if (snapshot.data!.isNotEmpty) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 15.0),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
// Stream with all pending invitations for that class:
var stream2 = DatabaseService(uid: hiphenUserSnapshot!.uid!).getPendingInvitationRequests(snapshot.data![index].classId!);
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Text(
"Class: ${snapshot.data![index].title!}",
style: const TextStyle(
fontSize: 16,
color: Color(0xff101010),
fontWeight: FontWeight.w500,
height: 1.0,
),
),
StreamBuilder<List<PendingInvitation>>(
stream: stream2,
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text("Loading...");
if (snapshot.data!.isNotEmpty) {
return AnimatedList( // WORKS FINE WHEN THIS IS A LISTVIEW.BUILDER
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
initialItemCount: snapshot.data!.length,
itemBuilder: (context, index, animation) {
return Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Pending invitation name: ${snapshot.data![index].name}",
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
],
),
],
);
},
);
} else {
return const SizedBox();
}
},
),
],
),
);
}
);
} else {
return const SizedBox();
}
},
),
);
}
}
I get a RangeError (index) most of the time when the list changes.
I know that I still have to provide an animation, but I'm trying to get the rest to work first. I believe this error happens because the AnimatedList has to know which elements to remove and which ones to insert through a listKey. I tried to use stream.listen((){}) and then compare the old list with the new list to remove/add the necessary items like explained in this question. The issue is that I can't figure out how to do this with each event. Basically, since the AnimatedList is nested inside a ListView, I don't think the approach explained in the question work for this case, or at least not without some modifications that I can't figure out.
I would really appreciate if someone could help me make this work.

Related

isempty function is printing the text even if the firestore has some data

new to flutter and working on this error.
this is the code.
PROBLEM- I want to to show the text(your cart is empty!) when the firestore is empty. The text is displaying when the firestore document is empty but the problem is when firestore has some data to display, the text(your cart is empty!) will display and after a moment(0.5 sec) the data will display.
I don't want to display the text (your cart is empty) even for a milliseconds when the firestore has data. I appreciate if you guys can light the bulb for me.
that happened because you didn't get response yet for the database so to make sure that you received respond you will use snapshot.hasData
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('User-Cart-Item')
.doc(FirebaseAuth.instance.currentUser!.email)
.collection("items")
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
if (snapshot.data == null || snapshot.data!.docs.isEmpty) {
return const Center(
child: Text(
'YOUR CART IS EMPTY!',
style: TextStyle(
color: Colors.purple,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
);
} else {
return ListView.builder(
itemCount:
snapshot.data == null ? 0 : snapshot.data!.docs.length,
itemBuilder: (_, index) {
DocumentSnapshot documentSnapshot =
snapshot.data!.docs[index];
return Card(
color: const Color.fromARGB(255, 255, 248, 250),
margin: const EdgeInsets.all(20),
child: Container(
height: 120,
padding: const EdgeInsets.all(0),
child: Row(
children: [
Expanded(
flex: 6,
child: Container(decoration: const BoxDecoration()),
),
],
),
),
);
},
);
}
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
I hope this work for you
With FutureBuilder, the app is listening to changes to the database. What you are noticing is the latency between a change being made, and your app receiving it. This can never be absolute 0 because a reading device only sees a change after the change has been made. You can decrease this number with optic internet (fast internet connection) and by moving the database closer to where you are. There are multiple regions that you can choose from in order to get you as close as possible.

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

Go to the end of the list as soon as you create it, Flutter

I read many great answers about a ScrollController in the ListView. Then when a button is clicked we do scrollController.jumpTo(scrollController.position.maxScrollExtent);
My question is, can we go to the bottom of a List, but at the creation of it ?
return ListView.builder(
itemCount: data.length,
shrinkWrap: true,
// CAN WE DO SOMETHING HERE FOR EXAMPLE ?
physics: const ScrollPhysics(),
itemBuilder: (context, index)
{
return Item(data[index]);
}
);
(For me, it's a chat so the reverse option doesn't look good)
If, as you said, you are using this list for a chat log, then the reverse options does work for you. You just have to also invert your chat log data:
data.reversed.toList()
There is not direct option on a ListView to go to the end of the list.
For a more complete example from one of my applications:
class MessageList extends StatelessWidget {
const MessageList(this.displayMessages);
final List<Map> displayMessages;
#override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
reverse: true,
children: displayMessages.map((message) {
return ChatMessage(message);
}).toList(),
),
),
);
}
}
displayMessages = List<Map>.from(clientMessages.data).reversed.toList();

use only one page with DefaultTabController of 3 TabBars

I am using DefaultTabController for 3 TabBars. But i need to use only one page to be shown fetching the data from database all three TabBars are the same, But only the fetched data is different according to the index of its tap. How could i do so without create 3 copies of the same object WidgetOfTape?
the code is:
DefaultTabController(
length: 3,
child: Scaffold(
body: const TabBarView(
children: [
WidgetOfTape(),
WidgetOfTape(),
WidgetOfTape(),
],
),
WidgetOfTape code is:
class WidgetOfTape extends HookWidget {
const WidgetOfTape ();
#override
Widget build(BuildContext context) {
return BlocSelector<ScheduledPossibleDayCubit, ScheduledPossibleDayState,
int>(selector: (state) {
return state.indexOfTap;
}, builder: (context, state) {
return BlocProvider<DisplayNoteInsidePagesCubit>(
lazy: false,
create: (context) => getIt<DisplayNoteInsidePagesCubit>()
..countDoneNoteOutOfAllNotes()
..retrieveData(indexOfTap: state),
child: Column(children: [
Expanded(child: BlocBuilder<DisplayNoteInsidePagesCubit,
DisplayNoteInsidePagesState>(
builder: (context, state) {
return ListView.builder(
shrinkWrap: true,
itemCount: state.notes.length,
itemBuilder: (BuildContext context, int index) {
return Row(
children: [
Expanded(
child: Text(
state.notes[index]['content'],
textAlign: TextAlign.justify,
style: Theme.of(context).textTheme.bodyText2,
),
),
],
);
});
},
))
]));
});
}
}
Yes, you can do that, by sending a new parameter to your WidgetOfTape( ). The new variable you send as a parameter is defined by you. It could be based on the fetched data for example:
DefaultTabController(
length: 3,
child: Scaffold(
body: const TabBarView(
children: [
WidgetOfTape(newParameter1), //*** This line changed ***
WidgetOfTape(newParameter2), //*** This line changed ***
WidgetOfTape(newParameter3), //*** This line changed ***
],
),
Dont forget to add that parameter to your actual class WidgetOfTape( ).
If you dont know how to pass parameters, check here:
In Flutter, how do I pass data into a Stateless Widget?

How to save data of FutureProvider to avoid rebuild

I am using FutureProvider to fetch data(places) from Laravel API to show that data in flutter. Now the problem is it is rebuilding again and again that even server started throwing error of "too many attempts". I am using ListView.builder to show all places fetch from API but when I scroll to next places and then scroll back to previous it rebuilds. Mean FutureProvider is rebuilding again and again. Here is code attached. I also tried to add addAutomaticKeepAlives: true, in ListView to save the state
Container(
height: 100,
child: ListView.builder(
addAutomaticKeepAlives: true,
scrollDirection: Axis.horizontal,
itemCount: places.length,
itemBuilder: (_, index) {
return FutureProvider(
initialData: null,
create: (context) =>
getFireIceCount(index, places),
child: Consumer<List<dynamic>>(
builder: (context, fireIceCount,
child) {
return (fireIceCount != null)
? (fireIceCount.isNotEmpty)
? PlacesImageViewer()
: Center(
child:
CircularProgressIndicator(
color:
Colors.cyan,
),
)
: Center(
child:
CircularProgressIndicator(
color: Colors.red,
),
);
},
),
);
},
),
),
You would need a stateful widget, because the State object associated with it is maintained between rebuilds. The first advice would be to extract the widget you are building out of the listview itself
class SampleItem extends StatefulWidget {
const SampleItem({Key? key}) : super(key: key);
#override
_SampleItemState createState() => _SampleItemState();
}
class _SampleItemState extends State<SampleItem> {
late final Future<List<dynamic>> items=getFireIceCount(index, places);
#override
Widget build(BuildContext context) {
return BuildYOurItemHere();
}
}
But the most important thing would be actually making less requests to the backend, fetch a range of items instead of one by one, and try to implement pagination both in your app and the server. This single handedly will decrease the times you hit the server, increase the performance as you are making less requests.