8

I am using FutureBuilder to show the data loaded from server. I want to show the loading state only once when the app starts, that is why I am calling the API from initState. The data I get from server may change and to reflect the change in UI, I am using refreshIndicator. The problem is that I could not come up with a solution to update the state.

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      GlobalKey<RefreshIndicatorState>();
  Future<List<Photo>> _photosServer;

  @override
  void initState() {
    super.initState();
    _photosServer = ApiRest.getPhotos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: RefreshIndicator(
        key: _refreshIndicatorKey,
        onRefresh: () {
          _refreshIndicatorKey.currentState.show();
          await getPhotosFromServer();
          ...
        },
        child: FutureBuilder(
          future: _photosServer,
          builder: (BuildContext context, snapshot) {
            if (snapshot.data == null) {
              return Center(
                child: Text('Loading...'),
              );
            }
            return ListView.builder(
              itemCount: snapshot.data.length,
              itemBuilder: (BuildContext context, index) => ListTile(
                title: Text(snapshot.data[index].title),
              ),
            );
          },
        ),
      ),
    );
  }
}

In the onRefresh function, I am using the following code to show the RefreshIndicator while getting data from server.

onRefresh: () {
          _refreshIndicatorKey.currentState.show();
          await getPhotosFromServer();

           ...

        }

What else should I do to handle the issue?

Ehsan Askari
  • 843
  • 7
  • 19

4 Answers4

13

You can have a separate List<Photo> variable which can be updated by the FutureBuilder or the RefreshIndicator, and do something like this:

class _MyHomePageState extends State<MyHomePage> {
  GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      GlobalKey<RefreshIndicatorState>();
  List<Photo> _photosList;
  Future<void> _initPhotosData;

  @override
  void initState() {
    super.initState();
    _initPhotosData = _initPhotos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FutureBuilder(
        future: _initPhotosData,
        builder: (BuildContext context, snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.none:
            case ConnectionState.waiting:
            case ConnectionState.active:
              {
                return Center(
                  child: Text('Loading...'),
                );
              }
            case ConnectionState.done:
              {
                return RefreshIndicator(
                    key: _refreshIndicatorKey,
                    onRefresh: _refreshPhotos,
                    child: ListView.builder(
                      itemCount: _photosList.length,
                      itemBuilder: (BuildContext context, index) => ListTile(
                        title: Text(_photosList[index].title),
                      ),
                    ));
              }
          }
        },
      ),
    );
  }

  Future<void> _initPhotos() async {
    final photos = await ApiRest.getPhotos();
    _photosList = photos;
  }

  Future<void> _refreshPhotos() async {
    final photos = await ApiRest.getPhotos();
    setState(() {
      _photosList = photos;
    });
  }
}
Alfred Jingle
  • 1,236
  • 9
  • 15
4
onRefresh: () {
   return Future(() { setState(() {}); });
},

In your RefreshIndicator will refresh child FutureBuilder

Danilyer
  • 59
  • 2
2

In order to refresh data and show the refresh indicator, simply wait for the results and afterwards update the future:

onRefresh: () async {
  final results = await getPhotosFromServer();
  setState(() {
    _photosServer = Future.value( results );
  });
},
moodstubos
  • 31
  • 1
  • 3
  • This will cause the FutureBuilder to unnecessarily rebuild as well. – Alfred Jingle Jan 12 '21 at 11:55
  • that's the way flutter works. why "unnecessarily" ? the data has been changed, so a rebuild IS necessary. – moodstubos Jan 13 '21 at 12:03
  • Sorry, this was a bit unclear. The question specifies "I want to show the loading state only once when the app starts". With your method, the FutureBuiler's build function will be unnecessarily re-run every time the user swipes down to refresh, causing the initial loading state to be shown (and the list to disappear) on every swipe down to refresh. – Alfred Jingle Jan 18 '21 at 15:10
  • Thanks for this. I figured there had to be a simple way. Not sure what Alfred is referring to. A loading widget is completely outside of the scope of redrawing a list unless you've written a loading widget into your list view. – toxaq Feb 10 '23 at 07:47
  • that is what I'm looking for thank you @moodstubos – Abdullah Bahattab Aug 02 '23 at 14:37
0

To use it without a separated variable with sync method i used this solution:

onRefresh: () {
    setState(() {
    _myFuture= getFromApi();
    });

    return Future.wait([
    _myFuture
    ]);
},
Kirus
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 29 '23 at 02:42