#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
  • ProviderReferenceref)是 Riverpod 提供的一個對象,用來與其他 Providers 進行交互或管理生命周期。儘管在這個例子中沒有直接使用 ref,它在更複雜的應用場景中非常有用。

寫成完整函數的樣子

如果我們不使用箭頭函數,而是用完整的函數語法來寫,會是這樣的:

final peopleCountProvider = StateProvider<int>((ref) {
return 0;
});

--

--