
在 Flutter App 中常常需要使用網路服務或者等待檔案系統回應,當我們使用這類服務時,整體的 UI 並不會阻塞。這些服務通常使用 async function 處理資料,並回傳 Future 物件。為了讓 UI 能在資料取得中與取得後切換,我們可以用 FutureBuilder 來建構畫面。FutureBuilder 在取得資料前及取得資料後會自動切換狀態,可以避免使用複雜的狀態管理。
範例程式傳送門: https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/future_builder
FutureBuilder  本身也是一個 StatefulWidget 。首先我們先了解如何建構它
class FutureBuilder<T> extends StatefulWidget {
  const FutureBuilder({
    super.key,
    required this.future,
    this.initialData,
    required this.builder,
  });
  final Future<T>? future;
  final AsyncWidgetBuilder<T> builder;
  final T? initialData;
  // ...
}
future 參數要代入一個 Future 物件,可以儲放向網路或檔案系統等異步請求的結果,為了方便演示,我們這裡設定延遲 2 秒後自動回傳一個字串:
Future.delayed(const Duration(seconds: 2), () => "SOME THING");
builder 的部分的一個函式參數,其中 context 的與 build() 中的 context 功能一樣,另一個 snapshot 用來代表 future 回傳值目前的狀態
Widget Function(BuildContext context, AsyncSnapshot<T> snapshot)
整體的寫法如下
FutureBuilder<String>(
  future: _fetch,
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    List<Widget> children;
    if (snapshot.hasData) {
      // 當 _fetch 回傳值後
      // 利用 snapshot.data 取得值
    } else if (snapshot.hasError) {
      // 當 _fetch 拋出錯誤 (throw error) 時
      // 利用 snaphost.error 取得值
    } else {
      // 初始狀態,通常我們會在這邊放一個轉圈圈 widget
    }
  }
)
有一點要注意的是:future 內的 Future 物件,必需在 build 之前被確定。也就是說我們不可以在 build() 才取得 Future 物件。如果每次 future 都和 FutureBuilder 同時建構,那麼當每次 FutureBuilder 的上層 Widget 被重建時,異步處理也會重新啟動。
// 錯誤範例
class UserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _future = ...
    return Scaffold(
        //...
        FutureBuilder(
          future: _future,
          builder: (context, snapshot) {
            // ...
          }
        )
我們可以利用上方的模版來設計畫面。延續昨天的進度,當我們登入後,也許還會需要向伺服器請求資料,因此我們可以在 UserPage 中,加入 FutuerBuilder
class UserPage extends StatelessWidget {
  UserPage({super.key});
  final Future<String> _fetch =
      Future.delayed(const Duration(seconds: 2), () => "SOME THING");
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text('User Page'),
        ),
        body: Center(
            child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 20),
                child: FutureBuilder(
                    future: _fetch,
                    builder: (context, snapshot) {
                      if (snapshot.hasData) {
                        return Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: <Widget>[
                            Text(
                                'Hello ${UserDataLayer.of(context).user?.name}'),
                            const SizedBox(height: 10),
                            Text('${snapshot.data}')
                          ],
                        );
                      } else if (snapshot.hasError) {
                        return Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              const Icon(
                                Icons.error_outline,
                                color: Colors.red,
                                size: 60,
                              ),
                              Padding(
                                padding: const EdgeInsets.only(top: 16),
                                child: Text('Error: ${snapshot.error}'),
                              ),
                            ]);
                      }
                      return const CircularProgressIndicator();
                    }))));
  }
}
當 _fetch 正常回傳後執行的效果如下:

我們也可以模擬當錯誤發生時:
final Future<String> _fetchError =
  Future.delayed(const Duration(seconds: 2), () {
    throw Exception('ERROR OCCUR');
  });
// ...
FutureBuilder(
  future: _fetchError

後記:9/12 只寫了一點點,9/13努力補完🙌