Pagination with Flutter, Ferry, Graphql? - flutter

I have graphql hooked up, and the collection in question has about 2000 documents. What I want to do is simply load in 25 at a time. If I perform the query to fetch the full list it takes 5-10 seconds, so what I need is for it to only ask for 25 at a time.
I have it working so it will load up X amount of documents and display them in a list. What I cannot figure out is how to get pagination working. Ive read the docs 100 times, but I cannot make sense of it. Here is the pagination doc for ferry https://ferrygraphql.com/docs/pagination
I am fairly new to flutter and dart so any help here would be appriecited. Thank you.
This is my code. Currently this just displays the first 25 documents and nothing else.
class DisplayPartnerOrganistions extends StatefulWidget {
#override
_DisplayPartnerOrganistionsState createState() =>
_DisplayPartnerOrganistionsState();
}
class _DisplayPartnerOrganistionsState
extends State<DisplayPartnerOrganistions> {
var offset = 0;
final client = GetIt.I<Client>();
late GFetchPartnerOrganisationsReq fetchPartnerOrganisationsReq;
#override
void initState() {
super.initState();
fetchPartnerOrganisationsReq = GFetchPartnerOrganisationsReq(
(b) => b
..vars.offset = offset
..vars.limit = 25,
);
}
Widget _buildList(partners) {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, item) {
return _buildRow(partners[item]);
});
}
Widget _buildRow(partnerOrganisation) {
return ListTile(
title: Text(partnerOrganisation.toString()),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Partner Organisations'),
),
body: Operation<GFetchPartnerOrganisationsData,
GFetchPartnerOrganisationsVars>(
client: client,
operationRequest: fetchPartnerOrganisationsReq,
builder: (context, response, error) {
if (response!.loading) {
return const Center(child: CircularProgressIndicator());
}
final partners = response.data?.partnerOrganizations;
return _buildList(partners);
}),
);
}
}
I been trying different things for about 12 hours but nothing is making any sense. Am I using the wrong widgets or something?

You will need to create a requestId so ferry considers the different request for the different pages the same one:
final fetchPartnerOrganisationsReq = GFetchPartnerOrganisationsReq((req) =>
req
..requestId = 'myRequestId'
..vars.offset = 0
..vars.limit = 25,
);
If you don't do this, ferry creates an id from your request type and the variables of the request, which will be different for the different pages (offset won't be the same).
Then when you need to load more data, you can rebuild the request with different variables (offset). Rebuilding it will allow you to keep the same requestId you specified in the first place.
final newRequest = fetchPartnerOrganisationsReq.rebuild((request) =>
// `requestId` is still `'myRequestId'`
request..vars.offset = fetchPartnerOrganisationsReq.vars.offset + 25 // <- You update the offset here
request..updateResult = (previous, result) {
// This tells ferry what to do when you get the result from the new query.
// Since it is using the same id as the initial one (with `offset = 0`), by default it will override the result.
// Here we can tell ferry how to keep both.
// `previous` is what you already received in the previous query (the previous page).
// `result` is the new page you get.
if (previous == null) return result; // If we didn't have a previous page yet, we just use what we got with the new query.
return previous!.rebuild((previous) =>
previous.partnerOrganizations.addAll(result!.partnerOrganizations), // We concatenate the lists of the previous pages and the new one.
);
},
);
When do you create a new request ?
It can be when you reach the end of a list. You can know that by using a ScrollController() with a listener. You can know you reached the end of the list with _scrollController.position.extentAfter == 0).

Related

retrieve more than expected products when load more data in flutter

