在開發 Flutter 應用程式時,管理組件的狀態是一個關鍵的議題。除了直觀容易理解的使用參數來傳遞狀態,還有其他不同的方式。下面我們概略的了解一下都有什麼解決方案,後續我們會在深入探討。
關於之前我們已經使用過了 InheritedWidget
。前面我們提到 InheritedWidget
是一個特殊的組件,用在其後代組件之間可以共享資料,在 Navigator
和 ScaffoldMessenger
例如 Navigator.of(context)
這樣的用法,可以搜尋上層特定型別的物件。這樣的方式也可以用在狀態上,讓我們可以搜尋環境中特定的狀態。特別是可以減少參數層層傳遞方式的缺點,因為中間的組件很可能不需要關注一些狀態。
為了更好理解 InheritedWidget 的概念,我們需要先理解 Flutter 中組件的樹狀結構以及階層。Widget Tree 組件樹狀結構代表了 Flutter 應用程式組件的組合,在這個結構中父子關係定義了 Widget 的層次。父層 Widget 下有子 Widget ,父層組件的變化可能會影響後代組件。InheritedWidget 的重點就在於結構下所有的組件所「繼承」例如下面實作範例:
import 'package:flutter/material.dart';
class CounterData extends InheritedWidget {
final int count;
final Function() increment;
CounterData({
Key? key,
required this.count,
required this.increment,
required Widget child,
}) : super(key: key, child: child);
static CounterData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterData>()!;
}
@override
bool updateShouldNotify(CounterData oldWidget) {
return count != oldWidget.count;
}
}
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return CounterData(
count: _count,
increment: _increment,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: ${CounterData.of(context).count}'),
ElevatedButton(
onPressed: CounterData.of(context).increment,
child: Text('Increment'),
),
],
),
),
);
}
}
context.dependOnInheritedWidgetOfExactType
這個方法是 BuildContext
的一部分,它會在樹狀結構中向上搜尋最近的 CounterData
一旦找到就會建立關聯。而子組件要能夠存取 CounterData
的關鍵在於 build
方法中,我們類似於 React 的 Context Provider 一樣包在上層。
這種方式的優點就是原生支持適合小型應用。
Business Logic Component 業務邏輯元件使用串流 Stream 的方式來分享狀態。組件可以監聽選擇的狀態,並且在狀態變化時接受通知。這樣的方式非常契合 Flutter 宣告式的風格,一旦狀態改變,者組件會被通知並根據狀態自行更新。通過更新內部狀態觸發重新渲染。它的核心概念就是 Event 觸發狀態變化,BLoC 接收 Event 並輸出 State。
此外,這種設計模式也可監聽外部系統例如資料庫或者網路請求。BLoC 模式將業務邏輯從表現層中分離出來,使得程式碼模組化和可測試。但是 BLoC 也有一些缺點,例如引入了一些模版,學習曲線也比較陡峭。
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: Text('Increment'),
),
],
),
);
},
);
}
}
這個章節,我們只是簡單的概覽其實作大概如何。
而 Redux 和 BLoC 最大的差異就是 Redux 具有一個管理全部狀態的物件,而 BLoC 各自有自己負責的狀態。
在 Redux 中有三個主要的概念:
Redux 強調單一數據流和純函數,狀態更新的可預測性。大多數開發者是因為 React 學習這個概念。但是引入了更多的概念,對於簡單的應用來說可能過於複雜且隨著專案成長這樣的管理方式也會變得有點厚重。
若想深入理解可以參考從琅琊榜學 Redux。
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
// 定義 Action
enum Actions { Increment }
// 定義 Reducer
int counterReducer(int state, dynamic action) {
if (action == Actions.Increment) {
return state + 1;
}
return state;
}
// 使用 Redux 的 Widget
class CounterPage extends StatelessWidget {
final Store<int> store;
CounterPage({Key? key, required this.store}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: StoreConnector<int, int>(
converter: (store) => store.state,
builder: (context, count) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => store.dispatch(Actions.Increment),
child: Text('Increment'),
),
],
),
);
},
),
);
}
}
Riverpod 是 Flutter 目前最流行的狀態管理方案之一。結合了 BLoC、Provider 和 InheritedWidget 的優點,提供了一種簡潔而靈活的方式來管理狀態。
Riverpod 的核心概念是 Provider,代表了一個資料源或狀態。可以是一個值,也可以是一個創建狀態的函數。我們通過 Provider 來讀取和訂閱狀態。當狀態發生變化時,依賴該狀態的組件會自動重建。
使用 Riverpod,我們可以將業務邏輯封裝在獨立的 Provider 中,然後在組件中使用 ref.watch()
來獲取狀態,使用 ref.read()
來呼叫狀態的方法。程式碼簡潔易懂。
加上 Flutter 官方的一些範例專案也開始使用 Riverpod,可見其潛力。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 定義一個整數類型的 Provider
final counterProvider = StateProvider((ref) => 0);
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Demo',
home: HomePage(),
);
}
}
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 使用 watch 讀取狀態
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Counter')),
body: Center(
child: Text(
'Count: $count',
style: TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
);
}
}
除了上面介紹的選項,其他還有下面選項可以參考:
選擇狀態管理方案時,需要考慮以下因素:
對於小型項目,使用 InheritedWidget 或簡單的 Provider 可能就足夠了。對於中大型項目,BLoC、Redux 或 Riverpod 可能更適合。
最重要的是,選擇一個你和你的團隊都感到舒適的方案,並且能夠隨著項目的增長而擴展。Flutter 提供了多種強大的狀態管理方案,每種方案都有其優缺點。理解這些方案的工作原理和適用場景,將幫助您為項目選擇最合適的解決方案。隨著經驗的積累,您將能夠更好地評估和選擇適合特定需求的狀態管理策略。
在之後「深入狀態管理」的章節,我們將更進一步介紹讓您可以具備更多的理解。