Pull to refresh
source link: https://www.flutterclutter.dev/flutter/tutorials/pull-to-refresh/2021/6142/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
The pull-to-refresh gesture is a popular UI mechanic that is used not only in Google’s Material Design, but also in Apple’s Human Interface Guidelines. No surprise this feature is also represented in the Flutter standard library. It’s called RefreshIndicator there.
But how do we use this handy UI element? And how does it play together with alternative state management solutions such as BLoC pattern?
How to use RefreshIndicator
RefreshIndicator
is a usual widget that is supposed to be wrapped around the widget which you want to give the ability to be reloaded on pull down. Whenever the child
‘s Scrollable
ist overscrolled, the circular progress indicator is shown on top. When the user releases its finger / cursor and the indicator has been dragged far enough to the bottom, it will call the provided callback (onRefresh
).
The widget’s constructor has two arguments. child
represents the widget that should be refreshed on pull down. onRefresh
is a callback function that is called when the refresh indicator has been dragged far enough to the bottom. This is the place where you typically trigger fetching new data.
Prerequisites
Before we jump right into the implementation, let’s build a sample widget we can test our implementation on.
Typically, there is a list of elements that is being refreshed when the indicator is shown. So let’s build a SampleListView
widget that resembles this.
import 'package:flutter/material.dart'; class SampleListView extends StatelessWidget { const SampleListView({Key? key, required this.entries}) : super(key: key); final List<int> entries; @override Widget build(BuildContext context) { return ListView( children: entries .map( (int e) => ListTile( leading: const Icon(Icons.android), title: Text('List element ${e + 1}'), ), ) .toList(), ); } }
The widget expects a list of int
. For every element in this list, it shows a ListTile with “List element xy”, xy being the index (+1) of the element.
The scenario
Because we are not dealing with actual dynamic data here, we define a recurring scenario once the user pulls down, the view idles for 2 seconds, resulting in the list being expanded by one element.
RefreshIndicator and StatefulWidget
import 'package:flutter/material.dart'; import 'package:flutter_app/sample_list_view.dart'; class RefreshWithStatefulWidget extends StatefulWidget { const RefreshWithStatefulWidget({Key? key}) : super(key: key); @override State<RefreshWithStatefulWidget> createState() => _RefreshWithStatefulWidgetState(); } class _RefreshWithStatefulWidgetState extends State<RefreshWithStatefulWidget> { List<int> entries = List<int>.generate(5, (int i) => i); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Refresh with StatefulWidget'), ), body: RefreshIndicator( onRefresh: () async { await Future.delayed(const Duration(seconds: 2)); setState(() { entries.add(entries.length); }); }, child: SampleListView( entries: entries, ), ), ); } }
The list (entries
) is initiated with 5 elements. After the user interaction with the RefreshIndicator
, we alter the state by adding one element to the entries
.
Alternative loading animation
So far, so good. But right now, the animation of the RefreshIndicator
is shown until everything’s ready. What if we did not want to show the default loading animation? If we had a branded loading indicator all across our app, we might wanted to show it during every occurrence of a loading state. What if we had our app-wide loading indicator in a dialog?
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_app/sample_list_view.dart'; class RefreshWithStatefulWidget extends StatefulWidget { const RefreshWithStatefulWidget({Key? key}) : super(key: key); @override State<RefreshWithStatefulWidget> createState() => _RefreshWithStatefulWidgetState(); } class _RefreshWithStatefulWidgetState extends State<RefreshWithStatefulWidget> { List<int> entries = List<int>.generate(5, (int i) => i); bool loading = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Refresh with StatefulWidget'), ), body: RefreshIndicator( onRefresh: () async { _indicateLoading(); Future.delayed(const Duration(seconds: 2)).then( (_) => _refresh(), ); }, child: loading ? const Center( child: CircularProgressIndicator(), ) : SampleListView( entries: entries, ), ), ); } void _refresh() { return setState( () { entries.add(entries.length); loading = false; }, ); } void _indicateLoading() { setState(() { loading = true; }); } }
We add a state property called loading
which indicates whether the data is being loaded. It is supposed to be set to true when the user pulls down and be set to false again when the asynchronous operation is finished.
Depending on the value of loading
we weither show a CircularProgressIndicator
or the SampleListView
.
The RequestIndicator
disappears as soon as the onRefresh()
method returns. That’s why we avoid using await
to wait for our Future
to be finished and use then
instead.
Using await instead of then()
What if we want to use await
? Actually, I like it much more than then()
if I have sequential operations anyways. Because with then()
, there is a steadily increasing nesting level. This decreases the readability of our code drastically.
How about letting the caller decide when to hide the RequestIndicator
?
import 'dart:async'; import 'package:flutter/material.dart'; class RefreshableWidget extends StatelessWidget { const RefreshableWidget({ Key? key, required this.child, required this.onRefresh, }) : super(key: key); final Widget child; final Function(Completer<void>) onRefresh; @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () { final Completer<void> completer = Completer<void>(); onRefresh(completer); return completer.future; }, child: child, ); } }
I created a wrapper around the RefreshIndicator
. The onRefresh()
function being its constructor argument, has a Completer<void>
type.
Now, what does that mean?
Completer is a way to produce Future objects. This is exactly what we want, because we want the caller to decide when the RefreshIndicator
disappears. And it disappears whenever the onRefresh()
function finishes. It’s an async
function that returns a Future
. We create a new function that returns the future
property of our newly created Completer
which is then passed to the onRefresh
function of our RefreshableWidget
.
Using our RefreshableWidget
, our list looks like this:
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Refresh with StatefulWidget'), ), body: RefreshableWidget( onRefresh: (Completer completer) async { completer.complete(); _indicateLoading(); await Future.delayed(const Duration(seconds: 2)); _refresh(); }, child: loading ? const Center( child: CircularProgressIndicator(), ) : SampleListView( entries: entries, ), ), ); }
In this case, we let the RefreshIndicator
disappear directly because we call completer.complete()
as the first statement inside our callback. Now we can safely use await
to wait for our time-intense operation.
How about using BLoC?
Now that we have abstracted the control over the disappearance of the, we can also easily use any other state management solution apart from StatefulWidget
without having to worry about await
statements.
The following example demonstrates a solution using the BLoC package.
part of 'refresh_cubit.dart'; class RefreshState extends Equatable { const RefreshState({this.entries = const []}); final List<int> entries; @override List<Object?> get props => [entries]; } class RefreshLoaded extends RefreshState { const RefreshLoaded({required List<int> entries}) : super(entries: entries); } class RefreshInitial extends RefreshState { const RefreshInitial() : super(entries: const <int>[0, 1, 2, 3, 4]); } class RefreshLoading extends RefreshState {}
First, we create a State
that is to be emitted from our Cubit
. It has three possible states: RefreshInitial
, RefreshLoading
and RefreshLoaded
. RefreshInitial
carries a list with 5 elements. RefreshLoading
is supposed to be emitted when the user pulls down. Finally, RefreshLoaded
should be emitted, when the time-intense operation is done and the data is fetched.
import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'refresh_state.dart'; class RefreshCubit extends Cubit<RefreshState> { RefreshCubit() : super(const RefreshInitial()); Future<void> refresh() async { emit(RefreshLoading()); List<int> newList = List<int>.from(state.entries); newList.add(newList.length); await Future.delayed(const Duration(seconds: 2)); emit(RefreshLoaded(entries: newList)); } }
In our Cubit
, we only provide a single method: refresh()
. It emits a loading state, adds an element to the list, and emits a loaded state.
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_app/sample_list_view.dart'; import 'package:flutter_app/with_refreshable_widget/refresh_cubit.dart'; import 'package:flutter_app/with_refreshable_widget/refreshable_widget.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class RefreshWithRefreshableWidget extends StatelessWidget { const RefreshWithRefreshableWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider<RefreshCubit>( create: (BuildContext context) => RefreshCubit(), child: BlocBuilder<RefreshCubit, RefreshState>( builder: (BuildContext context, RefreshState state) { return RefreshableWidget( onRefresh: (Completer<void> completer) async { completer.complete(); context.read<RefreshCubit>().refresh(); }, child: _buildScaffold(context, state), ); }, ), ); } Scaffold _buildScaffold(BuildContext context, RefreshState state) { return Scaffold( appBar: AppBar( title: const Text('Refresh with RefreshableWidget'), ), body: _buildBody(context, state), ); } Widget _buildBody(BuildContext context, RefreshState state) { if (state is RefreshLoading) { return const Center( child: CircularProgressIndicator(), ); } return SampleListView( entries: state.entries, ); } }
In the widget, we can now complete the future before calling the Cubit
‘s refresh method. If we wanted to give the control over the disappearance of the indicator to the cubit, we could expect a parameter in the refresh
method.
Conclusion
Flutter already provides a solution for handling the pull-to-refresh gesture which is very straightforward in its usage. However, when using await
or there is the desire to show an alternative loading animation, there are some some things to consider.
If you like what you’ve read, feel free to support me:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK