FutureBuilder shows the loading indicator althought the result is cached

Issue

I’m facing an issue trying to use FutureBuilder with Provider and cache.
The computation is done one time and then its cached. The code looks like this:

FutureBuilder(
     future: model.calculateStats(chartType: getChartType()),
     builder: (context, snapshot) {
         if(snapshot.connectionState != ConnectionState.done) {
           return Center(
              child: CircularProgressIndicator()
           );
         }

      return buildChart(model);
   },
)

That piece of code is inside a Consumer with a ViewModel, which has the method calculateStats, which is the following:

Future calculateStats({ChartType chartType = ChartType.Monthly}) async {
   return await MemoryCache.instance.getOrCreateAsync("stats:${chartType.index}", () async {
      var statsMaker = StatsMaker(chartType: chartType);
      
      this._currentStats = await statsMaker.generate(
        startDate: _getStartDateForChartType(chartType),
        endDate: DateTime.now()
      );
    }, allowNull: true);
  }

Here you can see a video of what is happening: https://i.imgur.com/SWQ7N7P.mp4

The helper class MemoryCache checks if the provided key is in a map, and if that is true, it returns the value without doing any computation, which should return immediately, if it not found, the future is awaited and the result stored. But here, although the result is cached, the FutureBuilder shows the loading indicator (which is orange in the video). What am I doing wrong?

Solution

Their might be 2 reasons FutureBuilder keep loading. One is because the calculateStats keep firing, that cause the snapshot‘s status to be refreshed repeatedly. Second is your getOrCreateAsync might return an already completed Future, that the FutureBuilder has no way to synchronously determine that a Future has already completed.

There are a more convenient way to cached and load the UI one-time, which is using StreamBuilder since you only need to call the async method once in the initState or didChangeDependencies, hence the UI don’t need to be reload all the time.

You should use the snapshot.hasData as well to check if the Future value is completed and not-null.
In the UI:

@override
initState() {
  super.initState();
  model.calculateStats(chartType: getChartType());
}

// ... other lines

return StreamBuilder(
     stream: model.statsStream,
     builder: (context, snapshot) {
         if(!snapshot.hasData) {
           return Center(
              child: CircularProgressIndicator()
           );
         }

      return buildChart(snapshot.data);
   },
)

In your async function:

StreamController _controller = StreamController();

Stream get statStream => _controller.stream;

Future calculateStats({ChartType chartType = ChartType.Monthly}) async {
   final stats = await MemoryCache.instance.getOrCreateAsync( ... );
   // add your stats to Stream here (stats is the value you want to send to UI)
   _controller.add(stats);

Make sure that your getOrCreateAsync return a non-null value so that the Stream is updated correctly.

Answered By – Bach

Answer Checked By – Mary Flores (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *