如果你曾經碰過前端框架如 React 或是 Vue,應該都會接觸過 Redux 或 Vuex 這類的狀態管理工具,在 Flutter 中也存在這類的工具。例如很多也是撰寫 flutter 主題的會介紹的 Bloc、Riverpod、GetX 等等,這些很多都是兼顧了商業邏輯、以及開發彈性的強大狀態管理工具。不過,畢竟我們主張從零開始,因此我打算從輕量版的 Provider 來開始切入。
應用程式常常需要在很多不同畫面中來共享應用程式的狀態資料,拿官方文件的範例來說。
假設我們今天製作的是一個電商網站,widget tree 如圖。我們透過MyCatalog
底下的 MyListItem
後方 Add
按鈕將商品加進購物車,並於 MyCart
頁面中進行檢視。
但問題來了,我們要將 item 加進購物車時,必須要將當前點擊項目先往上傳至 MyCatalog
再由 MayCatalog
向上傳至 MyApp
。因此為了紀錄購物車清單項目,我們須於 MyApp
中建立一個 cart
變數來記錄狀態。
每次更新購物車項目便會使 MyApp
觸發一次 build 更新。不過這個 build 的代價可是很驚人的... 因為會連同其子 widget tree 一同更新。從此例可看出因為是最上層的節點更新,所以更新的是整個 widget tree,很明顯的這並不是好作法。
這時候使用狀態管理工具的優勢就出來了!狀態管理工具可以幫助我們在不同頁面中共享應用程式的狀態資料。我們僅需透過工具在 MyApp
中聲明我們有一個 cart
變數,便可以直接的在任一 widget 中對其進行操作,並只會更新所需更新的部分,無需整體頁面的重新渲染。
Provider 是一個輕量版的狀態管理工具,讓我們先安裝 provider
到我們的專案中吧
flutter pub add provider
在接下來的篇幅當中,我們將使用 Provider 來觸發我們應用程式的黑暗模式功能。
ChangeNotifier 是一種封裝狀態的方法,從字面上就大概可以猜出其用途,當發生變化時可以向監聽器發出通知,且應用程式中可以同時有多個 ChangeNotifier
。在我們的應用程式中,讓我們先來定義一個觸動開啟、關閉黑暗模式開關的 ChangeNotifier
。請在 models
底下宣告一個 theme.dart
檔案並請參考下方程式碼:
class ThemeModel extends ChangeNotifier {
bool _isDark = false;
bool get isDark => _isDark;
void toggleTheme() {
_isDark = !_isDark;
notifyListeners();
}
}
方才我們定義一個 ThemeModel
,並封裝了一個狀態 _isDark
用於記錄當前是否為黑暗模式。當我們要在應用程式其他地方調用此變數時則是使用 ThemeModel.isDark
的 getter
來取值,同時也可確保無法直接更改該狀態。
唯一可以更動到該變數的方式便是透過呼叫 toggleTheme()
方法來更新狀態,在該方法中的 notifyListeners
則是用於觸動監聽該狀態的元件進行更新。
ChangeNotifierProvider
為其中一種 provider,可用於接收 ChangeNotifier
變化的通知,並作出相應的變化。
在我們的應用程式例子中,我們希望透過切換 ThemeModel
的 _isDark
變數來觸動整個應用程式的主題更新。因此我們要將 ChangeNotifierProvider
放置於 widget tree 的最上層。
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(
ChangeNotifierProvider(
// 接收來自於 ThemeModel 這個 ChangeNotifier 發出的變化通知
create: (context) => ThemeModel(),
// 此通知會觸動 MyApp 做出相應更新
child: const MyApp()
));
}
不過值得注意的是,請放置ChangeNotifierProvider
於合適的位階,避免破壞整體結構。
若同時有多個 ChangeNotifierProvider
,可使用 MultiProvider
,如下:
MultiProvider(
providers: [
ChangeNotifierProvider(...),
ChangeNotifierProvider(...),
],
child: const MyApp(),
)
Provider.of
可用於讀取當前 ChangeNotifier
的狀態。請打開 profile_screen.dart
,我們可以做點改寫:
// 首先請先移除 bool _isDarkMode 的 state,我們要改用 provider 來記錄狀態
@oveerride
Widget build(BuildContext context) {
final ThemeModel themeModel = Provider.of<ThemeModel>(context);
return CupertinoPageScaffold(
// 以上省略,直接移至切換夜間模式的 CupertinoSwitch 區塊
trailing: CupertinoSwitch(
value: themeModel.isDark, // 從原先的讀取本地 state 改為監聽 ThemeModel 提供的數據
onChanged: (bool? value) {
// 切換主題的狀態
themeModel.toggleTheme();
}
)
)
}
現在我們成功的切換主題拉,不過怎麼應用程式好像沒有任何動靜?
因為我們還需要設定 CupertinoThemeData
當中的 brightness
參數,其預設為淺色模式,我們需要讓他動態承接 ThemeModel
中的值。
brightness: themeModel.isDark ? Brightness.dark : Brightness.light,
當前效果如下圖:
看起來很棒... 才怪XDD 雖然可以成功切換主題了,但一堆該切換成顯示深色的都沒有,讓我們來一一處理吧!
我們以往在設定顏色時,都是直接給定單一顏色。但這會導致無論在深色模式或是淺色模式都只能顯示單一種顏色的樣式。
在 Cupertino 提供了 CupertinoDynamicColors
用於建立動態顏色。這裡我們要根據 brightness 來動態調整顏色,使用的是 CupertinoDynamicColors.brightness
來設定。我們看類別定義:
const CupertinoDynamicColor.withBrightness({
String? debugLabel,
required Color color, // 當 brightness 為 light 時套用此顏色
required Color darkColor, // 當 brightness 為 dark 時套用此顏色
})
所以只要將我們目前寫死的所有 color 欄位的內容都改成使用 withBrightness
來動態調整顏色,就大功告成拉!交給各位讀者自己試試看囉~
今天我們認識了 Flutter 中的 state management,也使用了 provider
這個輕量版的狀態管理工具來實作切換深色模式,並動態的調整整體顏色。
明天預計會來介紹製作本地化的通知功能,盡請期待喔!
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day22/micro_news_app