Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to show RefreshIndicator intially while waiting data from backend API?

Tags:

flutter

So, I have this "Notifications" screen that displays notifications for the user. When navigating to this screen, it's going to be blank, since the notifications are being loaded live from a backend API.

Here's some code to illustrate the problem:

class _MyItemsPageState extends State<MyItemsPage> {
  final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      new GlobalKey<RefreshIndicatorState>();
  List<MyItem> _items = [];

  @override
  void initState() {
    super.initState();

    // Nothing is displaying on screen initially, since the items are loaded from API on startup.
    // Preferably in this state, the refresh indicator would be shown while the items load.
    // It's not currently possible in this place, since it seems that the Widget hasn't been built yet.

    _refreshIndicatorKey.currentState.show(); // currentState null at this time, so the app crashes.
    _loadItems();
  }

  // (unrelated code removed)

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new RefreshIndicator(
        key: _refreshIndicatorKey,
        onRefresh: _loadItems,
        child: new ListView(
          padding: new EdgeInsets.symmetric(vertical: 8.0),
          children: _buildItemWidgets(),
        ),
      ),
    );
  }
}

The problem is that the _refreshIndicator.currentState is null when the initState() function is called, since the Widget hasn't been built yet.

What is the proper place of calling show() on the RefreshIndicator in this case?

like image 518
Iiro Krankka Avatar asked May 17 '17 17:05

Iiro Krankka


4 Answers

I think the best option is to schedule the _refreshIndicatorKey.currentState.show() using the https://docs.flutter.io/flutter/scheduler/SchedulerBinding/addPostFrameCallback.html that way when the widget finishes the build is safe to make a call the show() method, and not depend on a fixed amount of time in the Future.

Here the example:

void initState() {
    super.initState();
    SchedulerBinding.instance.addPostFrameCallback((_){  _refreshIndicatorKey.currentState?.show(); } );
  }

But, definitely, I do think that the widget itself can have an extra param to allow do that out the box.

like image 162
Alks Avatar answered Nov 09 '22 02:11

Alks


As it turns out, putting the _refreshIndicator.currentState.show() inside my _loadItems() function did the trick. So something like:

Future _loadItems() async { 
    _refreshIndicatorKey.currentState?.show(); 
    var items = await getItems();

    setState(() { 
        _items = items; 
    });
}

Then I called the _loadItems() function like usual in the initState() method, but removed the _refreshIndicatorKey.currentState.show() line from there.

I think this only works because of some possible race condition due to the async nature of the loadItems function, and feel like there should be some other solution related to the lifecycle of the widget state.

like image 45
Iiro Krankka Avatar answered Nov 09 '22 03:11

Iiro Krankka


Solution suggested by Iiro Krankka didn't work for me, but He is right about race condition. So I tried to add Future.delayed on initState, and it did the trick. This seems ugly, but you know, it just works.

@override
void initState() {
  super.initState();

  Future.delayed(Duration(milliseconds: 200)).then((_) {
    _refreshIndicatorKey.currentState?.show();
  });
}

Update

Basically .show() will trigger onRefresh of RefreshIndicator. So, actually by calling .show() inside _loadItems will trigger _loadItems twice. We don't want that. So I move .show() into initState and now we good, it just called once.

like image 45
axumnemonic Avatar answered Nov 09 '22 04:11

axumnemonic


I've written a package (declarative_refresh_indicator) that provides a declarative API for RefreshIndicator, similar to the Switch and Checkbox widgets.

Unlike other solutions, it can also allow the indicator to be shown initially without executing the refresh callback, allowing data to be requested in other places.

Here's an example that loads upon widget initialisation:

class _MyWidgetState extends State<MyWidget> {
  var _loading = false;
  
  void _load() async {
    setState(() => _loading = true);
    await _getData();
    if (mounted) setState(() => _loading = false);
  }

  @override
  void initState() {
    super.initState();
    _load();
  }
  
  @override
  Widget build(BuildContext context) {
    return DeclarativeRefreshIndicator(
      refreshing: _loading,
      onRefresh: _load,
      child: /* a scrollable widget */,
    );
  }
}
like image 2
hacker1024 Avatar answered Nov 09 '22 02:11

hacker1024