iT邦幫忙

2023 iThome 鐵人賽

DAY 17
1
Mobile Development

Flutter 從零到實戰 - 30 天の學習筆記系列 第 17

[Day 17] 實戰新聞 APP - FutureBuilder 與 StreamBuilder

  • 分享至 

  • xImage
  •  

昨天我們把我們的探索頁面的樣貌做的差不多拉,不過 Flutter 提供了兩個強大的 widget FutureBuilderStreamBuilder 專門用於處理這種非同步獲取數據的渲染畫面。所以讓我們來練習看看。

FutureBuilder

顧名思義 FutureBuilder 就是根據 Future 獲取的資料來建構畫面的內容。更正確得來說,讓我們來看看 FutureBuilder 類別的定義:

const FutureBuilder({
  super.key,
  this.future,             // Future 型態,放置非同步獲取的資料
  this.initialData,        // 初始化的資料
  required this.builder,   // 根據 future 狀態來建構內容
});

非同步的運算會在物件被呼叫的時候即開始,builder 會根據 future 的狀態來建構應顯示的內容。這裡文件中使用一個很有趣的詞叫做 snapshot (快照) ,可以將當前連線的狀態進行快照,並提供給 builder 表明當下的狀態。快照的狀態包括了:

  • ConnectionState.none :非同步的行為尚未開始
  • ConnectionState.waiting :非同步操作正在進行中,但仍未回傳結果
  • ConnectionState.active :專使用於 stream 情形,用以表示 stream 已連結並正在運作
  • ConnectionState.done :非同步操作已完成

因此我們就可以根據快照給定的狀態來決定我們應呈現的畫面內容。例如當操作還在 waiting 時呈現讀取動畫、done 時才真的顯示內容等。

所以在我們的使用場景會如下:

// 請將原先的 _categories 型態改為 Future
Future<NewsCategory> _categories;

// 在 initState 的時候就把請求發出去
void initState() {
  super.initState();
  _categories = NewsCategoryRepository().getCategories();
}

// FutureBuilder 回來等待
FutureBuilder({
  future: _categories,
  builder: (conext, snapshot) { ... }
})

// 請絕對不要這樣寫
// FutureBuilder({ future: NewsCategoryRepository.getCategories() })

請一定一定一定要記得!!請不要在 FutureBuilder 的時候才丟請求,這樣會造成你的畫面一旦重新 rebuild 時都會重新發一次請求。因為我們的新聞分類不會讓使用者進行操作,所以我們才會將請求數據的行為放在 initState 的階段,只會發出一次的請求行為。

接著我們要來看看 builder 裡的程式碼:

builder: (context, snapshot) {
  if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
    return _buildCategories(snapshot.data as List<NewsCategory>);
  } else if (snapshot.hasError) {
    return const Center(child: Text('發生錯誤'));
  } else {
    return const Center(child: CupertinoActivityIndicator());
  }
},

剛剛提過快照有四種連線的狀態,我們可以根據判斷狀態來給予顯示不同的狀態。因此上方程式碼僅有在確定連線完成並且快照中有資料時,才會透過呼叫 _buildCategories 來呈現新聞的分類們;若產生錯誤時,可以顯示錯誤訊息;最後若還在讀取時則回傳 CupertinoActivityIndicator widget,用轉圈圈的動畫來表示仍在讀取中。

至於 _buildCategories 要怎麼寫,其實只是簡單的拆分函式的動作,可以試著自己想想看。另一個新聞來源的區塊,因為動作也差不多,也交由各位自行練習。我們將在今天的參考程式碼中公布參考的答案。

StreamBuilder

既然有了 FutureBuilder 來應對 Future 的各種連線狀態,同樣的就也有 StreamBuilder 來應對 Stream 的使用場景。

const StreamBuilder({
  super.key,
  this.stream,             // Stream 型態
  this.initialData,        // 初始化的資料
  required this.builder,   // 根據 future 狀態來建構內容
});

從類別定義上來看,其實除了對象從 future 改成 stream 之外其餘的沒有什麼變化。也確實,在 builder 中也是針對不同快照的狀態來進行操作。所以我們直接來看看 StreamBuilder 要如何使用於我們的應用程式中。

請打開 main.dart ,我們之前一直沒有實現當使用者成功登入後,跳轉至應用程式首頁的流程。其實 firebase 有提供一個 stream 的方法可以用於偵測使用者的登入狀態是否改變,讓我們來套用吧

// 以上省略
home: StreamBuilder(
  // 偵測使用者的狀態是否改變
  stream: FirebaseAuth.instance.authStateChanges(),
  builder: (context, snapshot) {
    // 使用者的狀態有資料
    if (snapshot.hasData) {
      // 進到實作底部導航列的 widget
      return const TabLayout();
    } else {
      // 否則顯示登入畫面
      return const LoginScreen();
  }
}));

這麼一來,就可以實現完整的流程拉!

跳轉頁面

當初設計稿設計成點擊下圖左側的「新聞來源」標題,可以跳轉至顯示縱向新聞來源列表的頁面,從該頁面點擊左上角回上頁的按鈕會再回到探索頁面。我們會使用到前面介紹的 Navigator.pushNavigator.pop 來實作此功能。
https://ithelp.ithome.com.tw/upload/images/20231002/20135082sPg56Q0I02.png

由於是一個全新的頁面,因此請在 screens 資料夾底下建立一個 news_source_screen.dart 的檔案,並請參考下方程式碼:

