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

Viewed 8740

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?

5 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.

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.

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 */,
    );
  }
}

This doesn't work with CupertinoActivityIndicator.

I ended up adding this to my CustomScrollView list of slivers just above the SliverList, and included an animated container that checks if loading from my provider. Works nice, smooth, no issues.

SliverToBoxAdapter(
              child: AnimatedContainer(
                width: checkingForNewData || _images.isEmpty ? 70 : 0,
                height: checkingForNewData || _images.isEmpty ? 70 : 0,
                duration: Duration(milliseconds: 350),
                curve: Curves.ease,
                child: Visibility(
                  visible: checkingForNewData || _images.isEmpty,
                  child: SizedBox(
                    height: 150,
                    width: 50,
                    child: CupertinoActivityIndicator(
                      radius: 12,
                    ),
                  ),
                ),
              ),
            ),
Related