flutter state layout
前言
在 app 中我们经常会遇到这样的场景,进入页面时先发起网络请求从后台获取数据,此时应显示 loading 界面,待数据返回时显示正常的界面,如果发生了网络异常还需要显示 error 界面,并且具有点击重试的机制。
针对这样的场景我们肯定希望能封装一个 widget 来满足这样的需求,在这里分享一个我的 flutter 项目中的 state_layout给大家。
Future Builder
FutureBuilder是官方提供的一个 widget,具体使用可以去查看官方文档。
Widget that builds itself based on the latest snapshot of interaction with a Future.
我的这个控件就是基于 FutureBuilder 实现的。
视图View
class LoadingLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Center(
child:
SizedBox(width: 40, height: 40, child: CircularProgressIndicator()),
));
}
}
class EmptyLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Center(
child: Text('-暂无数据-'),
));
}
}
class ErrorLayout extends StatelessWidget {
final String errorInfo;
final VoidCallback onError;
ErrorLayout({this.errorInfo = '出错了!', this.onError});
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Center(
child: GestureDetector(
onTap: onError,
child: Text(errorInfo ?? '出错了!'),
),
),
);
}
}
视图 View 的代码很简单,其中错误视图我们需要给它一个点击事件,使它具有重新请求网络的功能。
核心代码
typedef SucViewBuilder<T> = Widget Function(T data);
typedef FutureFuc<T> = Future<T> Function();
class StateLayout<T> extends StatefulWidget {
StateLayout({@required this.future, @required this.builder, this.contoller});
final FutureFuc<T> future;
final SucViewBuilder<T> builder;
final StateLayoutContoller contoller;
@override
_StateLayoutState createState() => _StateLayoutState<T>();
}
class _StateLayoutState<T> extends State<StateLayout<T>> {
Future _future;
@override
void initState() {
super.initState();
_future = widget.future();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
// 这里为什么不直接使用 widget.future()
future: _future,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done) {
if (snap.hasError) {
final error = snap.error;
return ErrorLayout(
onError: onError,
errorInfo: (error is Exception) ? error.toString() : null,
);
} else if (snap.hasData && snap.data != null) {
return widget.builder(snap.data);
} else {
return EmptyLayout();
}
} else if (snap.connectionState == ConnectionState.waiting) {
return LoadingLayout();
} else {
return ErrorLayout(
onError: onError,
);
}
},
);
}
void onError() {
setState(() {
_future = widget.future();
});
}
}
核心代码基本是参照FutureBuilder的官方代码,这里需要特别注意的是FutureBuildler的参数使用的是成员变量 _future 而不是widget.future()。
在 flutter 中 StateFullWidget的 build方法会在父组件调用 setState或父组件视图树发生变化(比如页面跳转)时调用, build方法多次调用会导致多次闯将 Future实例,在 FutureBuild中则通过对比新旧 future是否相同来订阅 future。
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
所以为了避免出现重复的网络请求和视图变化,所以这里只能使用成员 _future。
到这里核心功能已经完成了,有时我们需要主动去请求网络更新数据,碰到这种情况我们只需要给 StateLayout增加一个 Controller即可,代码如下:
class StateLayoutContoller {
Function refresh;
void callRefresh() {
refresh();
}
}
class StateLayout<T> extends StatefulWidget {
// 增加 controller
StateLayout({@required this.future, @required this.builder, this.contoller});
}
class _StateLayoutState<T> extends State<StateLayout<T>> {
void initState() {
super.initState();
_future = widget.future();
// 初始化 controller
if (widget.contoller != null) {
widget.contoller.refresh = _refresh;
}
}
void _refresh() {
setState(() {
_future = widget.future();
});
}
}
使用
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('state layout'),
),
body: StateLayout<int>(
future: _fetchData,
builder: (count) {
return Text('数量是: $count');
},
),
);
}
Future<int> _fetchData() async {
await Future.delayed(Duration(seconds: 2));
print('fetch data');
return 1;
}
可以看到整个代码很简洁,不需要处理异常,不需要主动去显示 loading 视图或成功视图。最后,本文中的示例代码可以在我的 github上找到。