I have this code:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class Scroool extends StatefulWidget {
const Scroool({Key? key}) : super(key: key);
#override
State<Scroool> createState() => _ScrooolState();
}
class _ScrooolState extends State<Scroool> {
List posts = [];
int limit = 20;
bool isLoadingMore = false;
ScrollController controller = ScrollController();
#override
void initState() {
fetchPosts();
controller.addListener(scrolListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade300,
body: ListView.builder(
controller: controller,
padding: EdgeInsets.all(20),
itemCount: isLoadingMore ? posts.length + 1 : posts.length,
itemBuilder: (context, index) {
if(index < posts.length) {
final post = posts[index];
return Card(
child: ListTile(
leading: CircleAvatar(child: Text("${index + 1}"),),
title: Text(post['title'].toString()),
subtitle: Text(post['description']),
),
);
} else {
return Center(child: CircularProgressIndicator(),);
}
},
),
);
}
fetchPosts() async {
final url = Uri.parse("https://dummyjson.com/products?limit=$limit");
final response = await http.get(url);
if(response.statusCode == 200) {
final json = jsonDecode(response.body)['products'] as List;
setState(() {
posts = posts + json;
isLoadingMore = false;
});
} else {
print("ERROR");
}
}
scrolListener() async{
//don' call the api if user scroll whil calling the api not finished yet
if(isLoadingMore) return;
if(controller.position.pixels ==
controller.position.maxScrollExtent) {
setState(() {
isLoadingMore = true;
});
limit+=5;
await fetchPosts();
setState(() {
isLoadingMore = false;
});
} else {
print("not called");
}
}
}
The problem is when I access the end of the scrolling, add more products, but not only 5 like I have said in the code above, but more? why? and how to fix it?
https://dummyjson.com/products?limit=$limit retrieves limit elements each time you call it. You increase that limit by 5 every time you hit the bottom. Then you do posts = posts + json Ie, you are adding those 25 elements you receive from the api to the 20 already existing (giving you 45 elements). Next time you are adding 30 new elements to the 45 already existing and so on and so forth.
Either
completely replace your posts with the retrieved data
only append the last 5 elements from your new list to the existing posts
correctly use the limit and skip parameters docs
I'd suggest to go with the third option. To achieve this there are several options. For instance creating a separate limit and skip variable.
class _ScrooolState extends State<Scroool> {
int limit = 20;
int skip = 0;
fetchPosts() async {
final url = Uri.parse("https://dummyjson.com/products?limit=$limit&skip=$skip");
...
}
scrolListener() async{
if(isLoadingMore) return;
if(controller.position.pixels == controller.position.maxScrollExtent) {
setState(() {
isLoadingMore = true;
});
//if the limit is still the initial 20 it will set the skip to 20
//later it will increase the skip by 5
skip += limit;
//for all subsequent requests set the limit to 5 to only retrieve
//5 more elements
limit = 5;
await fetchPosts();
...
}
}
That you are talking about posts but actually are retrieving products suggests, in your real code, you are using a different API. Of course chosing an option also depends on the capabilities of the API you actually plan to use. But I can only answer you based on what I see in your question. Thus, if your API supports pagination via skip and limit (or similar) use that. Otherwise, use one of the other options I mentioned.

What would be the most effective work around/ bypass for the Wherein 10 limit for flutter firebase? [duplicate]

This question already has answers here:
Is there a workaround for the Firebase Query "IN" Limit to 10?
(10 answers)
Firebase Firestore query an array of more than 10 elements
(5 answers)
Closed 19 days ago.
I am currently working on a screen in a project where the user can see a list of posts that he or she has subscribed, ordered by the time they were last updated (using a timestamp field in the post document). Currently this is done by retrieving an array of Post Document IDs from the user's profile document through a future builder, and then using that array for a wherein query in a streambuilder that works with a listview builder so they can do the lazy loading thing and also can update in real time. My problem is the wherein limit of 10 values in the array, and I'm not certain of the way I should go about working around it. This is my code:
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:testApp/myServices/postDisplay.dart';
class homeView extends StatefulWidget {
const homeView({Key? key}) : super(key: key);
#override
State<homeView> createState() => _homeViewState();
}
class _homeViewState extends State<homeView> {
final FirebaseAuth auth = FirebaseAuth.instance;
final db = FirebaseFirestore.instance;
late String currentUID;
bool isReady = false;
List subscriptions = ['1, 2, 3'];
Stream<QuerySnapshot<Object?>>? postStream;
getProfileData() async {
return await db.collection("Profiles").doc(currentUID).get().then((value) {
subscriptions = value.data()!["subscriptions"];
postStream = db
.collection("Posts")
.where(FieldPath.documentId, whereIn: subscriptions)
.snapshots();
isReady = true;
});
}
void initState() {
currentUID = auth.currentUser!.uid;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: getProfileData(),
builder: (context, snapshot) {
if (isReady) {
return StreamBuilder<QuerySnapshot>(
stream: postStream,
builder: ((context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return Center(
child: Container(
width: 300,
child: ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
return postDisplay(
postSnapshot: snapshot.data!.docs[index],
currentUID: currentUID,
);
},
),
),
);
}
}),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}),
);
}
}
I have thought of splitting the array into chunks/batches and doing queries that way (as seen in some of the other similar questions on here), or even flipping the database scheme upside-down by storing the IDs of the users into the post documents and using the array contains query as it is not limited. I am somewhat inclined against this, however, as I want the post documents to contain as little information as possible as they're the most often loaded element in the project- the way that the project is set up means that certain popular individual posts would most likely have more people attached to it as subscribers than the average user would have posts attached to it as entities he or she follows. No matter what the solution, the resulting list needs to be ordered (this is what kind of differentiates my problem from the other questions on the site I think) by time of last modification (using the field in the post document). I'm not sure if this requirement makes chunks/batching unfeasible. Please let me know if you have any suggestions on how to succeed. Thank you so much in advance for any help!

