Flutter: Correct approach to get value from Future

24,682

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!

Share:
24,682
Alena
Author by

Alena

Updated on July 09, 2022

Comments

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

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

  • Alena
    Alena over 4 years
    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?
  • Alena
    Alena over 4 years
    thanks 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
    Chenna Reddy over 4 years
    Make sure there are no double slashes in the path, it can be an issue
  • Alena
    Alena over 4 years
    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.
  • Alena
    Alena over 4 years
    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.
  • Alena
    Alena over 4 years
    Error lines from console <asynchronous suspension> Image provider: AssetImage(bundle: null, name: "/data/user/0/com.alena.data_handler/app_flutter/images/1.jp‌​g")
  • Chenna Reddy
    Chenna Reddy over 4 years
    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 problem
  • Chenna Reddy
    Chenna Reddy over 4 years
    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))
  • Alena
    Alena over 4 years
    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!
  • Alena
    Alena over 4 years
    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.
  • Chenna Reddy
    Chenna Reddy over 4 years
  • Alena
    Alena over 4 years
    awesome, thank you again for your help. God Bless you!
  • SilSur
    SilSur about 4 years
    I love this approach - it helped me really reduce my code quite significantly. Great thinking @ChennaReddy !!
  • Jhourlad Estrella
    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.