I. Overview
RefreshIndicator is Flutter's built-in control based on Material Design Language, which combines pull-down gestures, load indicators and refresh operations. Its playability is much worse than FutureBuilder's, but you have used other controls in Material Design Language, and the visual effect is also good.
Refresh Indicator is not the only way to refresh the list. ScrollController is also needed to monitor the movement offset of ListView.
Two important components
- RefreshIndicator
- Constructor
/** * Drop-down refresh component *const RefreshIndicator ({ Key key, @required this.child, this.displacement: 40.0, // trigger drop-down refresh distance @ required this.onRefresh, // drop-down callback method, method needs async and await keywords, no await, refresh icon disappears immediately, no async, refresh icon will not disappear this.color, // progress indicator foreground color defaults to system theme color this.backgroundColor, // Background Color this.notificationPredicate: defaultScrollNotificationPredicate, }) */
- Constructor
Attention:
-
-
- The child element of RefreshIndicator must be a scrollable control
- If you encounter non-conforming controls, wrap them in scrollable controls such as ListView, PageView, etc.
- The onRefresh callback function must be Future < Null > type
-
- ScrollController
- Constructor
ScrollController({ double initialScrollOffset = 0.0, //Initial rolling position this.keepScrollOffset = true,//Whether to save the scroll position ... })
-
Attributes and methods
- offset: Scrollable Widget's current scrolling position.
- Jump To (double offset), animateTo(double offset,...): These two methods are used to jump to a specified location. They differ in that the latter performs an animation while jumping, while the former does not.
-
AddListener (listener)
ScrollController is indirectly inherited from Listenable, and we can listen for rolling events based on ScrollController. Such as:
controller.addListener(()=>print(controller.offset))
-
Keep Scroll Offset (initial Scroll Offset)
PageStorage is a Widget used to store data related to pages (routing). It does not affect the UI appearance of the subtree. In fact, PageStorage is a functional Widget. It has a bucket. Widgets in the subtree can store their data or state by specifying different PageStorageKey.
At the end of each scroll, the Scrollable Widget stores the scroll location offset in the PageStorage and restores it when the Scrollable Widget is recreated. If ScrollController.keepScrollOffset is false, the scroll location will not be stored, Scrollable Widget will be recreated using ScrollController.initialScrollOffset; ScrollController.keepScrollOffset will be true, and Scrollable Widget will scroll to initialScrollOffset when it is first created, because The scroll position has not been stored yet. The scroll position is stored and restored in the next scroll, while the initial ScrollOffset is ignored.
-
Rolling monitor
Flutter Widget Tree Neutron Widget can communicate with parent (including ancestor) Widget by sending Notification. Parent Widgets can listen for notifications of concern through NotificationListener Widget s, which are similar to browser event bubbles in Web development. We use the term "bubbles" in Flutter. Scrollable Widget s send ScrollNotification-type notifications when scrolling, and ScrollBar is implemented by listening for scrolling notifications. There are two main differences between listening for scrolling events through Notification Listener and ScrollController:
- With Notification Listener, you can listen anywhere from Scrollable Widget to Widget tree root. ScrollController can only be associated with a specific Scrollable Widget.
- The information obtained after receiving the rolling event is different; Notification Listener carries some information about the current rolling position and ViewPort in the notification when receiving the rolling event, while ScrollController can only get the current rolling position.
NotificationListener
NotificationListener is a Widget. Template parameter T is the type of notification that you want to listen on. If omitted, all types of notifications will be listened on. If a specific type is specified, only that type of notification will be listened on. NotificationListener needs an onNotification callback function to implement the listening processing logic. The callback can return a Boolean value to indicate whether to prevent the event from bubbling upwards. If true, the bubbling terminates, the event stops propagating upwards, and if not, if the return value is false. Bubble continues.
- Constructor
-
-
ScrollController Control Principle
Let's introduce three other methods of ScrollController:
ScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition ); void attach(ScrollPosition position) ; void detach(ScrollPosition position) ;
When Scrollable Controller is associated with Scrollable Widget, Scrollable Widget first calls Scrollable Controller's createScrollPosition() method to create a ScrollPosition to store scrolling location information, and then Scrollable Widget calls attach() method to add the created Scrollable Position to Scrolller's posi. In the actions attribute, this step is called "registration location", and only animateTo() and jumpTo() can be invoked after registration. When Scrollable Widget is destroyed, the detach() method of ScrollController is called to remove its ScrollPosition object from the position attribute of ScrollController. This step is called "logout location". After logout, animateTo() and jumpTo() can no longer be called.
It should be noted that animateTo() and jumpTo() of ScrollController call animateTo() and jumpTo() of all ScrollPosition s internally to achieve that all Scrollable Widget s associated with the ScrollController scroll to the specified location.
-
Third, pull-down loading, pull-up refresh implementation
class Widget_RefreshIndicator_State extends State<Widget_RefreshIndicator_Page> { var list = []; int page = 0; bool isLoading = false;//Are you requesting new data? bool showMore = false;//Whether to display the prompt in the bottom load bool offState = false;//Whether to display the circular progress bar when entering the page ScrollController scrollController = ScrollController(); @override void initState() { super.initState(); scrollController.addListener(() { if (scrollController.position.pixels == scrollController.position.maxScrollExtent) { print('Slide to the bottom ${scrollController.position.pixels}'); setState(() { showMore = true; }); getMoreData(); } }); getListData(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text("RefreshIndicator"), ), body: Stack( children: <Widget>[ RefreshIndicator( child: ListView.builder( controller: scrollController, itemCount: list.length + 1,//List length+Tips for bottom loading itemBuilder: choiceItemWidget, ), onRefresh: _onRefresh, ), Offstage( offstage: offState, child: Center( child: CircularProgressIndicator(), ), ), ], ) ), ); } @override void dispose() { super.dispose(); //Manual stop sliding monitor scrollController.dispose(); } /** * Which subcomponent to load */ Widget choiceItemWidget(BuildContext context, int position) { if (position < list.length) { return HomeListItem(position, list[position], (position) { debugPrint("Click on the $position strip"); }); } else if (showMore) { return showMoreLoadingWidget(); }else{ return null; } } /** * Load more prompt components */ Widget showMoreLoadingWidget() { return Container( height: 50.0, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text('Loading...', style: TextStyle(fontSize: 16.0),), ], ), ); } /** * Simulate access to pages to get data */ void getListData() async { if (isLoading) { return; } setState(() { isLoading = true; }); await Future.delayed(Duration(seconds: 3), () { setState(() { isLoading = false; offState = true; list = List.generate(20, (i) { return ItemInfo("ListView A row of data $i"); }); }); }); } /** * Simulate loading more data to the bottom */ void getMoreData() async { if (isLoading) { return; } setState(() { isLoading = true; page++; }); print('Up-pull refresh start,page = $page'); await Future.delayed(Duration(seconds: 3), () { setState(() { isLoading = false; showMore = false; list.addAll(List.generate(3, (i) { return ItemInfo("Pull-up Add ListView A row of data $i"); })); print('Up-pull refresh end,page = $page'); }); }); } /** * Simulated drop-down refresh */ Future < void > _onRefresh() async { if (isLoading) { return; } setState(() { isLoading = true; page = 0; }); print('Drop-down refresh start,page = $page'); await Future.delayed(Duration(seconds: 3), () { setState(() { isLoading = false; List tempList = List.generate(3, (i) { return ItemInfo("Drop-down Add ListView A row of data $i"); }); tempList.addAll(list); list = tempList; print('Drop-down refresh end,page = $page'); }); }); } }