0

I'm relatively new to Flutter and am currently struggling with FutureBuilders. I've read Remi's answer on this thread, which basically states Flutter's philosophy that widget builds should be idempotent. I get the principle in theory but how does that work practically though?

Consider the following snippet:

return Scaffold(
  body: SafeArea(
    child: Stack(
      children: [
        Consumer<DataPresenter>(
            builder: (context, presenter, _) {
              return DefaultFutureBuilder<List<Data>>(
                    future: presenter.data(),
                    builder: (context, data) {
                      // bla bla 
                    }
                );
            }
        ),
        // More widgets
      ],
    ),
  ),
);

}

Where this is my DefaultFutureBuilder:

class DefaultFutureBuilder<T> extends StatelessWidget {
  final Future<T> future;
  final Widget Function(BuildContext, T data) builder;
  final Widget Function(BuildContext) errorBuilder;

  DefaultFutureBuilder({
    @required this.future,
    @required this.builder,
    this.errorBuilder,
  });

  @override
  Widget build(BuildContext context) {
    var errBuilder = errorBuilder ?? _buildDefaultErrorScreen;

    return FutureBuilder<T>(
      future: future,
      builder: (context, snapshot) {
        var indicator = Center(child: CircularProgressIndicator());

        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasData)
            return builder(context, snapshot.data);
          else if (snapshot.hasError)
            return errBuilder(context);
        }

        return indicator;
      },
    );
  }

  Widget _buildDefaultErrorScreen(BuildContext context) {
    // default
  }
}


Now I know that the call to presenter.data() (fetches some async data) is not kosher. But I do want to re-fetch the data when the presenter notifies the consumer while at the same time I do not want to fetch it when Flutter rebuilds my widget tree because of framework shenanigans.

My initial idea was to build the presenter so that it only fetches the data when it does not have any data at the moment. I could then set the data to null from other methods and notify the listeners to rebuild the affected widget subtrees.
Like that:

class DataPresenter extends ChangeNotifier {
  // fields, services, bla bla
  List<Data> _data; 

  Future<List<Data>> data() async {
    if (_data == null) {
      var response = await _service.fetchMyStuff(params: params);
      _data = response.data;
    }

    return _data;
  }
}

The problem with this solution is that any rebuilds that happen while I fetch the data for the first time will cause another request.

I'd be grateful if someone could point out which part of the framework / architecture I didn't get.

