转载注明出处:https://blog.csdn.net/skysukai
1、why flutter?
我们在进行Android开发的时候,比如布局文件,会创建一个xml来存放布局。写熟悉了觉得没什么,可是,用xml来存放布局文件是十年前的技术了。在十年过后,再用xml来写布局文件运行时在由系统负责渲染看起来有些过时。
关于为什么是flutter,网上有很多讨论,在我看来最重要的应该是大前端的思想。作为一个Android开发者,我们也应该去学习这种先进的思想。
2、背景
项目中有很多ListView、GridView的场景,通常来说,从服务器获取数据都会分页获取。而flutter官方并没有提供一个loadmore控件。这就需要开发者自己实现。先贴出一张效果图:
网上有一些关于ListView加载更多的实现,无外乎都是判断是否滚动到底,滚动到底之后再加载更多。但是在我们的项目中,不仅有ListView还有GridView、StaggeredGrid(瀑布流)。作为一个懒程序员,我更愿意用一套代码解决这三个控件的加载更多 ,而不愿意分别为他们都写一套代码。
3、实现
站在巨人的肩膀上才能看得更远,这篇blog给了我莫大的启示,感谢作者。我的加载更多控件姑且叫做LoadMoreIndicator
吧。
3.1 总体思路
3.1.1 状态定义
总体来说,我们还是需要判断是否滚动到底。如果滚动到底且还有数据,则加载更多;否则,无更多数据。所以,我们的LoadMoreIndicator
至少应该包含IDLE、NOMORE这两个状态,除此之外,应该还有FAIL、LOADING两个状态,分别对应加载失败、正在加载。
3.1.2 监听滚动事件
作为加载更多的实现,如何判断滚动到底?flutter提供了ScrollController来监听滚动类控件,而我最开始也是使用ScrollController来做的,不过后面还是换成了Notification,其中遇到过一个坑,后边再详细说明。有关flutter的notification机制网上有很多介绍,总的来说就是一个flutter的事件分发机制。
3.1.3 如何统一封装
上面提到过,项目里面用到的滚动控件包括ListView、GridView、StaggeredGrid,那这三个不同的控件该如何封装到一起呢?单论这个问题似乎有很多解。再仔细分析项目需求,除了那三个滚动控件之外,可能还需要Appbar用于定义title,也还需要pinnedHeader。能把三个滚动控件统一起来,且还支持Appbar、pinnedHeader的控件只有CustomScrollView了。CustomScrollView包含多个滚动模型,能够处理许多个滚动控件带来的滑动冲突。
那么,LoadMoreIndicator的主体也很清晰了——通过CustomScrollView封装不同的滚动控件,并且处理各种业务场景。
3.2 主体框架
给出一小段代码,说明LoadMoreIndicator
的主体:
class LoadMoreIndicator<T extends Widget, K extends Widget>extends StatefulWidget {/// the Sliver headerfinal K header;/// the Sliver bodyfinal T child;/// callback to loading morefinal LoadMoreFunction onLoadMore;/// footer delegatefinal LoadMoreDelegate delegate;/// whether to load when emptyfinal bool whenEmptyLoad;///define emptyview or use default emptyviewfinal Widget emptyView;const LoadMoreIndicator({Key key,@required this.child,@required this.onLoadMore,this.header,this.delegate,this.whenEmptyLoad = true,this.controller,this.emptyView}) : super(key: key);@override_LoadMoreIndicatorState createState() => _LoadMoreIndicatorState();……
}
class _LoadMoreIndicatorState extends State<LoadMoreIndicator> {……/// original widget need to be wrapped by CustomScrollViewfinal List<Widget> _components = [];@overrideWidget build(BuildContext context) {/// build headerif (childHeader != null) {_components.add(SliverToBoxAdapter(child: childHeader,));}/// add body_components.add(childBody);/// build footer_components.add(SliverToBoxAdapter(child: _buildFooter(),));return _rebuildConcrete();}/// build actual Sliver BodyWidget _rebuildConcrete() {return NotificationListener<ScrollNotification>(onNotification: _onScrollToBottom,child: CustomScrollView(slivers: _components,),);}bool _onScrollToBottom(ScrollNotification scrollInfo) {/// if is loading returnif (_status == LoadMoreStatus.LOADING) {return true;}/// scroll to bottomif (scrollInfo.metrics.extentAfter == 0.0 &&scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent * 0.8) {if (loadMoreDelegate is DefaultLoadMoreDelegate) {/// if scroll to bottom and there has more data then loadif (_status != LoadMoreStatus.NOMORE && _status != LoadMoreStatus.FAIL) {loadData();}}}return false;}……}
以上这一小段代码就是LoadMoreIndicator
最核心代码了,非常简单。只需要把需要封装的控件传递过来,添加header、footer即可。有一个问题是,这样封装的话,滚动控件必须是sliver的实现,如:SliverGrid、SliverList、SliverStaggeredGrid,目前没有想到其他更好的解决办法。loadData()就是加载更多的实现,一般是连接到服务器获取数据。
3.3 构造footer
在LoadMoreIndicator
中,封装完滚动控件之后,最重要的工作就是构造footer了。选中了LoadMoreIndicator
代码的主体是Customscrollview之后,其实构造footer也很简单了。SliverToBoxAdapter
就是flutter提供的用于封装的其他Widget的控件,只需要把构造的footer用SliverToBoxAdapter再包装一层即可大功告成。给出代码片段:
Widget _buildFooter() {return NotificationListener<_RetryNotify>(child: NotificationListener<_AutoLoadNotify>(child: DefaultLoadMoreView(status: _status,delegate: loadMoreDelegate,),onNotification: _onAutoLoad,),onNotification: _onRetry,);}
DefaultLoadMoreView用于设置默认的加载更多动画,如果用户没有设置,则使用这个加载效果;否则使用定义过的加载效果。
/// if don't define loadmoreview use default
class DefaultLoadMoreView extends StatefulWidget {final LoadMoreStatus status;final LoadMoreDelegate delegate;const DefaultLoadMoreView({Key key,this.status = LoadMoreStatus.IDLE,@required this.delegate,}) : super(key: key);@overrideDefaultLoadMoreViewState createState() => DefaultLoadMoreViewState();
}class DefaultLoadMoreViewState extends State<DefaultLoadMoreView> {LoadMoreDelegate get delegate => widget.delegate;@overrideWidget build(BuildContext context) {notify();return GestureDetector(behavior: HitTestBehavior.translucent,onTap: () {if (widget.status == LoadMoreStatus.FAIL ||widget.status == LoadMoreStatus.IDLE) {/// tap to load_RetryNotify().dispatch(context);}},child: Container(alignment: Alignment.center,child: delegate.buildChild(context,widget.status,),),);}……
}
加载动画的实现在DefaultLoadMoreDelegate
中,通过代理的模式来设置默认的加载动画:
///default LoadMoreView delegate
class DefaultLoadMoreDelegate extends LoadMoreDelegate {@overrideWidget buildChild(BuildContext context, LoadMoreStatus status) {switch (status) {case LoadMoreStatus.IDLE:case LoadMoreStatus.LOADING:return LoadingAnimation(blockBackKey: false);break;case LoadMoreStatus.NOMORE:return Center(child: Padding(padding: EdgeInsets.all(10.0),child: Row(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[Text(S.of(context).loadMore_Nomore,style: TextStyle(color: Colors.white),),],),),);break;case LoadMoreStatus.FAIL:return Text(S.of(context).loadMore_Fail,style: TextStyle(color: Colors.white),);break;}return null;}
}
3.4 其他问题
到这里,基本讲清楚了LoadMoreIndicator
的实现思路,还有很多细节问题需要花功夫完善,如:怎么判断是否加载完,没有更多数据?是否可以提供默认的EmptyView?
3.4.1 loadData()的实现
前面已经提到过当判断滚动到底的时候需要触发加载更多,loadData()这个函数怎么实现呢?
/// notify UI to load more data and receive resultvoid loadData() {if (_status == LoadMoreStatus.LOADING) {return;}if(mounted) {setState(() {_updateStatus(LoadMoreStatus.LOADING);});}widget.onLoadMore((int count, int pageNum, int pageSize) {if (pageNum * pageSize >= count) {_updateStatus(LoadMoreStatus.NOMORE);} else {_updateStatus(LoadMoreStatus.IDLE);}if(mounted) {setState(() {_isEmpty = count == 0;});}}, (int errorCode) {_updateStatus(LoadMoreStatus.FAIL);if (mounted) {setState(() {});}});}
在LoadMoreIndicator
中滚动到底之后,需要触发真实的页面去请求数据,而不可能在控件里边去完成业务逻辑。在java中可以使用回调接口来完成,再把请求结果传回LoadMoreIndicator
,用于更新footer状态。在dart中可以使用typedef来完成相同的功能,即用方法来代替回调接口,这部分不是本文的重点,在此略过。
来看一下LoadMoreIndicator
中回调方法的定义:
typedef void LoadMoreOnSuccess(int totalCount, int pageNum, int pageSize);
typedef void LoadMoreOnFailure(int errorCode);
typedef void LoadMoreFunction(LoadMoreOnSuccess success, LoadMoreOnFailure failure);
LoadMoreFunction
作为LoadMoreIndicator
的一个成员变量,它的实现在具体业务逻辑中。LoadMoreOnSuccess
和LoadMoreOnFailure
是业务逻辑加载失败或成功的回调,用于通知LoadMoreIndicator
更新footer状态。
3.3.2 为什么不能用ScrollController
在LoadMoreIndicator
完成之后,能够满足项目中大部门场景,但是在一个场景中,页面不能滚动了。先来看下设计图:
在这个界面中,有三个页签,每一个页签都要求能够加载更多。flutter提供了NestedScrollView
来实现一个滑动头部折叠的动画效果。在NestedScrollView
的body中设置TabBarView
,即可达到效果。
之前提到,为了监听滚动,一般来说得给控件设置ScrollController来监听,但是NestedScrollView
本身自带一个监听,用于处理滚动冲突,并且在NestedScrollView
有一段注释:
// The "controller" and "primary" members should be left// unset, so that the NestedScrollView can control this// inner scroll view.// If the "controller" property is set, then this scroll// view will not be associated with the NestedScrollView.// The PageStorageKey should be unique to this ScrollView;// it allows the list to remember its scroll position when// the tab view is not on the screen.
所以,在LoadMoreIndicator
只能使用ScrollNotification
来监听滚动到底,但是在这样修改之后,理论上能够监听tabbarview的滚动了,实际上,tabbarview还是不能滚动到底,头像依然不能被收起。来看下那个包裹头像的appbar是怎么写的吧:
SliverAppBar(pinned: true,expandedHeight: ScreenUtils.px2dp(1206),forceElevated: innerBoxIsScrolled,bottom: PreferredSize(child: Container(child: TabBar(indicatorColor: Colors.red,indicatorWeight: ScreenUtils.px2dp(12),indicatorPadding: EdgeInsets.only(top: 10.0),indicatorSize: TabBarIndicatorSize.label,labelColor: Colors.red,labelStyle: _tabBarTextStyle(),unselectedLabelColor: Colors.white,unselectedLabelStyle: _tabBarTextStyle(),tabs: _tabTagMap.keys.map((String tag) => Tab(child: Tab(text: tag),),).toList(),),color: Colors.black,),preferredSize:Size(double.infinity, ScreenUtils.px2dp(192))),flexibleSpace: Container(child: Column(children: <Widget>[AppBar(backgroundColor: Colors.black,),Expanded(child: _userInfoHeadWidget(context, _userInfo, UserInfoType.my),),],),),),
),
看上去没有什么问题,但是tabbar无论如何不能被收起,后来无意在github上发现,改为以下可实现:
SliverAppBar(expandedHeight: ScreenUtils.px2dp(1206),flexibleSpace: SingleChildScrollView(physics: NeverScrollableScrollPhysics(),child: Container(child: Column(children: <Widget>[AppBar(backgroundColor: Colors.black,),_userInfoHeadWidget(context, _userInfo, UserInfoType.my),],),),),
其实思想就是把用户头像appbar的flexiblespace里,同时设置flexiblespace可滚动。这样,tarbar就可以收起了。
4、结语
经过一些踩坑,一个在flutter下的加载更多就完成了。总体来说,flutter的开发是比Android开发效率高。不过,目前还是很不成熟,在Android中一句话可以搞定的事情,在flutter中确不一定。能够做出这个加载更多,也是站在巨人的肩膀上,感谢以下作者给予的启发。
相关参考:https://blog.csdn.net/qq_28478281/article/details/83827699
相关参考:https://juejin.im/post/5bfb9cb7e51d45592b766769
相关参考:https://stackoverflow.com/questions/48035594/flutter-notificationlistener-with-scrollnotification-vs-scrollcontroller
相关参考:https://github.com/xuelongqy/flutter_easyrefresh/blob/master/README.md