Flutter 在一開始其實就提供了一種狀態管理方式,StatefulWidget
,然而它僅適合用於在單個Widget 內部維護其狀態,但是專案開發時,可能需要不同的頁面共享變數的狀態,且當這個狀態發生改變時,所有依賴這個狀態的 UI 都需要隨之發生改變,此時StatefulWidget
就不太適合,雖然可以透過設置callback
在多個widget 之間傳遞狀態,但是當專案複雜度一深,很容易大大增加我們程式碼的耦合度
可參考邦友寫的 高內聚與低耦合
為了解決此問題,官方提供了一些解決方案,網上有各種方式的介紹以及比較,這邊就先以官方推薦的為主來介紹,而之前官方推薦的是 Bloc ,但去年改推薦Provider,我們之後將針對這兩種做介紹
首先,因為Bloc 將會大量使用到Stream
,我們需要先為各位介紹Stream
流 ( Stream ),是一系列非同步的資料佇列 ( 先進先出 (FIFO) ),例如說:介面上使用者觸發的動作 (像是點餐) 等等,可以使用await for
( 非同步的for 迴圈 ) 或 listen()
方法來監聽Stream,來處理要接收到的資料
Stream
和Future
都是Dart中非同步程式設計的核心內容,Future
為一次性取得非同步的資料,Stream
為取得多次非同步的資料
以下我們介紹幾種常見的建立 Stream 方式
轉換 Steam
假如已經有了一個Stream,但是它的值不是我們想要的,Stream提供了map()
、where()
、expand()
,以及take()
方法,能夠輕鬆將已有的Stream 事件轉化為新的事件
例如:把整數事件流轉成字串
import 'dart:async';
Future<String> _intStreamToStringStream(Stream<int> stream) async {
return await stream.map((event) => event.toString()).join(',');
}
Stream<int> testStream() async* {
for (int i = 1; i <= 5; i++) {
yield i;
}
}
main() async {
var test = testStream();
dynamic result = await _intStreamToStringStream(test);
print('intStreamToStringStream result:$result');
//印出 intStreamToStringStream result:1,2,3,4,5
}
使用StreamController
創建
add()
將資料加入裡listen()
方法來一直監聽此streamimport 'dart:async';
main() {
StreamController anyController = StreamController(); //未定義類型,任意型態皆可的Stream
anyController.sink.add(123);
anyController.sink.add("abc");
anyController.sink.add(3.14);
StreamController<int> intController = StreamController(); //指定整數型態的Stream
intController.sink.add(123);
//透過 listen(),監聽一個Stream,當有事件發出時,即會觸發listener
anyController.stream.listen((data) => print("anyController:$data"));
intController.stream.listen((data) => print("intController:$data"));
}
/*印出
anyController:123
intController:123
anyController:abc
anyController:3.14
*/
使用async*
建立Stream
如果我們有一系列事件需要處理,這時候可以使用async* 以及 yield來生成一個Stream
範例:
商品皆為10元 的商店
import 'dart:async';
Future<int> getTotalPrice(Stream<int> stream) async {
var totalPrice = 0;
//透過 await for 使用由建構傳進來的Stream,能夠在此整數事件流中的每個事件到來的時候處理它,當迴圈結束時,函數將暫停,直到下一個事件到達或流完成為止,需搭配 async 使用
await for (var numbers in stream) {
print("數量:$numbers");
totalPrice = numbers * 10; //收到我們整數事件流的事件
}
return totalPrice;
}
/*
商品數量的Stream
async*:創建Stream 的一種方法
yield:在Stream 上發出事件
*/
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
main() async {
var count = countStream(5);
var totalPrice = await getTotalPrice(count);
print('總金額: $totalPrice');
}
/* 印出
數量:1
數量:2
數量:3
數量:4
數量:5
總金額: 50
*/
在某些情況下,流完成之前會發生錯誤;可能是網路從伺服器上取得文件時發生異常,或者創建事件的代碼存在錯誤等等,而流還可以傳遞錯誤事件,就像傳遞一般事件一樣 ( 大多數流將在出現第一個錯誤後就會停止 )
import 'dart:async';
Future<int> getTotalPrice(Stream<int> stream) async {
var totalPrice = 0;
try {
await for (var numbers in stream) {
print("數量:$numbers");
totalPrice = numbers * 10;
}
} catch (e) {
return -1;
}
return totalPrice;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
if (i == 7) {
throw new Exception(i);
} else {
yield i;
}
}
}
main() async {
var count = countStream(10);
var totalPrice = await getTotalPrice(count);
print('總金額: $totalPrice');
}
/*印出
數量:1
數量:2
數量:3
數量:4
數量:5
數量:6
總金額: -1
*/
Stream 有兩種
單一訂閱流最常見的流包含一系列事件,事件必須以正確的順序傳遞,並且不能丟失任何事件。像是在讀取文件或接收Web
請求時獲得的流,它在被監聽之前不會生成事件,並且在取消監聽後它會停止發送事件,這樣的流只能被訂閱監聽一次,即使在第一個訂閱被取消後,也不允許在單個訂閱流上進行兩次監聽
import 'dart:async';
main() {
StreamController controller = StreamController();
controller.stream.listen((data) => print(data));
controller.stream.listen((data) => print(data));
controller.sink.add("test");
}
//執行後出現異常:Exception: Bad state: Stream has already been listened to.
廣播流允許任意數量的監聽,且不管是否被監聽,它都會生成事件,所以之後才監聽的無法收到之前的事件,不過可以隨時開始監聽這樣的流
如果在Single-subscription Streams想要進行多次監聽,可以使用asBroadcastStream
在非廣播流上建立廣播流
import 'dart:async';
main() {
StreamController controller = StreamController();
Stream stream = controller.stream.asBroadcastStream();
stream.listen((data) => print(data));
stream.listen((data) => print(data));
controller.sink.add("test");
}
/*印出
test
test
*/
簡單介紹完Stream
,我們來介紹一下Bloc 吧
全名為Business Logic Component,使我們能把商業邏輯從UI 抽離,降低程式碼彼此之間的耦合性,讓Widget 只需要著重於UI,顯示處理後的結果畫面,這讓我們達成幾項目的:
使用
將bloc
、flutter_bloc
的包作為依賴項(dependencies) 添加到我们的pubspec.yaml
中
dependencies:
bloc: ^6.0.3
flutter_bloc: ^6.0.5
在要用到bloc 的地方再透過import 引入即可
Cubit 是Bloc 運作的基礎 ( Bloc 繼承了Cubit ),Cubit 是一種特殊類型的Stream
,可以用來管理任何類型的狀態。
Stream:
Cubit需要一個初始狀態,是在發出調用之前的狀態,可以通過狀態getter
取得cubit 的當前狀態,並可以通過調用帶有新狀態的emit
方法來更新cubit 的狀態
當我們調用事先定義的函數,cubit 狀態就會開始更新,這些函數可以使用emit
方法輸出新狀態,而每個狀態更改都會調用onChange
方法 (其中包含當前狀態和下一個狀態)
實作:
設計一個計數器,初始值為0,並有加法方法,能對值做修改
我們在Android Studio 新建一個全新的專案,記得在pubspec.yaml
添加bloc
的依賴
建立 CounterCubit 類別,用來管理計數器的 int 狀態:
import 'package:bloc/bloc.dart';
class CounterCubit extends Cubit<int> { // Cubit<int>,管理的狀態為 int
//初始值設為0
CounterCubit() : super(0);
// 設計一個加法,當此方法被呼叫時,現在的狀態 (int) 就要加一,並透過emit方法更新為新的狀態
void increase() => emit(state + 1);
}
我們來用用看
main.dart
:
import 'CounterCubit.dart';
void main() {
//建立一個計數器物件
CounterCubit cubit = CounterCubit();
// 印出現在計數器的狀態 (CounterCubit 的狀態為 int)
print(cubit.state); // 印出 0
// 呼叫計數器的加法,該方法會去將狀態改變 (state + 1)
cubit.increase();
print(cubit.state); // 印出 1
// 使用完把計數器 Cubit 物件關閉
cubit.close();
}
onChange
:可以被覆寫( override ),來處理當Cubit
狀態改變時要做的行為
onError
:可以被覆寫( override ),來處理當Cubit
拋出異常時要做的行為
import 'package:bloc/bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increase() {
print("increase");
emit(state + 1);
}
void makeError() {
addError(new Exception("test"));
}
@override
void onChange(Change<int> change) {
print("onChange:$change");
super.onChange(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('onError:$error, $stackTrace');
super.onError(error, stackTrace);
}
}
main.dart
:
import 'CounterCubit.dart';
void main() {
CounterCubit cubit = CounterCubit();
print(cubit.state); // 印出 0
cubit.increase();
/* 印出
increase
onChange:Change { currentState: 0, nextState: 1 }
*/
print(cubit.state); // 印出 1
cubit.makeError(); // 印出 onError:Exception: test, null,並拋出異常
}
若想對每個Cubit
發生狀態改變或拋出異常時,都要客製化行為的話,可以使用BlocObserver
建立一個MyBlocObserver
:
import 'package:bloc/bloc.dart';
class MyBlocObserver extends BlocObserver {
@override
void onChange(Cubit cubit, Change change) {
print("MyBlocObserver onChange:$cubit, $change");
super.onChange(cubit, change);
}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
print('MyBlocObserver onError:$cubit, $error, $stackTrace');
super.onError(cubit, error, stackTrace);
}
}
main.dart
:
import 'package:bloc/bloc.dart';
import 'CounterCubit.dart';
import 'MyBlocObserver.dart';
void main() {
// 設定我們的 observer
Bloc.observer = MyBlocObserver();
CounterCubit cubit = CounterCubit();
print(cubit.state); // 印出 0
cubit.increase();
/* 印出
increase
onChange:Change { currentState: 0, nextState: 1 }
MyBlocObserver onChange:Instance of 'CounterCubit', Change { currentState: 0, nextState: 1 }
*/
print(cubit.state); // 印出 1
cubit.makeError();
/* 印出
onError:Exception: test, null
MyBlocObserver onError:Instance of 'CounterCubit', Exception: test, null
並拋出異常
*/
}
今天我們先介紹到這,下一篇進一步介紹 Bloc 的觀念以及使用