Wecherowski
  • 818
  • 11
  • 24
  • It appears you have a few typos in your `DataPresenter`, am I right? You're checking if `_datas` is null, otherwise you fetch, then you set `_pets` to the result of the fetch (instead of `_datas`). Then you return `_data`, and not `_pets` not `_datas`. – matehat Apr 18 '20 at 01:39
  • 1
    if you want to "re-fetch" the latest snapshot of data why dont you use `StreamBuilder` instead? – pskink Apr 18 '20 at 03:50
  • @matehat, you're correct. I fixed the typos – Wecherowski Apr 18 '20 at 08:29
  • @pskink, I'm mainly from an imperative background. Since my app is not massively reactive, I thought I'd not dig into Streams and reactive programming, probably lazy on my part :-) Can you outline how a minimal approach would work with StreamBuilders in a separate answer? (i.e. I don't want to rebuild my entire app with Bloc right now) – Wecherowski Apr 18 '20 at 08:32
  • 1
    in a nutshell: if you want one shot notification you use a `FutureBuilder` which has either "waiting" and "done" (with the final data) state - if you want multiple notifications you need a `StreamBuilder` which produces "waiting" and multiple "active" states with the up-to-date data – pskink Apr 18 '20 at 08:58
  • @pskink ok, makes sense that I wasn't able to use multiple futures in FutureBuilder if the API's not designed for that :-) So the idea would be to make my data() method return a stream of Data objects. And then my presenter could update that stream in response to user interaction in other components which in turn would rebuild the snippet shown in this question, right? – Wecherowski Apr 18 '20 at 09:05
  • 1
    yes, exactly, see [Creating streams in Dart](https://dart.dev/articles/libraries/creating-streams) for more info on dart's streams, also you can use `rx-dart` package that adds a lot of useful stuff to streams – pskink Apr 18 '20 at 09:27

3 Answers3

3

I think this might solve your problem I had this problem too very annoying anything you do and your app rebuilds. So they way I solved it was by memorizing my future. Now this may or may not work for you. If it treats the Consumer rebuild as a new future then you will be good because then the FutureBuilder will rebuild when the Consumer does which is what you want if I understand correctly. Here's what you do.

//Initialize at top of class
final AsyncMemoizer _memoizer = AsyncMemoizer(); 


 Future<void> _someFuture() {
    return this._memoizer.runOnce(() async {
     //Do your async work
    });
  }

Then _someFuture() would be what you pass to FutureBuilder. Here is a good article on this issue.

Flutter: My FutureBuilder Keeps Firing!

wcyankees424
  • 2,554
  • 2
  • 12
  • 24
1

If you want to rebuild a provider then you can use

context.refresh(providerThatYouWantToRebuild);

This method is useful for features like "pull to refresh" or "retry on error", to restart a specific provider at any time.

Documentation is available at: https://riverpod.dev/docs/concepts/combining_providers#faq https://pub.dev/documentation/riverpod/latest/all/ProviderContainer/refresh.html https://pub.dev/documentation/flutter_riverpod/latest/all/BuildContextX/refresh.html

Anees Hameed
  • 5,916
  • 1
  • 39
  • 43
0

I would suggest a solution, without a futureBuilder

You fetch data after your first Build of your Stateless Widget

WidgetsBinding.instance.addPostFrameCallback((_) => Provider.of<DataPresenter>(context, listen: false).fetchdata());

you have a difference in your _data List with value null or lenght==0

class DataPresenter extends ChangeNotifier {
  // fields, services, bla bla
  List<Data> _data; 
  // getter for the Consumer
  List<Data> get data => _data;


  Future<List<Data>> fetchdata() async {

     _data = await _service.fetchMyStuff(params: params);
     if (_data == null) 
       _data = new List(); //difference between null and length==0

     //rebuild Consumer
     notifyListeners();

  }
}

The Page without FutureBilder

class ProvPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    // calls after the widget ist build
    WidgetsBinding.instance.addPostFrameCallback((_) => Provider.of<TodoListModel>(context, listen: false).refreshTasks());

    return MultiProvider(
        providers: [
              ChangeNotifierProvider(create: (context) => DataPresenter()),
        ],
        child: Scaffold(
          body: SafeArea(
            child: Stack(
              children: [
                Consumer<DataPresenter>(
                    builder: (context, presenter, _) {

                       //get current Values vom the DataPresenter._data
                       List<Data> currData = presenter.data;

                       // firstime fetch ??
                       if(currData == null)
                         return CircularProgressIndicator();

                       // DataPresenter.getData is finished
                       return new RefreshIndicator(
                           onRefresh: () => presenter.fetchdata(),
                           child: new Text("length " + currData.length.toString())
                       );

                    },
                ),
                // More widgets
              ],
            ),
          ),
        ),
    );
}
}
FloW
  • 1,211
  • 6
  • 13
  • But doesn't WidgetBinding add a callback after every rebuild? So my http service is still called with every rebuild? That doesn't seem right ... – Wecherowski Apr 18 '20 at 08:39
  • PostFrameCallback() calls every time when the whole widget was building, but only the Consumer is rebuilding when a notifyListeners() comes – FloW Apr 18 '20 at 10:54
  • yeah but any widget above that can also rebuild. – Wecherowski Apr 18 '20 at 10:54
  • you can do it also with a stateful widget one time in the initstate – FloW Apr 18 '20 at 10:56