Create infinite scrolling effect using Firebase Realtime Database data in flutter

I am integrating a chat feature in my mobile application, and decided to use Firebase Realtime Database for the backend instad of Firestore as a cost reduction mechanism. I am running into a problem, however. There seems to be very sparse documentation on how to create infinite scrolling using data from Realtime Database instead of Firestore.
Below is the organization of my chat messages. This is the query I want to use:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
And this is the widget I want to return for each result:
MessageWidget(
message: message.text,
id: message.uid,
name: message.name,
lastSender: message.lastSender,
date: message.timeStamp,
profilePicture: message.profilePicture,
);
Here is the database structure
The query works, and I have already programmed the MessageWidget from the JSON response of the query. All I need is for the query to be called whenever it reaches the top of its scroll, and load more MessageWdigets. Also note, this is a chat app where users are scrolling up, to load older messages, to be added above the previous.
Thank you!
EDIT: here is the code I currently have:
Flexible(
child: StreamBuilder(
stream: FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.limitToLast(20)
.onValue,
builder:
(context, AsyncSnapshot<DatabaseEvent> snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
} else {
Map<dynamic, dynamic> map =
snapshot.data!.snapshot.value as dynamic;
List<dynamic> list = [];
list.clear();
list = map.values.toList();
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ListView.builder(
controller: _scrollController,
// shrinkWrap: true,
itemCount: list.length,
itemBuilder: (context, index) {
final json = list[index]
as Map<dynamic, dynamic>;
final message = Message.fromJson(json);
return MessageWidget(
message: message.text,
id: message.uid,
name: message.name,
lastSender: message.lastSender,
date: message.timeStamp,
profilePicture:
message.profilePicture,
);
}),
),
);
}
},
),
),
My initState
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
bool isTop = _scrollController.position.pixels == 0;
if (isTop) {
//add more messages
} else {
print('At the bottom');
}
}
});
}
Your code already loads all messages.
If you want to load a maximum number of messages, you'll want to put a limit on the number of messages you load. If you want to load only the newest messages, you'd use limitToLast to do so - as the newest messages are last when you order them by their timeStamp value.
So to load for example only the 10 latest messages, you'd use:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.limitToLast(10);
This gives you the limited number of messages to initially show in the app.
Now you need to load the 10 previous messages when the scrolling reaches the top of the screen. To do this, you need to know the timeStamp value and the key of the message that is at the top of the screen - so of the oldest message you're showing so far.
With those two values, you can then load the previous 10 with:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.endBefore(timeStampValueOfOldestSeenItem, keyOfOldestSeenItem)
.limitToLast(10);
The database here again orders the nodes on their timeStamp, it then finds the node that is at the top of the screen based on the values you give, and it then returns the 10 nodes right before that.
After several days of testing code, I came up with the following solution
The first step is to declare a ScrollController in your state class.
final ScrollController _scrollController = ScrollController();
You will also need to declare a List to store query results
List list = [];
Next, use the following function to get initial data
getStartData() async {
//replace this with your path
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('messages/${widget.placeID}');
starCountRef
.orderByChild("timeStamp")
//here, I limit my initial query to 6 results, change this to how many
//you want to load initially
.limitToLast(6)
.onChildAdded
.forEach((element) {
setState(() {
list.add(element.snapshot.value);
list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
});
});
}
Run this in initState
void initState() {
super.initState();
FirebaseDatabase.instance.setPersistenceEnabled(true);
getStartData();
}
Now to display the initial data that was generated when the page was loaded, build the results into a ListView
ListView.builder(
itemCount: list.length,
controller: _scrollController,
//here I use a premade widget, replace MessageWidget with
//what you want to load for each result
itemBuilder: (_, index) => MessageWidget(
message: list[index]["text"],
date: list[index]["timeStamp"],
id: list[index]["uid"],
profilePicture: list[index]["profilePicture"],
name: list[index]["name"],
lastSender: list[index]["lastSender"],
),
),
Note that your ListView must be constrained, meaning that you can scroll to the beginning or end of your ListView. Sometimes, the ListView won't have enough data to fill and be scrollable, so you must declare a height with a Container or bound it to its contents.
Now you have the code that fetches data when the page is loaded using getStartData() and initState. The data is stored in list, and a ListView.builder builds a MessageWidget for each item returned by getStartData. Now, you want to load more information when the user scrolls to the top.
getMoreData() async {
var moreSnapshots = await FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.endBefore(list[0]["timeStamp"])
.limitToLast(20)
.once();
var moreMap = moreSnapshots.snapshot.value as dynamic;
setState(() {
list.addAll(moreMap.values.toList());
list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
});
}
Then, make the function run when the ListView.builder is scrolled all the way to the top by adding this to the already existing initState.
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
bool isTop = _scrollController.position.pixels == 0;
if (isTop) {
getMoreData();
}
}
});
Hopefully this helps or gives you a pointer on where to go from here. Thanks to Frank van Puffelen for his help on which query to use based on the previous answer.