class NewsSourceScreen extends StatelessWidget {
  const NewsSourceScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('新聞來源',
            // 套用在 CupertinoApp 設定的 navLargeTitleTextStyle 樣式,並將 fontSize 設為 20
            style: CupertinoTheme.of(context)
                .textTheme
                .navLargeTitleTextStyle
                .copyWith(fontSize: 20)),
        previousPageTitle: '探索',
      ),
      child: const Center(
        child: Text('新聞來源'),
      ),
    );
  }
}

建立好以上頁面後便是要實作跳轉的功能,請開啟 browse_screen.dart 檔案並找到建構「新聞來源」標題的 widget。

原先該標題僅為 Text 單純顯示,但我們希望點擊該標題時可觸發跳轉行為。因此我們需要做以下的兩個步驟:

  1. 在標題後方加入 > 的圖標,用以標示該標題是可點擊的
  2. 將標題外面包一層可點擊的按鈕,並寫好觸發的事件

結果可參考下方程式碼

CupertinoButton(
  padding: const EdgeInsets.fromLTRB(16, 32, 16, 0),
  onPressed: () {
    // 點擊後,便會導向方才建立的縱向新聞來源頁面
    Navigator.push(context, CupertinoPageRoute(builder: (context) {
      return const NewsSourceScreen();
    }));
  },
  child: const Row(
    children: [
      Text('新聞來源',
          style: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w500,
              color: CupertinoColors.black)),
      SizedBox(width: 4),
      Icon(CupertinoIcons.forward,
          size: 22, color: CupertinoColors.systemGrey),
    ],
  ),
),

讓我們來看看效果吧~
https://i.imgur.com/WZ05Su7.gif

補充
前幾天我們有統一設定了 CupertinoApp 的文字樣式,若運用原先的樣式可能會遇到問題。因此補充說明一下,在 main.dart 中的樣式設定,我們加入一行

```dart
// 以上省略
textTheme: CupertinoTextThemeData(
    navLargeTitleTextStyle: TextStyle(
      inherit: false, // 加入此設定
      fontSize: 34,
      fontWeight: FontWeight.w700,
      fontFamily: 'GenSenRounded',
      color: CupertinoColors.black,
      letterSpacing: 1.05),
// 以下省略
```

inherited 的參數設定意義在於是否要讓子組件繼承相同的文字樣式 (此例為應用程式所有頂端列標題) 。在今天我們的內容中跳轉頁面時頂端標題會呈現過度動畫,若 inherited 值為 true 時會使的在 news_source_screen.dart 頁面頂端顯示「新聞來源」四字標題,因同時繼承了 CupertinoApp 設定的文字樣式與該 widget 本身賦予的文字樣式,使得過度動畫會因不知要套用何者而導致錯誤發生。因此將該值設為 false 即可避免該錯誤。

頂端原先「探索」大標題的放大/縮小動畫看起來很棒,已經非常接近原生 iOS 應用程式的效果。不過有個真的沒辦法改的東西... 就是回上頁的按鈕顏色,理論上應要貼合我們給定應用程式的主色,我有追到 CupertinoNavigationBar 底層的類別定義去看,從定義上來說確實該按鈕的顏色應要貼合我們在 Theme 給定的主色。不過不知道為什麼 Cupertino並沒有套用,只能等未來官方修正拉~

練習 17-1

既然完成了跳轉的頁面實作,現在是時候來完成 news_source_screen.dart 中的內容拉。該頁面的實作流程如下:

  1. 呼叫取得所有新聞來源的 API
  2. 等 API 回應完成後,根據回應的資料建構縱向的列表

上述的流程與實作「探索」頁面時的流程大致相同,需要用到 FutureBuilderListView.builder 來實作。因此請試著使用以上兩 widget 使得該頁面能盡可能的與設計稿的結果相符。如下圖:

在開始寫之前我們來介紹一下 ListTile 這個工具

ListTile

ListTile 是 flutter 中常用於建構 ListView 顯示文字、圖片與可點擊操作列表元素項目的 widget ,從類別定義上來查看 (Cupertino 提供的為 CupertinoListTile )

CupertinoListTile({
  super.key,
  required this.title,     // 主要標題
  this.subtitle,           // 副標題
  this.additionalInfo,     // 位階較副標題低的顯示內容
  this.leading,            // 放置於標題左側的項目
  this.trailing,           // 放置於標題右側的項目
  this.onTap,              // 點擊該元素的行為
  this.backgroundColor,    // 元素背景顏色
  this.backgroundColorActivated, // 元素被選擇時的背景顏色
  this.padding,            // 內部與外框的間距
  this.leadingSize = _kLeadingSize,  // leading 與左側邊界的距離
  this.leadingToTitle = _kLeadingToTitle,  // 主標題與 leading 的距離
})

因此透過 CupertinoListTile 建構每個列表項目顯示內容,leading 皆為方形圖標、title 皆為該來源名稱、subtitle 為該來源 id 與 trailing 放置 「前往」的按鈕。

所以就實作看看吧,同樣我會將參考答案分享於文末

今日總結

從昨天的內容中我們學習如何串接 API;今天則是基於昨天的基礎更加延伸的進階內容。我們認識到 FutureBuilderStreamBuilder 的使用方式,並透過快照狀態更方便的對頁面內容進行加值,例如顯示讀取動畫、錯誤訊息等等。最後我們也複習了如何跳轉頁面並完成了「探索」、「新聞來源」頁面的切換。

明天我們要運用我們這兩天所學的技巧,將搜尋頁面完成。我們繼續加油!

今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day17/micro_news_app


上一篇
[Day 16] 實戰新聞 APP - 串接 API
下一篇
[Day 18] 實戰新聞 APP - Custom Widget
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言