Flutter error : The argument type 'List<Future<Widget>>' can't be assigned to the parameter type 'List<Widget>' - flutter

I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}

I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}

You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?

Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.

Related

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 [];
}
}

what changes do i need to make to this code so that it does not show real time changes from the database..but only loads new data when refreshed.?

I need the onRefresh function here to refresh the brewList when i pull. I tried a few things but its not working and only refreshes after hot reload.
so when i pull down to refresh it should reflect any changes that were made in the firestore.
class BrewList extends StatefulWidget {
#override
_BrewListState createState() => _BrewListState();
}
class _BrewListState extends State<BrewList> {
#override
Widget build(BuildContext context) {
final brews = Provider.of<List<Brew>>(context) ?? [];
return RefreshIndicator(
onRefresh: refresh,
ListView.builder(
itemCount: brews.length,
itemBuilder: (context, index) {
return BrewTile(brew: brews[index]);
},
),
);
}
}
// get brews future
Future<List<Brew>> get brews async {
QuerySnapshot snapshot = await brewCollection.getDocuments();
return _brewListFromSnapshot(snapshot);
}
List<Brew> _brewListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc){
//print(doc.data);
return Brew(
name: doc.data['name'] ?? '',
strength: doc.data['strength'] ?? 0,
sugars: doc.data['sugars'] ?? '0'
);
}).toList();
}
try changing your stream to this and see if this helps
// get brews future
Future<List<Brew>> get brews async {
QuerySnapshot snapshot = await brewCollection.getDocuments();
return _brewListFromSnapshot(snapshot);
}
and then just call your provider when you want to update. My guess is you are also using a stream somewhere too doesn't seem necessary if you are not actively updating content.
okay I took a look at the github for this project and there are probably other changes you need to make first of now that your not using that stream it doesn't make sense to use a StreamProvider change that to a FutureProvider setup should be very similar and actually i know it was my orgional suggestion to change the provider to listen false but after looking at your project try changing back. since you do want to make periodic updates
Future refreshList() async {
setState(() {
brews = Provider.of<List<Brew>>(context) ;
});
}

Infinite loop on using FutureBuilder with API call

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.

How to reference Firestore documentID in a Flutter FutureBuilder

I have working code that gets a collection ('songs') from Firestore using a Future and a QuerySnapshot. I have that in a small function getSongs(). While I'm inside that function I have access to the documents' IDs ... so if I call say:
print(songsQuery.documents[1].documentID);
I get -LSvpZxM2pUIYjjp0qby
But later in my code I use a FutureBuilder where I call getSongs() for the future: and then build out a ListView with tiles of song info (Artist, Title, etc) from the snapshot in the builder:.
While I'm now in this widget I can't seem to figure out how to reference my .documentID anymore. I can get to all the .data elements for each document...but not the actual documentID.
Is there something very obvious that I'm missing?
Thanks for any help.
ER
I have scoured the internet trying to resolve with no luck. It seems like many people take the list of documents, load them into an array, add the doc.id, push it all into an array of items. Then use items. I would like to just use the snapshots as rendered back from Firestore and reference the doc.id directly if possible.
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
class AllSongs extends StatelessWidget {
Future getSongs() async {
var firestore = Firestore.instance;
QuerySnapshot songsQuery = await firestore.collection('songs').getDocuments();
print(songsQuery.documents[1].documentID);
//Here I can get to documentID...
return songsQuery.documents;
}
#override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder(
future: getSongs(),
builder: (_, songSnapshots){
print('How do I reference the DocumentID in here?');
print(songSnapshots.data.length);
print(songSnapshots.data[0].data['title']);
//print(songSnapshots.data[0].documentID);
//print(songSnapshots.data[0].ref);
//print(songSnapshots.data[0].data[ DOCUMENTID?? ]);
if(songSnapshots.connectionState == ConnectionState.waiting){
return Center(
child: Text('Loading...'),
);
} else {
return ListView.builder(
itemCount: songSnapshots.data.length ,
itemBuilder: (_, index){
return ListTile(
title: Text(songSnapshots.data[index].data['title']),
subtitle: Text(songSnapshots.data[index].data['artist']),
);
});
}
},
)
);
}
}
You're trying to access the value the value of the future before it resolves.
Try adding this line:
if (!songSnapshots.hasData) {
// Future hasn't resolved
return something;
}
// Future has resolved, you can access your data (including documentId)
Your future resolves to a list of DocumentSnapshot so just wait for the future to resolve and you should have access to all your data. Alternatively, you can try to access this inside your else statement where the state of the connection is not waiting, but in this case you are considering any non-waiting states as successful so I'd recommend using the hasData property of the AsyncSnapshot class instead.

How to inform FutureBuilder that database was updated?

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.