Flutter: Correct approach to get value from Future
Asked Answered
R

2

17

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() returns Future, instead of actual data.

I beleive there is very small logical mistake, but unable to find it myself.

Respondent answered 21/9, 2019 at 8:18 Comment(6)
and what is your question? i dont see any FutureBuilder in your code... so are you using it or StatefulWidget for exmple?Antitype
I'm using StatefulWidget and I've shared complete code, please lemme know if you need more info.Respondent
no, in the code you posted now you are using FutureBuilder, so my question: is it a right code? or maybe you are using StatefulWidget?Antitype
I saw you know how to use FutureBuilder in the code, and you also know to make _getImagesPath an async function. Is there any errors when you combine these two?Neoplasty
@Antitype As mentioned in my first comment, I'm using StatefulWidget. The Widget build method is inside StatefulWidget class.Respondent
@Neoplasty there isn't any runtime error, and no error indication in IDE. However the value of imagesPath is null at run time.Respondent
A
16

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.

Antiphonal answered 21/9, 2019 at 9:44 Comment(12)
thanks 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 inside Image.asset and it worked fine. How is that possible?Respondent
Make sure there are no double slashes in the path, it can be an issueAntiphonal
I'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.Respondent
I 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 requires async/await.Respondent
Error lines from console <asynchronous suspension> Image provider: AssetImage(bundle: null, name: "/data/user/0/com.alena.data_handler/app_flutter/images/1.jpg")Respondent
Instead 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 problemAntiphonal
Are 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))Antiphonal
I was setting image asset like this leading: Image.asset(image),, and I've also tried leading: new Image(image: new AssetImage(image)) but no luck. Replacing that with Image.file(File(path)) resolves the issue. It works now. Thank you so much for your help!Respondent
Another 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.Respondent
api.dartlang.org/stable/2.5.0/dart-io/FileSystemEntity/… is your friend.Antiphonal
awesome, thank you again for your help. God Bless you!Respondent
I love this approach - it helped me really reduce my code quite significantly. Great thinking @ChennaReddy !!Aby
F
6

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!

Fornication answered 21/9, 2019 at 10:15 Comment(2)
thanks for your answer, it doesn't work at first load. But it works when we go back, and come again, very strange issue.Respondent
@Atlas Stick to the OP's structure. If somebody asks how to fix a car, don't suggest to buy a new one.Overspill

© 2022 - 2024 — McMap. All rights reserved.