ValueListenableBuilder is not rebuilding the screen, when hotreloading, it is working

I'm trying to build a note app, all data and other things is working perfectly, cos the data is displaying to the screen when the code file is saving, its weird , first time facing this problem
in short, the valuelistanble is not listening when the data adding from app, but when just hot reloading the data is displaying
how can i fix this,
here is the code
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback((_) async {
final value = await NoteDB.instance.getAllNotes();
});
____________________________________________
____________________________________________
//code line for aligment
Expanded(
child: ValueListenableBuilder(
valueListenable: NoteDB.instance.noteListNotifier,
builder: (context, List<NoteModel> newNotes, _) {
return GridView.count(
childAspectRatio: 3 / 4,
crossAxisCount: 2,
mainAxisSpacing: 34,
crossAxisSpacing: 30,
padding: const EdgeInsets.all(20),
//generating list for all note
children: List.generate(
newNotes.length,
(index) {
//setting the notelist to a variable called [note]
final note = newNotes[index];
if (note.id == null) {
//if the note's id is null set to sizedbox
//the note id never be null
const SizedBox();
}
return NoteItem(
id: note.id!,
//the ?? is the statement (if null)
content: note.content ?? 'No Content',
title: note.title ?? 'No Title',
);
},
),
);
},
)),
here is the NoteDB.instance.getAllNotes(); function
#override
Future<List<NoteModel>> getAllNotes() async {
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
final noteResponse = GetAllNotes.fromJson(_result.data);
noteListNotifier.value.clear();
noteListNotifier.value.addAll(noteResponse.data.reversed);
noteListNotifier.notifyListeners();
return noteResponse.data;
} else {
noteListNotifier.value.clear();
return [];
}
}
and also there is a page to create note , and when create note button pressed there is only one function calling here is function
Future<void> saveNote() async {
final title = titleController.text;
final content = contentController.text;
final _newNote = NoteModel.create(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
content: content,
);
final newNote = await NoteDB().createNote(_newNote);
if (newNote != null) {
print('Data Added to the DataBase Succesfully!');
Navigator.of(scaffoldKey.currentContext!).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => HomePage()),
(Route<dynamic> route) => false);
} else {
print('Error caught while data adding to the DataBase');
}
}
everything work fine, but while add the data the UI isn't refreshing even tho notifier is active
and if you need full code please have a look at this github link : https://github.com/Mishalhaneef/Note-app
Since this ValueNotifier has a type of List<NoteModel>, the value will not change when you add new items to the list or delete from it or clear all. The value here is a reference to the list which does not change.
You have to assign a new value to it, like:
noteListNotifier.value = List<NoteModel>[<add your current items here>];
You can manipulate your current list with List.from, removeWhere, add etc., and then re-assign the complete list.
Besides you don't need to call notifyListeners in case of a ValueNotifier, the framework handles it, see here.
Another approach would be to use a custom ChangeNotifierProvider where you can call notifyListeners when the contents of your list are changed.
Some further suggestions:
In your homescreen.dart file, instead of NoteDB.instance.noteListNotifier.value[index] you can use newNotes[index].
In data.dart, within getAllNotes, you have to set a new value for noteListNotifier in order to get the changes propagated. Currently you are just modifying items in this list and that is not considered to be a change. Try this code:
#override
Future<List<NoteModel>> getAllNotes() async {
//patching all data from local server using the url from [Post Man]
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
//if the result data is not null the rest operation will be operate
//recived data's data decoding to json map
final _resultAsJsonMap = jsonDecode(_result.data);
//and that map converting to dart class and storing to another variable
final getNoteResponse = GetAllNotes.fromJson(_resultAsJsonMap);
noteListNotifier.value = getNoteResponse.data.reversed;
//and returning the class
return getNoteResponse.data;
} else {
noteListNotifier.value = <NoteModel>[];
return [];
}
}

