#82 Flutter 04-Riverpod 狀態管理,StateProvider 實作-路口觀察計數 App
Flutter 提供多種狀態管理方案,其中最基礎的是 Provider。Riverpod 則是由 Provider 的作者 Rémi Rousselet 所打造的強化版。本文將介紹如何使用 Riverpod 中的 StateProvider 來建立一個簡單的計數應用程式,用於計算十字路口上的「行人」、「交通工具」和「動物」的計數。
操作 GIF:
螢幕截圖
步驟 1:建立專案
首先,建立一個新的 Flutter 專案並命名為 crossroad_counter
。
步驟 2:安裝 Riverpod
在 pubspec.yaml
文件中添加 flutter_riverpod
依賴:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1 # 確保這行有最新的版本號
版本的資訊可以看 pub.dev 的 flutter_riverpod 文件
然後在終端(terminal)中執行以下指令來安裝依賴:
flutter pub get
步驟 3:定義 StateProvider
Riverpod 幫助我們管理應用程序中的數據。這樣,我們可以在不同的地方使用這些數據,並且當數據改變時,自動更新顯示。
在 main.dart
中定義三個不同的 StateProvider
,分別管理 "人"、"交通工具" 和 "動物" 的計數狀態:
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 這裡定義三個不同的 StateProvider 來管理不同類型的計數
final peopleCountProvider = StateProvider<int>((ref) => 0);
final vehiclesCountProvider = StateProvider<int>((ref) => 0);
final animalsCountProvider = StateProvider<int>((ref) => 0);
步驟 4:構建 UI 並顯示計數
接著,我們需要編寫一個 Widget
來顯示計數結果。因為我們會在 build
方法中使用 WidgetRef
參數,所以需要使用 ConsumerWidget
:
使用 WidgetRef.watch
來監聽變數的狀態:
然後寫一個 Widget 我們把 計算的結果顯示出來,因為我們會需要在 build 裡多一個 WidgetRef 參數,所以 extends 先改為 ConsumerWidget
使用 WidgetRef.watch 來監聽變數的狀態
class TotalCountDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final peopleCount = ref.watch(peopleCountProvider);
final vehiclesCount = ref.watch(vehiclesCountProvider);
final animalsCount = ref.watch(animalsCountProvider);
然後使用 $
來綁定變數顯示在 Text
中:
Text('人 總數: $peopleCount', style: TextStyle(fontSize: 18)),
Text('交通工具 總數: $vehiclesCount', style: TextStyle(fontSize: 18)),
Text('動物 總數: $animalsCount', style: TextStyle(fontSize: 18)),
例如:
class TotalCountDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final peopleCount = ref.watch(peopleCountProvider);
final vehiclesCount = ref.watch(vehiclesCountProvider);
final animalsCount = ref.watch(animalsCountProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('行人 總數: $peopleCount', style: TextStyle(fontSize: 18)),
Text('交通工具 總數: $vehiclesCount', style: TextStyle(fontSize: 18)),
Text('動物 總數: $animalsCount', style: TextStyle(fontSize: 18)),
],
);
}
}
預期看到的樣子:
步驟 5:修改 main 函數
將 main
函數修改為如下,使其包含 ProviderScope
:
void main() {
runApp(const MyApp());
}
改為
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
先在 MyApp 中加入 TotalCountDisplay() 來看看
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text('十字路口觀察'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TotalCountDisplay(),
],
),
),
);
}
}
步驟 6:添加按鈕並更新計數
我們加入三個 ElevatedButton
,點擊時將對應的計數變數加1:
ElevatedButton(
onPressed: () {
ref.watch(peopleCountProvider.notifier).state++;
},
child: Text('行人 +1'),
),
ElevatedButton(
onPressed: () {
ref.watch(vehiclesCountProvider.notifier).state++;
},
child: Text('交通工具 +1'),
),
ElevatedButton(
onPressed: () {
ref.watch(animalsCountProvider.notifier).state++;
},
child: Text('動物 +1'),
),
完整程式:
//main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final peopleCountProvider = StateProvider<int>((ref) => 0);
final vehiclesCountProvider = StateProvider<int>((ref) => 0);
final animalsCountProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(
ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class TotalCountDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final peopleCount = ref.watch(peopleCountProvider);
final vehiclesCount = ref.watch(vehiclesCountProvider);
final animalsCount = ref.watch(animalsCountProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('行人 總數: $peopleCount', style: TextStyle(fontSize: 18)),
Text('交通工具 總數: $vehiclesCount', style: TextStyle(fontSize: 18)),
Text('動物 總數: $animalsCount', style: TextStyle(fontSize: 18)),
],
);
}
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text('十字路口觀察'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TotalCountDisplay(),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(
onPressed: () {
ref.watch(peopleCountProvider.notifier).state++;
},
child: Text('行人 +1'),
),
ElevatedButton(
onPressed: () {
ref.watch(vehiclesCountProvider.notifier).state++;
},
child: Text('交通工具 +1'),
),
ElevatedButton(
onPressed: () {
ref.watch(animalsCountProvider.notifier).state++;
},
child: Text('動物 +1'),
),
],
),
],
),
),
);
}
}
步驟 6:優化按鈕部分
由於三個按鈕的程式碼很相似,我們可以將它們提取到一個單獨的 CountButton
類中:按鍵要處理二個事情,1是顯示名稱,2是將變數增加值,所以我們需要從外部傳入2個變數:label 與 StateProvider
final String label;
final StateProvider<int> countProvider;
再把 ElevatedButton 程式替換。
class CountButton extends ConsumerWidget {
final String label;
final StateProvider<int> countProvider;
CountButton(this.label, this.countProvider);
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
ref.watch(countProvider.notifier).state++;
},
child: Text('$label +1'),
);
}
}
然後在主界面中使用這個 CountButton
:
因為 MyApp 裡面已經不需要 WidgetRef 了,所以 MyApp 的 extends 要再改回 StatelessWidget
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Road Count'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TotalCountDisplay(),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
CountButton('行人', peopleCountProvider),
CountButton('交通工具', vehiclesCountProvider),
CountButton('動物', animalsCountProvider),
],
),
],
),
),
);
}
}
步驟 7:優化變數部分
如果變數很多的話,是不是要寫很多 StateProvider, 可以考慮組織這些狀態變量。這樣做可以增加代碼的結構性和可讀性,同時減少重複代碼。以下是幾種有效的方法來組織和管理這些狀態變量:
使用類(Class)
你可以創建一個 Dart 類來表示所有需要管理的狀態。這個類可以包含各種狀態變量以及它們的初始化值。
class RoadCount {
final StateProvider<int> peopleCountProvider;
final StateProvider<int> vehiclesCountProvider;
final StateProvider<int> animalsCountProvider;
RoadCount()
: peopleCountProvider = StateProvider<int>((ref) => 0),
vehiclesCountProvider = StateProvider<int>((ref) => 0),
animalsCountProvider = StateProvider<int>((ref) => 0);
}
然後,你可以在需要使用這些狀態的地方實例化這個類:
final roadCount = RoadCount();
// 使用方式
ref.watch(roadCount.peopleCountProvider.notifier).state++;
- 在 Flutter 中,通常使用類來定義自定義的 Widget 或模型(Model),它們用於構建 UI 界面、處理業務邏輯、管理狀態等。
- 類可以是 StatefulWidget 或 StatelessWidget 的子類,具體取決於它們是否需要維護狀態。
- 類中可以包含其他類、函數、變數等,用於實現特定功能和邏輯。
- 類的作用範圍限於定義它們的 Dart 文件中,或者可以通過封裝和繼承在其他文件中重複使用。
使用 Map
另一種方法是使用 Dart 的 Map 來存儲狀態提供者。這樣可以更動態地管理和訪問不同的狀態變量。
final stateProviders = {
'people': StateProvider<int>((ref) => 0),
'vehicles': StateProvider<int>((ref) => 0),
'animals': StateProvider<int>((ref) => 0),
};
// 使用方式
ref.watch(stateProviders['people']!.notifier).state++;
使用 Provider Container
如果狀態變量屬於同一個概念性單元,你可以考慮將它們包裝在一個提供者容器(Provider Container)中。這種方式將類似的狀態分組在一起,便於管理。
final roadCountsProvider = Provider((ref) => RoadCounts());
class RoadCounts {
final StateProvider<int> peopleCountProvider;
final StateProvider<int> vehiclesCountProvider;
final StateProvider<int> animalsCountProvider;
RoadCounts()
: peopleCountProvider = StateProvider<int>((ref) => 0),
vehiclesCountProvider = StateProvider<int>((ref) => 0),
animalsCountProvider = StateProvider<int>((ref) => 0);
}
// 使用方式
final roadCounts = ref.watch(roadCountsProvider);
ref.watch(roadCounts.peopleCountProvider.notifier).state++;
- 在 Riverpod 或 Provider 中,Provider Container(如 ProviderScope)是一種管理和提供依賴注入的機制。
- Provider Container 提供了一個範圍,允許您在其中定義和管理不同類型的 Provider(如 StateProvider、FutureProvider 等)。
- 它提供了全局訪問依賴項目的方法,通常用於整個應用程序,使得在不同的 Widget 中能夠訪問和使用相同的狀態和數據。
- 通過 Provider Container,您可以在整個應用程序中共享和管理狀態,並透過 Provider 來實現狀態管理,例如通過 StateProvider 來管理局部狀態。
使用類(Class)跟使用 Provider Container 的差別
類主要用於定義 UI 和業務邏輯,而 Provider Container 主要用於管理和共享狀態。它們各自解決不同層次的問題,但在大型應用程序中往往需要結合使用,以實現更好的代碼組織和模組化。
改用 Class 的方式來改寫
那我們使用 Class 的方式來改寫一下
完整程式碼
最後,完整的 main.dart
如下:
//main.dart
//main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final roadCount = RoadCount();
class RoadCount {
final StateProvider<int> peopleCountProvider;
final StateProvider<int> vehiclesCountProvider;
final StateProvider<int> animalsCountProvider;
RoadCount()
: peopleCountProvider = StateProvider<int>((ref) => 0),
vehiclesCountProvider = StateProvider<int>((ref) => 0),
animalsCountProvider = StateProvider<int>((ref) => 0);
}
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('十字路口觀察'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TotalCountDisplay(),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
CountButton('行人', roadCount.peopleCountProvider),
CountButton('交通工具', roadCount.vehiclesCountProvider),
CountButton('動物', roadCount.animalsCountProvider),
],
),
],
),
),
);
}
}
class CountButton extends ConsumerWidget {
final String label;
final StateProvider<int> countProvider;
const CountButton(this.label, this.countProvider, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// Use stateController to update the state
ref.watch(countProvider.notifier).state++;
},
child: Text('$label +1'),
);
}
}
class TotalCountDisplay extends ConsumerWidget {
const TotalCountDisplay({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final peopleCount = ref.watch(roadCount.peopleCountProvider);
final vehiclesCount = ref.watch(roadCount.vehiclesCountProvider);
final animalsCount = ref.watch(roadCount.animalsCountProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('行人 總數: $peopleCount', style: const TextStyle(fontSize: 18)),
Text('交通工具 總數: $vehiclesCount', style: const TextStyle(fontSize: 18)),
Text('動物 總數: $animalsCount', style: const TextStyle(fontSize: 18)),
],
);
}
}
改用 Provider Container 的方式來改寫
那我們使用 Provider Container 的方式來改寫一下
完整程式碼
最後,完整的 main.dart
如下:
//main.dart
//main.dart
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final roadCountsProvider = Provider((ref) => RoadCounts());
class RoadCounts {
final StateProvider<int> peopleCountProvider;
final StateProvider<int> vehiclesCountProvider;
final StateProvider<int> animalsCountProvider;
RoadCounts()
: peopleCountProvider = StateProvider<int>((ref) => 0),
vehiclesCountProvider = StateProvider<int>((ref) => 0),
animalsCountProvider = StateProvider<int>((ref) => 0);
}
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('十字路口觀察'),
),
body: Center(
child: Consumer(builder: (context, ref, _) {
// Access the instance of RoadCounts provided by roadCountsProvider
final roadCounts = ref.read(roadCountsProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const TotalCountDisplay(),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
CountButton('人', roadCounts.peopleCountProvider),
CountButton('交通工具', roadCounts.vehiclesCountProvider),
CountButton('動物', roadCounts.animalsCountProvider),
],
),
],
);
}),
),
);
}
}
class CountButton extends ConsumerWidget {
final String label;
final StateProvider<int> countProvider;
const CountButton(this.label, this.countProvider, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
ref.watch(countProvider.notifier).state++;
},
child: Text('$label +1'),
);
}
}
class TotalCountDisplay extends ConsumerWidget {
const TotalCountDisplay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final roadCounts = ref.read(roadCountsProvider);
final peopleCount = ref.watch(roadCounts.peopleCountProvider);
final vehiclesCount = ref.watch(roadCounts.vehiclesCountProvider);
final animalsCount = ref.watch(roadCounts.animalsCountProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('人 總數: $peopleCount', style: const TextStyle(fontSize: 18)),
Text('交通工具 總數: $vehiclesCount', style: const TextStyle(fontSize: 18)),
Text('動物 總數: $animalsCount', style: const TextStyle(fontSize: 18)),
],
);
}
}
這樣的結構不僅清晰,還可以輕鬆擴展以應對更多類型的計數需求。
重點回顧:
重點回顧一下 Riverpod 的核心使用方法,包括定義變數、修改變數和顯示變數。以下是重點說明:
定義提供者變數
final peopleCountProvider = StateProvider<int>((ref) => 0);
修改變數
Widget build(BuildContext context, WidgetRef ref) {
...
ElevatedButton(
onPressed: () {
ref.watch(peopleCountProvider.notifier).state++;
},
child: Text('人 +1'),
),
讀取及顯示變數
使用 Riverpod,如果您想從多個提供者讀取值,您可以簡單地編寫多個ref.watch
語句,如下所示:
Widget build(BuildContext context, WidgetRef ref) {
final peopleCount = ref.watch(peopleCountProvider);
final vehiclesCount = ref.watch(vehiclesCountProvider);
final animalsCount = ref.watch(animalsCountProvider);
...
Text('人 總數: $peopleCount', style: TextStyle(fontSize: 18)),
在這個示例中,我們使用 ref.watch(peopleCountProvider)
來監聽 peopleCountProvider
的狀態變化,並使用 ref.read(peopleCountProvider.notifier).state++
來更新狀態。每次按下按鈕,peopleCount
的值都會加 1,並且 UI 會自動更新以反映這一變化。
參考:
Riverpod is a reactive caching and data-binding framework。在官網第一眼看到的說明,有發現沒有 State Management 嗎,其實它本身不是狀態管理框架,而是進行響應式緩存以及數據綁定,不是以管理狀態為主軸,但是它有這個能力。
Riverpod 可以作為狀態管理者,但很明顯地,它可以做的事更多。大家很常將它認定為狀態管理其實作者也無奈呀。
StateProvider
final peopleCountProvider = StateProvider<int>((ref) => 0);
StateProvider<int>
:
StateProvider
是一個特殊的 Provider,它提供了一個可變的狀態,這個狀態可以是任何類型。在這個例子中,我們選擇了int
作為狀態的類型。
peopleCountProvider
:
- 我們使用
final
關鍵字來定義peopleCountProvider
,這意味著它是一個不可變的引用,但其內部的狀態是可變的。
(ref) => 0
:
- 這個部分定義了一個匿名函數(也叫 Lambda 表達式或箭頭函數),它接收一個
ProviderReference
並返回一個初始值0
。 ProviderReference
(ref
)是 Riverpod 提供的一個對象,用來與其他 Providers 進行交互或管理生命周期。儘管在這個例子中沒有直接使用ref
,它在更複雜的應用場景中非常有用。
寫成完整函數的樣子
如果我們不使用箭頭函數,而是用完整的函數語法來寫,會是這樣的:
final peopleCountProvider = StateProvider<int>((ref) {
return 0;
});