I'm trying to return a FutureBuilder from a SearchDelegate but result shown are not correct.
query seems to be correct and from network I can see all http calls done correctly, so the problem is related to the list data update itself.
In buildSuggestions (of SearchDelegate) a StatefulWidget called 'ResultList' is returned. This widget has for state:
previousQuery - last search term before rerendering
list - list of data returned from a Future
from - first element to show
pageSize - number of elements returned
I need those variables in order to implement infinite scroll so in ResultList build method first of all I check if widget.query has changed from last rendering
if (previousQuery != widget.query) {
setState(() {
from = 0;
list.clear();
previousQuery = widget.query;
});
}
I'm using a ScrollController, so in initState of ResultList when user reach the bottom of the screen i just update "from":
setState(() {
from += pageSize;
});
In FutureBuilder builder method, if snapshot has new data, I append it to list. I should update list in setState but I can't do this inside a builder.
builder: (context, snapshot) {
List<int> ids = [];
List<int> newids = [];
if (snapshot.hasData) {
ids = list.map((item) => item.id as int).toSet().toList();
newids = (snapshot.data.results as List)
.map((item) => item.id as int)
.toSet()
.toList()
.where((id) => !ids.contains(id))
.toList();
if (newids.length != 0) {
setState(() {//can't do this here
list = [
...list,
...(snapshot.data.results as List)
.where((element) => newids.contains(element.id as int))
];
});
}
}
Any hint? Thanks in advance.
when we Use Future Builder Inside of StateFull Widget we Should know On Every SetState
Future Builder call Future Function. and rebuild it self,
so your problem is going to solve if you Remove Future Builder ,
so please change your code to some thing like below...
#override
Widget build(BuildContext context) {
return newids.Empty && ids.Empty ? CircularProgressIndicator() : MyWidget();
}
and Call Your Future in Init State (and When You Want get new Items(next page))
Related
I have a TODO List function (Alarmas), but I feel I'm not taking advantage of Firebase's Realtime features enough.
The Widget displays the list very well, however when someone puts a new task from another cell phone, I am not being able to show it automatically, but I must call the build again by clicking on the "TODO button" in the BottomNavigationBar.
Is there a way that the new tasks are automatically displayed without doing anything?
I'm using BLOC Pattern and Provider to get Data through Streams...
#override
Widget build(BuildContext context) {
alarmaBloc.cargarAlarmas();
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
return ListView(
children: [
for (var itemPendiente in tareasList)
_crearItem(context, alarmaBloc, itemPendiente),
//more widgets
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
#puf published a solution in How to display a Firebase list in REAL TIME? using setState, but I don't know how to implement it because I can't use setState inside my BLoC pattern page.
UPDATE
My BLoC Pattern looks like this...
class AlarmaBloc {
final _alarmaController = new BehaviorSubject<List<AlarmaModel>>();
final _alarmaProvider = new AlarmaProvider();
Stream <List<AlarmaModel>> get alarmasStream => _alarmaController.stream;
Future<List<AlarmaModel>> cargarAlarmas() async {
final alarmas = await _alarmaProvider.cargarAlarmas();
_alarmaController.sink.add(alarmas);
return alarmas;
}
//---
dispose() {
_alarmaController?.close();
}
And my PROVIDER looks like this...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
print('Provider - Nuevo onChild Alarma ${element.snapshot.value['fecha']} - ${element.snapshot.value['nombreRefEstanque']} - ${element.snapshot.value['pesoPromedio']}}');
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp); // element.snapshot.value.
});
await resp.once().then((snapshot) {
print("Las Alarmas se cargaron totalmente - ${alarmaList.length}");
});
return alarmaList;
How can I display a List from Firebase in "true" Real Time using BLoC Pattern?
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.
I am trying to call the output of a Future Provider of type Future> into a List View builder. I think I am very near as I am able to render the final List View itself, however, prior that, an error appears and is quickly replaced by the List View after completing the Future. I believe there may be something wrong with my implementation there.
Here's what I've got so far (these are derivatives of my actual code, there's too many going on there that aren't necessary, I tried to simplify it):
class TempProvider extends ChangeNotifier(){
List<Widget> _list = <Widget>[];
List<Widget get list => _list;
Future<List<Widget>> getList() async{
List _result = await db....
_result.forEach((_item){
addToList(_item);
});
}
addToList(Widget widget){
_list.add(widget);
notifyListeners();
}
}
class Parent extends StatelessWidget{
#override
Widget build(BuildContext context) {
return FutureProvider(
create: (context) => TempProvider().getList(),
child: Child(),
);
}
}
class Child extends StatelessWidget{
#override
Widget build(BuildContext context) {
var futureProvider = Provider.of<List<Widget>>(context);
return FutureBuilder(
initialData: <Widget>[],
future: TempProvider().getList(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.none &&
snapshot.hasData == true) {
return ListView.builder(
itemCount: futureProvider.length,
itemBuilder: (BuildContext context, int index) {
return futureProvider[index];
},
);
} else {
return Text('ALAWS');
}
},
);
}
}
So basically, the output of my Future will be a list of widgets that will populate a List View that I am trying to build. Though I am able to render the list view in the end, the error below appears in between:
The getter 'length' was called on null.
Receiver: null
Tried calling: length
The relevant error-causing widget was
FutureBuilder<List<Widget>>
Hoping someone can help with this one or at least give a better example.
Thank you so much!
Not sure but, this is happening because your widget might be building twice and at first futureProvider is null and in second time it has some value.
Workaround:
Replace this:
futureProvider.length
With this:
futureProvider?.length ?? 0
What the above code does?
futureProvider?.length: if futureProvider is null don't access it's length.
Now the value returned will be null.
?? 0: if the value returned is null then return 0;
You need to think over following things and edit your code.
At first place futureProvider should not be null.
Why are you not using snaphot.data when you are using FutureBuilder.
So I have been doing my research and found the article below which shows a definite implementation based on what I need:
Flutter Provider Examples - Codetober
Credits to Douglas Tober for the article. Thanks again to Kalpesh for the quick help!
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 :)
I am trying to populate my ListView with the result from an API. The API call must take place after the values have been retrieved from Shared Preference. However on execution my function for API call runs an infinite loop and the UI doesn't render. I tracked this behaviour through debug statements.
The circular indicator that should be shown when Future builder is building UI is also not showing.
How can I resolve this?
My code:
class _MyHomePageState extends State<MyHomePage>{
#override MyHomePage get widget => super.widget;
String userID = "";
String authID = "";
//Retrieving values from Shared Preferences
Future<List<String>> loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> l= new List<String>();
if(prefs.getString("ID") == null){
l.add("null");
}
else{
l.add(prefs.getString("ID"));
}
if(prefs.getString("authID") == null){
l.add("null");
}
else{
l.add(prefs.getString("authID"));
}
return l;
}
//Setting values retrieved from Shared Pref
setData() async{
await loadData().then((value) {
setState(() {
userID = value[0];
print('the user ID is' + userID);
authID = value[1];
print('the authID is' + authID);
});
// getAllTasks(userID, authID);
});
print("Set data execution completed ");
}
//FUNCTION to use values from Shared Pref and make API Call
Future<List<Task>> getAllTasks() async{
await setData();
//Waiting for Set Data to complete
print('Ive have retrived the values ' + userID + authID );
List<Task> taskList;
await getTasks(userID, authID, "for_me").then((value){
final json = value;
if(json!="Error"){
Tasks tasks = tasksFromJson(json); //of Class Tasks
taskList = tasks.tasks; //getting the list of tasks from class
}
});
if(taskList != null) return taskList;
else {
print('Tasklist was null ');
throw new Exception('Failed to load data ');
}
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context){
_signedOut(){
widget.onSignedOut();
}
//To CREATE LIST VIEW
Widget createTasksListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty ? Ink(....
) : CircularProgressIndicator();
},
);
}
//MY COLUMN VIEW
Column cardsView = Column(
children: <Widget>[
....
Expanded(
child: FutureBuilder(
future: getAllTasks(),
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
],
);
return Scaffold(
body: cardsView,
);
}
}
Instead of being called once... my setData function is being called repeatedly.. How can I resolve this..please help
You're creating Future object on every rebuild of the widget. And since you're calling setState inside your setData method, it triggers a rebuild recursively.
To solve this problem you have to keep a reference to the Future object. And use that reference for the FutureBuilder then it can understand that it is the previously used one.
E.g:
Future<List<Task>> _tasks;
#override
void initState() {
_tasks = getAllTasks();
super.initState();
}
And in your widget tree use it like that:
Expanded(
child: FutureBuilder(
future: _tasks,
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
The FutureBuilder widget that Flutter provides us to create widgets based on the state of some future, keeps re-firing that future every time a rebuild happens!
Every time we call setState, the FutureBuilder goes through its whole life-cycle again!
One option is Memoization:
Memoization is, in simple terms, caching the return value of a function, and reusing it when that function is called again.
Memoization is mostly used in functional languages, where functions are deterministic (they always return the same output for the same inputs), but we can use simple memoization for our problem here, to make sure the FutureBuilder always receives the same future instance.
To do that, we will use Dart’s AsyncMemoizer.
This memoizer does exactly what we want! It takes an asynchronous function, calls it the first time it is called, and caches its result. For all subsequent calls to the function, the memoizer returns the same previously calculated future.
Thus, to solve our problem, we start by creating an instance of AsyncMemoizer in our widget:
final AsyncMemoizer _memoizer = AsyncMemoizer();
Note: you shouldn’t instantiate the memoizer inside a StatelessWidget, because Flutter disposes of StatelessWidgets at every rebuild, which basically beats the purpose. You should instantiate it either in a StatefulWidget, or somewhere where it can persist.
Afterwards, we will modify our _fetchData function to use that memoizer:
_fetchData() {
return this._memoizer.runOnce(() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
});
}
Note: you must wrap inside runOnce() only the body, not the funciton call
Special thanks to AbdulRahman AlHamali.
You need to save the Future in the State because doing getAllTasks() is triggering the call on every build callback.
In the initState:
this.getAllTasksFuture = getAllTasks();
Then you would use this Future property in the FutureBuilder.