GridView.count() not updating when values changed in cron - flutter

What I'm trying to do
What I'm trying to do is retrieve data from an API call and pass the data in the response to a GridView.count() widget every minute because the data could change.
I did this using a FutureBuilder widget and the Cron functionality from the cron/cron.dart if that helps.
Here is the code:
FutureBuilder<dynamic>(
future: Api.getFoods(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<Widget> slots = [];
if (snapshot.data == null) {
return Text('');
}
var data = snapshot.data;
cron.schedule(new Schedule.parse('*/1 * * * *'), () async {
setState(() {
data = snapshot.data;
});
slots = [];
});
for (int i = 0; i < snapshot.data.length; i++) {
slots.add(new FoodSlot(
snapshot.data[i]['name'],
snapshot.data[i]['created_at'],
snapshot.data[i]['qty'].toString()
));
}
return new GridView.count(
crossAxisCount: 2,
scrollDirection: Axis.vertical,
children: slots
);
})
The FoodSlot widget creates a Card and displays the value passed in the arguments on the card.
What I tried
After debugging, I saw that the cron job works fine, but the GridView widgets just won't update.
I tried using a Text widget instead of the GridView and return the values returned by the API call and the widget is updated automatically every 1 minute as expected.
Why is GridView.count() acting like this and how can I fix this?
UPDATE
When the changes in the database are made, the GridView does update, but only when the application is restarted (using R not r).

Solved!
Turns out I had to add the cron job in the FoodSlot widget and update the widget itself periodically.
Ended up adding the following in the initState() function for FoodSlot:
#override
void initState() {
super.initState();
colorSettings = HelperFunctions.setStateColour(expiry);
cron.schedule(new Schedule.parse('*/1 * * * *'), () async {
setState(() {
Api.getFood(id).then((res) {
expiry = res['created_at'];
name = res['name'];
qty = res['qty'].toString();
colorSettings = HelperFunctions.setStateColour(expiry);
});
});
});
}
Where id is the id of the database entry referenced by the FoodSlot widget.
Hope this helps someone :)

Related

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.

streamBuilder not updating itsself after sorting or filtering - Flutter

