Flutter: Correct approach to get value from Future
Solution 1
I see that you have to build your widget from the output of two futures. You can either use two FutureBuilders
or have a helper method to combine them into one simplified code unit.
Also, never compute/invoke async function from build
function. It has to be initialized before (either in constructor or initState
method), otherwise the widget might end up repainting itself forever.
Coming to the solution: to simplify code, it is better to combine both future outputs into a single class as in the example below:
Data required for build
method:
class DataRequiredForBuild {
String imagesPath;
List items;
DataRequiredForBuild({
this.imagesPath,
this.items,
});
}
Function to fetch all required data:
Future<DataRequiredForBuild> _fetchAllData() async {
return DataRequiredForBuild(
imagesPath: await getImagesPath(),
items: await databaseHelperGetList(),
);
}
Now putting everything together in Widget:
Future<DataRequiredForBuild> _dataRequiredForBuild;
@override
void initState() {
super.initState();
// this should not be done in build method.
_dataRequiredForBuild = _fetchAllData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
//removed
body: FutureBuilder<DataRequiredForBuild>(
future: _dataRequiredForBuild,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.items.length,
itemBuilder: (_, int position) {
final item = snapshot.data.items[position];
final image = "${snapshot.data.imagesPath}/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.asset(image),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
},
),
);
}
Hope it helps.
Solution 2
Moving this piece of code inside FutureBuilder
should resolve the issue.
getImagesPath().then((path){
imagesPath = path;
print(imagesPath); //prints correct path
});
So your final code should look like this:
@override
Widget build(BuildContext context) {
return Scaffold(
//removed
body: FutureBuilder<List>(
future: databaseHelper.getList(),
initialData: List(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, int position) {
getImagesPath().then((path){
imagesPath = path;
});
final item = snapshot.data[position];
final image = "$imagesPath/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.file(File(image)),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
}));
}
Hope it helps!
Alena
Updated on July 09, 2022Comments
-
Alena almost 2 years
I have a function which returns images directory path, it performs some additional check like if directory exists or not, then it behaves accordingly.
Here is my code:
Future<String> getImagesPath() async { final Directory appDir = await getApplicationDocumentsDirectory(); final String appDirPath = appDir.path; final String imgPath = appDirPath + '/data/images'; final imgDir = new Directory(imgPath); bool dirExists = await imgDir.exists(); if (!dirExists) { await new Directory(imgPath).create(recursive: true); } return imgPath; }
This piece of code works as expected, but I'm having issue in getting value from
Future
.Case Scenario: I have data stored in local database and trying to display it, inside listview. I'm using
FutureBuilder
, as explained in this answer. Each data row has an image connected with it (connected means, the image name is stored in db).Inside
Widget build
method, I have this code:@override Widget build(BuildContext context) { getImagesPath().then((path){ imagesPath = path; print(imagesPath); //prints correct path }); print(imagesPath); //prints null return Scaffold( //removed body: FutureBuilder<List>( future: databaseHelper.getList(), initialData: List(), builder: (context, snapshot) { return snapshot.hasData ? ListView.builder( itemCount: snapshot.data.length, itemBuilder: (_, int position) { final item = snapshot.data[position]; final image = "$imagesPath/${item.row[0]}.jpg"; return Card( child: ListTile( leading: Image.asset(image), title: Text(item.row[1]), subtitle: Text(item.row[2]), trailing: Icon(Icons.launch), )); }) : Center( child: CircularProgressIndicator(), ); }));
}
Shifting
return Scaffold(.....)
inside.then
doesn't work. Because widget build returns nothing.The other option I found is
async/await
but at the end, same problem, code available below:_getImagesPath() async { return await imgPath(); }
Calling
_getImagesPath()
returnsFuture
, instead of actual data.I beleive there is very small logical mistake, but unable to find it myself.
-
Alena over 4 yearsthanks for your answer, the path issue is resolved by using your solution. But for some reasons, the assets are still not showing. There's an error in the console
unable to load asset: complete_path_of_asset
, the path is 100% correct and asset also exists. For testing, I copied one path from debug console and input insideImage.asset
and it worked fine. How is that possible? -
Alena over 4 yearsthanks for your answer, it doesn't work at first load. But it works when we go back, and come again, very strange issue.
-
Chenna Reddy over 4 yearsMake sure there are no double slashes in the path, it can be an issue
-
Alena over 4 yearsI've double checked, and there isn't any issue; including double slashes. The path is correct, because I've copied same path from console and used inside
Image.asset
and it worked fine. -
Alena over 4 yearsI noticed one thing weird, if I run app once and navigate to that screen. After that, If I click on
Restart
button from IDE, it re-opens app and goes to login screen. When I login and navigate there, the images are working fine. Nothing changed in code, during restart and I tried it several time, it works. Maybe there is something wrong with paint or it requiresasync/await
. -
Alena over 4 yearsError lines from console
<asynchronous suspension>
Image provider: AssetImage(bundle: null, name: "/data/user/0/com.alena.data_handler/app_flutter/images/1.jpg")
-
Chenna Reddy over 4 yearsInstead of displaying image, try replacing it with text with image path. If it display path properly it means it is issue with image only. Just to narrow down the problem
-
Chenna Reddy over 4 yearsAre you using flutter images or just images on file system. If first make sure assets are properly set, If second one try
Image.file(File(path))
-
Alena over 4 yearsI was setting image asset like this
leading: Image.asset(image),
, and I've also triedleading: new Image(image: new AssetImage(image))
but no luck. Replacing that withImage.file(File(path))
resolves the issue. It works now. Thank you so much for your help! -
Alena over 4 yearsAnother question, how can I check if asset exists before displaying it? Because if the asset it not available for some reason then it gonna break the app, and I wanted to display icon if the image is not available.
-
Chenna Reddy over 4 yearsapi.dartlang.org/stable/2.5.0/dart-io/FileSystemEntity/… is your friend.
-
Alena over 4 yearsawesome, thank you again for your help. God Bless you!
-
SilSur about 4 yearsI love this approach - it helped me really reduce my code quite significantly. Great thinking @ChennaReddy !!
-
Jhourlad Estrella almost 4 years@Atlas Stick to the OP's structure. If somebody asks how to fix a car, don't suggest to buy a new one.