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?
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.
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.
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 */,
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With