昨天我們把我們的探索頁面的樣貌做的差不多拉,不過 Flutter 提供了兩個強大的 widget FutureBuilder
與StreamBuilder
專門用於處理這種非同步獲取數據的渲染畫面。所以讓我們來練習看看。
顧名思義 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
要怎麼寫,其實只是簡單的拆分函式的動作,可以試著自己想想看。另一個新聞來源的區塊,因為動作也差不多,也交由各位自行練習。我們將在今天的參考程式碼中公布參考的答案。
既然有了 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.push
與 Navigator.pop
來實作此功能。
由於是一個全新的頁面,因此請在 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
單純顯示,但我們希望點擊該標題時可觸發跳轉行為。因此我們需要做以下的兩個步驟:
結果可參考下方程式碼
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),
],
),
),
讓我們來看看效果吧~
補充
前幾天我們有統一設定了 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並沒有套用,只能等未來官方修正拉~
既然完成了跳轉的頁面實作,現在是時候來完成 news_source_screen.dart
中的內容拉。該頁面的實作流程如下:
上述的流程與實作「探索」頁面時的流程大致相同,需要用到 FutureBuilder
與 ListView.builder
來實作。因此請試著使用以上兩 widget 使得該頁面能盡可能的與設計稿的結果相符。如下圖:
在開始寫之前我們來介紹一下 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;今天則是基於昨天的基礎更加延伸的進階內容。我們認識到 FutureBuilder
與 StreamBuilder
的使用方式,並透過快照狀態更方便的對頁面內容進行加值,例如顯示讀取動畫、錯誤訊息等等。最後我們也複習了如何跳轉頁面並完成了「探索」、「新聞來源」頁面的切換。
明天我們要運用我們這兩天所學的技巧,將搜尋頁面完成。我們繼續加油!
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day17/micro_news_app