FutureBuilder and StreamBuilder 優雅的構建高質量項目

image

本篇文章將介紹從 setState 開始,到 futureBuilderstreamBuilder 來優雅的構建你的高質量項目,而不引起 setState 帶來的反作用,如對文章感興趣,請 點擊查看源碼git

基礎的setState更新數據

首先,咱們使用基礎的 StatefulWidget 來建立頁面,以下:github

class BaseStatefulDemo extends StatefulWidget {
  @override
  _BaseStatefulDemoState createState() => _BaseStatefulDemoState();
}

class _BaseStatefulDemoState extends State<BaseStatefulDemo> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

而後,咱們使用 Future 來建立一些數據,來模擬網絡請求,以下:後端

Future<List<String>> _getListData() async {
    await Future.delayed(Duration(seconds: 1)); // 1秒以後返回數據
    return List<String>.generate(10, (index) => '$index content');
  }

initState() 方法中調用 _getListData() 來初始化數據,以下:網絡

List<String> _pageData = List<String>();

  @override
  void initState() {
    _getListData().then((data) => setState(() {
              _pageData = data;
            }));
    super.initState();
  }

使用 ListView.builder 來處理這些數據構建UI,以下:app

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Base Stateful Demo'),
      ),
      body: ListView.builder(
        itemCount: _pageData.length,
        itemBuilder: (buildContext, index) {
          return Column(
            children: <Widget>[
              ListTile(
                title: Text(_pageData[index]),
              ),
              Divider(),
            ],
          );
        },
      ),
    );
  }

最後,咱們就能夠看到界面了 😎 ,如圖:less

no-shadow

固然,你也能夠將 UI 顯示單獨提取成一個方法,方便後期維護,使代碼層次更清晰,以下:async

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Base Stateful Demo'),
      ),
      body: ListView.builder(
        itemCount: _pageData.length,
        itemBuilder: (buildContext, index) {
          return getListDataUi(int index);
        },
      ),
    );
  }

  Widget getListDataUi(int index) {
    return Column(
                children: <Widget>[
                  ListTile(
                    title: Text(_pageData[index]),
                  ),
                  Divider(),
                ],
              );
  }

繼續,咱們來完善它,正常從後端獲取數據,後端應該會給咱們返回不一樣信息,根據這些信息須要處理不一樣的狀態,如:編輯器

  • BusyState(加載中):咱們在界面上顯示一個加載指示器
  • DataFetchedState(數據加載完成):咱們延遲2秒,來模擬數據加載完成
  • ErrorState(錯誤):顯示錯誤提示
  • NoData(沒有數據):請求成功,但沒有數據,顯示提示

先來處理 BusyState 加載指示器,以下:ide

bool get _fetchingData => _pageData == null; // 判斷數據是否爲空

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Base Stateful Demo'),
      ),
      body: _fetchingData
          ? Center(
              child: CircularProgressIndicator( // 加載指示器 
                valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow), // 設置指示器顏色
                backgroundColor: Colors.yellow[100],  // 設置背景色
              ),
            )
          : ListView.builder(
              itemCount: _pageData.length,
              itemBuilder: (buildContext, index) {
                return getListDataUi(index);
              },
            ),
    );
  }

效果如圖:函數

no-shadow

接着,咱們來處理 ErrorState ,我給 _getListData() 添加 hasError 參數來模擬後端返回的錯誤,以下

Future<List<String>> _getListData({bool hasError = false}) async {
    await Future.delayed(Duration(seconds: 1)); // 1秒以後返回數據

    if (hasError) {
      return Future.error('獲取數據出現問題,請再試一次');
    }

    return List<String>.generate(10, (index) => '$index content');
  }

而後,在 initState() 方法中捕獲異常更新數據,以下:

@override
  void initState() {
    _getListData(hasError: true)
        .then((data) => setState(() {
              _pageData = data;
            }))
        .catchError((error) => setState(() {
              _pageData = [error];
            }));
    super.initState();
  }

效果如圖( 固然這裏可使用一個錯誤頁面來展現 ):

no-shadow

接着,咱們來處理 NoData ,我給 _getListData() 添加 hasData 參數來模擬後端返回空數據,以下:

Future<List<String>> _getListData(
      {bool hasError = false, bool hasData = true}) async {
    await Future.delayed(Duration(seconds: 1));

    if (hasError) {
      return Future.error('獲取數據出現問題,請再試一次');
    }

    if (!hasData) {
      return List<String>();
    }

    return List<String>.generate(10, (index) => '$index content');
  }

而後,在 initState() 方法更新數據,以下:

@override
  void initState() {
    _getListData(hasError: false, hasData: false)
        .then((data) => setState(() {
              if (data.length == 0) {
                data.add('No data fount');
              }
              _pageData = data;
            }))
        .catchError((error) => setState(() {
              _pageData = [error];
            }));
    super.initState();
  }

效果如圖:

no-shadow

這就是經過 setState() 來更新數據,是否是很簡單,一般狀況下咱們這麼使用是沒什麼問題,可是,若是咱們的頁面足夠複雜,要處理的狀態足夠多,咱們須要使用更多的 setState() ,意味着咱們要更多的代碼來更新數據,並且,咱們每次 setState() 的時候 build() 方法就會從新執行一次( 這就是上文提到的反作用 )。

其實,Flutter 已經提供了更優雅的方式來更新咱們的數據及處理狀態,它就是咱們接下來要介紹的 futureBuilder

FutureBuilder

FutureBuilder 經過 future: 參數能夠接收一個 Future ,而且經過 builder: 參數來構建 UIbuilder: 參數是一個函數,它提供了一個 snapshot 參數裏面帶着咱們須要的狀態和數據。

接下來,咱們將上面的 StatefulWidget 改爲 StatelessWidget ,並使用 FutureBuilder 替換,以下:

class FutureBuilderDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Future Builder Demo'),
      ),
      body: FutureBuilder(
        future: _getListData(),
        builder: (buildContext, snapshot) {
          if (snapshot.hasError) {  // FutureBuilder 已經給咱們提供好了 error 狀態
            return _getInfoMessage(snapshot.error);
          }

          if (!snapshot.hasData) { // FutureBuilder 已經給咱們提供好了空數據狀態
            return Center(
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
                backgroundColor: Colors.yellow[100],
              ),
            );
          }
          var listData = snapshot.data;
          if (listData.length == 0) {
            return _getInfoMessage('No data found');
          }

          return ListView.builder(
            itemCount: listData.length,
            itemBuilder: (buildContext, index) {
              return Column(
                children: <Widget>[
                  ListTile(
                    title: Text(listData[index]),
                  ),
                  Divider(),
                ],
              );
            },
          );
        },
      ),
    );
  }

  ...

經過查看源碼,咱們能夠了解的 FutureBuilder 已經給我處理好了一些基本狀態,如圖


snapshot

咱們使用 _getInfoMessage() 方法來處理狀態提示,以下:

Widget _getInfoMessage(String msg) {
    return Center(
      child: Text(msg),
    );
  }

就這樣咱們不使用任何一個 setState() 就能完成和上面同樣的效果,而且不會產生反作用,是否是很給力 💪。

可是,它並非完美的,好比,咱們想刷新數據,咱們須要從新調用 _getListData() 方法,結果它並無刷新。

StreamBuilder

StreamBuilder 經過 stream: 參數能夠接收一個 stream ,一樣,經過 builder: 參數來構建 UI ,和 futureBuilder 用法相似,惟一的好處就是,咱們能夠隨意控制 stream 的輸入輸出,添加任何的狀態來更新指定狀態下的 UI

首先,咱們使用 enum 來表示咱們的狀態,在文件的頭部添加它,以下:

enum StreamViewState { Busy, DataRetrieved, NoData }

接着,使用 StreamController 建立一個流控制器,把 FutureBuilder 替換成 StreamBuilder ,把 future: 參數 改爲 stream: 參數,以下:

final StreamController<StreamDemoState> _stateController = StreamController<StreamDemoState>();

@override
  Widget build(BuildContext context) {
    return Scaffold(

      ...

      body: StreamBuilder(
        stream: model.homeState,
        builder: (buildContext, snapshot) {
          if (snapshot.hasError) {
            return _getInfoMessage(snapshot.error);
          }
          // 使用 枚舉的 Busy 來更新數據
          if (!snapshot.hasData || StreamViewState.Busy) {
            return Center(
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
                backgroundColor: Colors.yellow[100],
              ),
            );
          }
          //使用 枚舉的 NoData 來更新數據
          if (listItems.length == StreamViewState.NoData) {
            return _getInfoMessage('No data found');
          }

          return ListView.builder(
            itemCount: listItems.length,
            itemBuilder: (buildContext, index) {
              return Column(
                children: <Widget>[
                  ListTile(
                    title: Text(listItems[index]),
                  ),
                  Divider(),
                ],
              );
            },
          );
        },
      ),
    );
  }

只是新增了枚舉值來判斷是否須要更新數據,其餘基本保持不變。

接下來,我須要修改 _getListData() 方法,使用流控制器添加狀態及數據,以下:

Future _getListData({bool hasError = false, bool hasData = true}) async {
    _stateController.add(StreamViewState.Busy);
    await Future.delayed(Duration(seconds: 2));

    if (hasError) {
      return _stateController.addError('error'); // 往 stream 裏新增 error 數據
    }

    if (!hasData) {
      return _stateController.add(StreamViewState.NoData); // 往 stream 裏新增無數據狀態
    }

    _listItems = List<String>.generate(10, (index) => '$index content');
    _stateController.add(StreamViewState.DataRetrieved); // 往 stream 裏新增數據獲取完成狀態
  }

此時咱們並無返回數據,因此咱們須要建立 listItems 存儲數據,而後把 StatelessWidget 改爲 StatefulWidget ,以便咱們根據 stream 的輸出來更新數據,這個轉換很是方便,VS Code 編輯器可使用 Option + Shift + R (Mac)或者 Ctrl + Shift + R (Win)快捷鍵 ,Android Studio 使用Option + Enter 快捷鍵,以後在 initState() 方法中初始化數據,以下:

List<String> listItems;

@override
void initState() {
  _getListData();
  super.initState();
}

到這裏咱們已經解決了 FutureBuilder 的侷限性問題,咱們能夠新增一個 FloatingActionButton 來刷新數據,以下:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stream Builder Demo'),
      ),
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.yellow,
        child: Icon(
          Icons.cached,
          color: Colors.black87,
        ),
        onPressed: () {
          model.dispatch(FetchData());
        },
      ),
      body: StreamBuilder(

        ...
        
      ),
    );
  }

如今,點擊 FloatingActionButton 加載指示器已經顯示,可是,咱們的 listItems 數據並沒真正的更新,點擊 FloatingActionButton 只是更新的加載狀態而已,並且咱們的業務邏輯代碼和 UI 代碼還在同一個文件中,很顯然,他們已經解耦,因此,咱們能夠繼續完善它,將業務邏輯代碼和 UI 代碼分離出來。

分離業務邏輯代碼和 UI 代碼

咱們能夠把處理 stream 的代碼抽離成一個類,以下:

import 'dart:async';
import 'dart:math';

import 'package:pro_flutter/demo/stream_demo/stream_demo_event.dart';
import 'package:pro_flutter/demo/stream_demo/stream_demo_state.dart';


enum StreamViewState { Busy, DataRetrieved, NoData }

class StreamDemoModel {
  final StreamController<StreamDemoState> _stateController = StreamController<StreamDemoState>();

  List<String> _listItems;

  Stream<StreamDemoState> get streamState => _stateController.stream;

  void dispatch(StreamDemoEvent event){
    print('Event dispatched: $event');
    if(event is FetchData) {
      _getListData(hasData: event.hasData, hasError: event.hasError);
    }
  }

  Future _getListData({bool hasError = false, bool hasData = true}) async {
    _stateController.add(BusyState());
    await Future.delayed(Duration(seconds: 2));

    if (hasError) {
      return _stateController.addError('error');
    }

    if (!hasData) {
      return _stateController.add(DataFetchedState(data: List<String>()));
    }

    _listItems = List<String>.generate(10, (index) => '$index content');
    _stateController.add(DataFetchedState(data: _listItems));
  }
}

而後,把狀態也封裝成一個文件且將數據和狀態關聯,以下:

class StreamDemoState{}

class InitializedState extends StreamDemoState {}

class DataFetchedState extends StreamDemoState {
  final List<String> data;

  DataFetchedState({this.data});

  bool get hasData => data.length > 0;
}

class ErrorState extends StreamDemoState{}

class BusyState extends StreamDemoState{}

再封裝一個事件文件,以下:

class StreamDemoEvent{}

class FetchData extends StreamDemoEvent{
  final bool hasError;
  final bool hasData;

  FetchData({this.hasError = false, this.hasData = true});

  @override
  String toString() {
    return 'FetchData { hasError: $hasError, hasData: $hasData }';
  }
}

最後,咱們 UI 部分的代碼以下:

class _StreamBuilderDemoState extends State<StreamBuilderDemo> {
  final model = StreamDemoModel(); // 建立 model

  @override
  void initState() {
    model.dispatch(FetchData(hasData: true)); // 獲取 model 裏的數據
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(

      ...

      body: StreamBuilder(
        stream: model.streamState,
        builder: (buildContext, snapshot) {
          if (snapshot.hasError) {
            return _getInformationMessage(snapshot.error);
          }

          var streamState = snapshot.data;

          if (!snapshot.hasData || streamState is BusyState) {  // 經過封裝的狀態類來判斷是否更新UI
            return Center(
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
                backgroundColor: Colors.yellow[100],
              ),
            );
          }

          if (streamState is DataFetchedState) { // 經過封裝的狀態類來判斷是否更新UI
            if (!homeState.hasData) {
              return _getInformationMessage('not found data');
            }
          }
          return ListView.builder(
            itemCount: streamState.data.length,  // 此時,數據再也不是本地數據,而是從 stream 中輸出的數據
            itemBuilder: (buildContext, index) =>
                _getListItem(index, streamState.data),
          );
        },
      ),
    );
  }

  ...

}

此時,業務邏輯代碼和 UI 代碼已徹底分離,且可擴展性和維護加強,且咱們的數據和狀態已關聯起來,此時,點擊 FloatingActionButton 效果和上面同樣,且數據已更新。

最後附上個人博客、GitHub地址:

博客地址:https://h.lishaoy.net/futruebuilder-streambuilder
GitHub地址:https://github.com/persilee/flutter_pro

相關文章
相關標籤/搜索