FutureBuilder and StreamBuilder 優(yōu)雅的構(gòu)建高質(zhì)量項目

cover

本篇文章將介紹從 setState 開始,到 futureBuilderstreamBuilder 來優(yōu)雅的構(gòu)建你的高質(zhì)量項目,而不引發(fā) setState 帶來的副作用,如對文章感興趣,請 點擊查看源碼。

基礎(chǔ)的setState更新數(shù)據(jù)

首先,我們使用基礎(chǔ)的 StatefulWidget 來創(chuàng)建頁面,如下:

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

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

然后,我們使用 Future 來創(chuàng)建一些數(shù)據(jù),來模擬網(wǎng)絡(luò)請求,如下:

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

initState() 方法中調(diào)用 _getListData() 來初始化數(shù)據(jù),如下:

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

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

使用 ListView.builder 來處理這些數(shù)據(jù)構(gòu)建UI,如下:

  @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(),
            ],
          );
        },
      ),
    );
  }

最后,我們就可以看到界面了 ?? ,如圖:

list-data

當(dāng)然,你也可以將 UI 顯示單獨提取成一個方法,方便后期維護,使代碼層次更清晰,如下:

  @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(),
                ],
              );
  }

繼續(xù),我們來完善它,正常從后端獲取數(shù)據(jù),后端應(yīng)該會給我們返回不同信息,根據(jù)這些信息需要處理不同的狀態(tài),如:

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

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

bool get _fetchingData => _pageData == null; // 判斷數(shù)據(jù)是否為空

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

效果如圖:

indicator

接著,我們來處理 ErrorState ,我給 _getListData() 添加 hasError 參數(shù)來模擬后端返回的錯誤,如下

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

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

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

然后,在 initState() 方法中捕獲異常更新數(shù)據(jù),如下:

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

效果如圖( 當(dāng)然這里可以使用一個錯誤頁面來展示 ):

error

接著,我們來處理 NoData ,我給 _getListData() 添加 hasData 參數(shù)來模擬后端返回空數(shù)據(jù),如下:

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

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

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

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

然后,在 initState() 方法更新數(shù)據(jù),如下:

  @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-data

這就是通過 setState() 來更新數(shù)據(jù),是不是很簡單,通常情況下我們這么使用是沒什么問題,但是,如果我們的頁面足夠復(fù)雜,要處理的狀態(tài)足夠多,我們需要使用更多的 setState() ,意味著我們要更多的代碼來更新數(shù)據(jù),而且,我們每次 setState() 的時候 build() 方法就會重新執(zhí)行一次( 這就是上文提到的副作用 )。

其實,Flutter 已經(jīng)提供了更優(yōu)雅的方式來更新我們的數(shù)據(jù)及處理狀態(tài),它就是我們接下來要介紹的 futureBuilder。

FutureBuilder

FutureBuilder 通過 future: 參數(shù)可以接收一個 Future ,并且通過 builder: 參數(shù)來構(gòu)建 UI ,builder: 參數(shù)是一個函數(shù),它提供了一個 snapshot 參數(shù)里面帶著我們需要的狀態(tài)和數(shù)據(jù)。

接下來,我們將上面的 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 已經(jīng)給我們提供好了 error 狀態(tài)
            return _getInfoMessage(snapshot.error);
          }

          if (!snapshot.hasData) { // FutureBuilder 已經(jīng)給我們提供好了空數(shù)據(jù)狀態(tài)
            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 已經(jīng)給我處理好了一些基本狀態(tài),如圖

snapshot

我們使用 _getInfoMessage() 方法來處理狀態(tài)提示,如下:

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

就這樣我們不使用任何一個 setState() 就能完成和上面一樣的效果,并且不會產(chǎn)生副作用,是不是很給力 ??。

但是,它并不是完美的,比如,我們想刷新數(shù)據(jù),我們需要重新調(diào)用 _getListData() 方法,結(jié)果它并沒有刷新。

StreamBuilder

StreamBuilder 通過 stream: 參數(shù)可以接收一個 stream ,同樣,通過 builder: 參數(shù)來構(gòu)建 UI ,和 futureBuilder 用法類似,唯一的好處就是,我們可以隨意控制 stream 的輸入輸出,添加任何的狀態(tài)來更新指定狀態(tài)下的 UI 。

首先,我們使用 enum 來表示我們的狀態(tài),在文件的頭部添加它,如下:

enum StreamViewState { Busy, DataRetrieved, NoData }

接著,使用 StreamController 創(chuàng)建一個流控制器,把 FutureBuilder 替換成 StreamBuilder ,把 future: 參數(shù) 改成 stream: 參數(shù),如下:


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 來更新數(shù)據(jù)
          if (!snapshot.hasData || StreamViewState.Busy) {
            return Center(
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
                backgroundColor: Colors.yellow[100],
              ),
            );
          }
          //使用 枚舉的 NoData 來更新數(shù)據(jù)
          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(),
                ],
              );
            },
          );
        },
      ),
    );
  }

只是新增了枚舉值來判斷是否需要更新數(shù)據(jù),其他基本保持不變。

接下來,我需要修改 _getListData() 方法,使用流控制器添加狀態(tài)及數(shù)據(jù),如下:

  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 數(shù)據(jù)
    }

    if (!hasData) {
      return _stateController.add(StreamViewState.NoData); // 往 stream 里新增無數(shù)據(jù)狀態(tài)
    }

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

此時我們并沒有返回數(shù)據(jù),所以我們需要創(chuàng)建 listItems 存儲數(shù)據(jù),然后把 StatelessWidget 改成 StatefulWidget ,以便我們根據(jù) stream 的輸出來更新數(shù)據(jù),這個轉(zhuǎn)換非常方便,VS Code 編輯器可以使用 Option + Shift + R (Mac)或者 Ctrl + Shift + R (Win)快捷鍵 ,Android Studio 使用Option + Enter 快捷鍵,之后在 initState() 方法中初始化數(shù)據(jù),如下:

List<String> listItems;

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

到這里我們已經(jīng)解決了 FutureBuilder 的局限性問題,我們可以新增一個 FloatingActionButton 來刷新數(shù)據(jù),如下:

@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(

        ...
        
      ),
    );
  }

現(xiàn)在,點擊 FloatingActionButton 加載指示器已經(jīng)顯示,但是,我們的 listItems 數(shù)據(jù)并沒真正的更新,點擊 FloatingActionButton 只是更新的加載狀態(tài)而已,而且我們的業(yè)務(wù)邏輯代碼和 UI 代碼還在同一個文件中,很顯然,他們已經(jīng)解耦,所以,我們可以繼續(xù)完善它,將業(yè)務(wù)邏輯代碼和 UI 代碼分離出來。

分離業(yè)務(wù)邏輯代碼和 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));
  }
}

然后,把狀態(tài)也封裝成一個文件且將數(shù)據(jù)和狀態(tài)關(guān)聯(lián),如下:

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(); // 創(chuàng)建 model

  @override
  void initState() {
    model.dispatch(FetchData(hasData: true)); // 獲取 model 里的數(shù)據(jù)
    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) {  // 通過封裝的狀態(tài)類來判斷是否更新UI
            return Center(
              child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.yellow),
                backgroundColor: Colors.yellow[100],
              ),
            );
          }

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

  ...

}

此時,業(yè)務(wù)邏輯代碼和 UI 代碼已完全分離,且可擴展性和維護增強,且我們的數(shù)據(jù)和狀態(tài)已關(guān)聯(lián)起來,此時,點擊 FloatingActionButton 效果和上面一樣,且數(shù)據(jù)已更新。

最后附上博客、GitHub地址:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容