I am using StreamBuilder and ListView to show data from FireStoreDatabase in UI. Usually, it updates itself when I make any change in the database (without refresh). but when I sort the data in the stream, it no longer updates. here's the code am using for StreamBuilder.
Stream<List<Attraction>> attractionsStream = Stream.value([]);
void initState() {
final database = Provider.of<Database>(context, listen: false);
setState(() {
//this.attractions = attractions.toList();
attractionsStream = database.attractionStream();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<List<Attraction>>(
stream: attractionsStream,
builder: (context, snapshot) {...}
);
}
_sortData() async {
final db = Provider.of<Database>(context, listen: false);
var attractions = await db.attractionStream().first;
attractions.sort((a, b) {...}
setState(() {
attractionsStream = Stream.value(attractions);
});
}
Could you please guide me on this?
Try adding a unique Key to each widget that your StreamBuilder builds.
It is also uncommon to completely reset the stream, as opposed to pushing new data through the stream already in use.

Efficient storage fetching for ListView.builder, GridView.builder

In order to fetch data from the API with the builder function of ListView.builder or GridView.builder you have to create a list of Widgets that is being filled when you scroll.
The actual reason for using the Builder function is, to deal with large Lists so that only widgets are being rendered/build when needed:
List<Widget> _mediaList = [];
int currentPage = 0;
int? lastPage;
#override
void initState() {
super.initState();
_fetchNewMedia();
}
_handleScrollEvent(ScrollNotification scroll) {
if (scroll.metrics.pixels / scroll.metrics.maxScrollExtent > 0.33) {
if (currentPage != lastPage) {
_fetchNewMedia();
}
}
}
_fetchNewMedia() async {
lastPage = currentPage;
setState(() {
_mediaList.add(
Text("Some Widget"),
);
currentPage++;
});
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scroll) {
_handleScrollEvent(scroll);
return false;
},
child: GridView.builder(
controller: widget.scrollCtr,
itemCount: _mediaList.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (BuildContext context, int index) {
return _mediaList[index];
}),
);
}
The problem that I see is, that if you have an endless list (like a Post feed), that the list would store every Data source and the list would eventually jam up the RAM.
I would imagine that you either have to clear the list after scrolling for a long time or you would need to only store String-IDs and load the data according to them.
Is that concern appropriate or does the builder also optimize the storage in that case?
You should store String-IDs and build the list items using FutureBuilder(). Because although ListView.builder() renders only those items which are visible on the device screen but in case of post feed the post data may consume so much storage which can cause efficiency problem.

How to reload a FutureBuilder in Flutter?

I have a FutureBuilder here :
FutureBuilder<List<Task>>(
future:
getTasks(),
builder: (BuildContext context, AsyncSnapshot<List<Task>> snapshot){
if(snapshot.hasError){
return const Text('Erreur');
}
if(snapshot.hasData){
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: taskList.map((e) => TaskContainer(
task: e
)).toList(),
);
}
return const Center(child: CircularProgressIndicator());
}
)
Which return a list of tasks as colored block container in a calendar as shown below :
The problem is whenever I navigate to another month in the calendar, the FutureBuilder is fetching the again fetching the data from the webservice, which duplicate the tasks every time I change months :
Here is the code of my functions to navigate between months :
_toPreviousMonth(){
setState(() {
datess.clear();
dayss.clear();
startMonth = new DateTime(startMonth.year, startMonth.month - 1, 1);
});
}
_toNextMonth(){
setState(() {
datess.clear();
dayss.clear();
startMonth = new DateTime(startMonth.year, startMonth.month + 1, 1);
});
}
Actually I don't mind the FutureBuilder fetching the data when I change month, because the final goal would be to display tasks from the selected day.
But what I would like is to have a way to suppress the containers build from the previous fetch as I navigate between months,
Thanks for helping :)
Your code is not showing how you are using snapshot in your FututrBuilder??
I suspect you are adding the tasks directly to a list everytime you re-build using setState( ).
Solution: reset your tasks list at the beginning of the buid( ) method
#override Widget build(BuildContext context) {
yourTaskList = [] //if an array or adjust if other type

Data not updating to another widget flutter

I am trying to show the price of items in the cart but the total value should be shown in TextField. I am saving data to SQLite and retrieving then show to a widget, but when I try to access total_price to another widget it's not updating, but When I press hot reload again the data shows but not first time when I am opening the page
return FutureBuilder<List<CartModel>>(
future: fetchCartFromDatabase(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.length > 0) {
cartCount = snapshot.data.length;
for(int i = 0;i<snapshot.data.length;i++){
var price = snapshot.data[i].product_price.split("₹");
total_price+=double.parse(price[1]);
}
} else if (snapshot.hasData && snapshot.data.length == 0) {
return new Text("No Data found");
}
else
{
return new Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(),
);
}
);
value initialized
int cartCount = 0;
double total_price=0.0;
The FutureBuilder updates only its children. To update the value of another widget you must use setState.
The best way would be putting FutureBuilder in an upper level or using some sort of state manager, like provider.
To use setState you need to initialize you fetch from an initState of a stetefullWidget (or to call it from a function). This way you will not need a FutureBuilder and must refactor your code:
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
double total_price = 0;
#override
void initState() {
super.initState();
fetchCartFromDatabase().then((value){
setState((){
for(int i = 0;i<value.length;i++){
var price = value[i].product_price.split("₹");
total_price+=double.parse(price[1]);
}
});
});
}
}
The addPostFrameCallback is not a good solution, since it updates the value only in the next frame. When the app grows it leads to lags.
To continue using the FutureBuilder, move your widget tree that needs to be updated to be inside of the FutureBuilder.