flutter state layout

843 阅读3分钟

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 中 StateFullWidgetbuild方法会在父组件调用 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上找到。