Dart - processing RxCommand result before send it to RxLoader

I'm writing a Flutter app and I decided to use RxDart to pass my data and events along the managers, services and UI.
Basically I have a service which fetches data from a web service and returns it. Let's assume it returns a List of a model called ExploreEntity.
class ExploreMockService extends ExploreServiceStruct {
final String response = /** a sample json **/;
#override
Future<List<ExploreEntity>> loadExploreData(PaginationInput input) async {
await Future.delayed(new Duration(seconds: 2));
return List<ExploreEntity>.from(jsonDecode(response));
}
}
Now in my manager class I call the loadExploreData method inside a RxCommand.
class ExploreManagerImplementation extends ExploreManager {
#override
RxCommand<void, List<ExploreEntity>> loadExploreDataCommand;
ExploreManagerImplementation() {
loadExploreDataCommand = RxCommand.createAsync<PaginationInput, List<ExploreEntity>>((input) =>
sl //Forget about this part
.get<ExploreServiceStruct>() //and this part if you couldn't understand it
.loadExploreData(input));
}
}
And finally I get the result by a RxLoader and pass it to a GridView if data was fetched successfully.
class ExplorePageState extends State<ExplorePage>{
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Explore"),
),
body: Column(children: <Widget>[
Expanded(
child: RxLoader<List<ExploreEntity>>(
commandResults:
sl.get<ExploreManager>().loadExploreDataCommand.results,
dataBuilder: (context, data) => ExploreGridView(data),
placeHolderBuilder: (context) => Center(
child: Center(
child: CircularProgressIndicator(),
),
),
errorBuilder: (context, error) => Center(
child: Text("Error"),
)),
)
]));
}
}
It works like a charm but when I wanted to load the data of the next page from web service and append it to the list, I couldn't find a solution to store the content of previous pages and just append the new page's contents to them, since data is passed along the RxCommand and RxLoader automatically.
When loadExploreData sends the reponse to the manager, I need to firstly append the result to a list, and then send that list as the result to RxLoader. Any suggestions?
Hmm that's a good question if this can be done just using Rx. What I would do is keeping a list of the received items in the manager. So when triggering the command to get the next page the command would first add the new data to the list and then push the whole list to the UI.
I"m curious if there is another solution.
My described approach in a rough code sample
class ExploreManagerImplementation extends ExploreManager {
List<ExploreEntity>> receivedData = <ExploreEntity>[];
#override
RxCommand<void, List<ExploreEntity>> loadExploreDataCommand;
ExploreManagerImplementation() {
loadExploreDataCommand = RxCommand.createAsync<PaginationInput, List<ExploreEntity>>((input)
async {
var newData = await sl //Forget about this part
.get<ExploreServiceStruct>() //and this part if you couldn't understand it
.loadExploreData(input);
receivedData.addAll(newData);
return receivedData;
};
}
}