0%

Flutter 筆記(二) - 狀態管理

前言

本篇記錄了 Flutter 眾多概念中較為複雜的一個領域,那就是 狀態管理 State Management 的部分。然而之所以複雜的原因就是它並沒有一套實作的標準。例如 Google 自行發表的 BLoC PatternRx 系列以及 Flutter 原生的狀態管理,都會在這篇一一做出差異比較與使用時機。

Flutter 完整開發架構

Stream 初識

  • 比喻

  • 巧克力工廠的特色

    1. 工廠收到一個 ‘order’, 做一些處理後, 將蛋糕從工廠的另外一端吐出
    2. 當有第一個訂單以前工廠是尚未建立的
    3. 工廠花大多數的時間在等訂單進來
    4. 必須有人(order taker :) 等待新的訂單. 這是進入工廠的入口
    5. 當蛋糕做好的時候必須有人來拿

來一段程式碼 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import 'dart:async';

// 代表比喻中工廠另一端所生產出來的蛋糕
class Cake{}

// 代表比喻中裡面的訂單(消費者拿給order taker)
class Order{
String type; // 什麼樣類型的蛋糕
Order(this.type);
}

void main() {
//1.自動生成sink和stream物件並指派給參考
var controller = StreamController();

//2.消費者有一個香蕉蛋糕的訂單
final order = Order('banana');

//5.
final baker = StreamTransformer.fromHandlers(
//第一個參數為用來傳入order inspector所抽取的data event
//第二個參數為sink, 用來將data event放回stream
handleData: (cakeType, sink) {
//如果抽取的資料和baker可以製作的元素型態是一致的就將
//蛋糕(data event)放回sink當中變回stream
if (cakeType == 'chocolate') {
//這裡的sink和stream.sink的sink是不同的, 我們不會
//從頭開始處理stream
sink.add(Cake());
} else {
sink.addError('I cant bake that type!!!');
}
},
);

//3.將訂單加入到sink裡面去 (order taker)
controller.sink.add(order);

//stream 代表工廠
//map 將所有加入到stream裡面的元素(order)一一取出來
//匿名函式 代表order inspector
//他不關心進來的元素(訂單)是啥, 他抽取出她想要的
//屬性(可以單一或是多個)或是改變(transform)進來的元素
//這裡是抽取訂單裡的type屬性(String)並將它傳給baker
controller.stream
//4.
.map((order) => order.type)
//6. 將event塞回stream
.transform(baker)
//7. 代表pickup offfice, 將stream裡面的元素取出
.listen( //和stream是做綁定的
// 正確的data event這裡就是蛋糕
(cake) => print('Heres your cake $cake'),
// 不正確的資料 error event
onError: (err) => print(err)
);
}

I cant bake your type!!!

什麼是 StreamController

  1. StreamController 有其中兩個屬性 sinkstream

  2. StreamController 被創建時, 會自動生成該兩個物件並指派

  3. sink 有能力將data傳入stream裡面

  4. 不能直接將資料傳給stream, 需要透過sink(order taker)

總覽

  • Order Taker : StreamControllersink.add 函數
    • 功能 : 將加入的 event 提供給 stream 去處理
  • Order Inspector : stream 的map函數
    • 功能 : 檢視每一個進來 stream 的 event , 並加以處理(抽取或轉換)然後返回
  • Baker : StreamTransformer

    • 功能 : 處理每一個進來的處理過的資料, 然後塞回stream
  • Pickup Office : listen函式


map function 和 StreamTransformer 的差別
相同 :

  • 都是用來處理 stream 裡面的 event

不同 :

  • map function : 一對一的data進出
  • StreamTransformer : 一對多的資料進出(輸出多個資料)
    • 更加彈性
    • e.g. 可以呼叫多次 sink.add , 將 event data 加入到 stream

Why Stream ?


緣起

Stream 的處理方式看似複雜, 但在 flutter 或 dart 裡面經常會提供觸發的事件資料(透過 sink 放入 stream 裡面), 而無需自己製作. 我們只需創建另一個 stream


來看一段程式碼 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'dart:html';

void main() {

final ButtonElement button = querySelector('button');

// onClick方法會返回一個stream(在此之前, flutter/dart會幫我們
//處理透過sink將事件塞入stream的過程)在每一次我們按按鈕時會射出一個事件
button.onClick
.timeout( // 會創建一個新的stream(手動)
Duration(seconds: 1), // 超過一秒中沒有新的事件
// 將事件加入到sink當中(sink 之後會交給新的stream)
onTimeout: (sink) => sink.addError('You lost!!!')
)
// .map(event) => 'sucess') 可以加入這行做額外的轉換
.listen(
(event) {},
onError: (err) => print(err)
);

}

Stream ‘take’ and ‘where’ function ?

  • take(int) - 當事件進入到 stream 的次數高達設定的數字時, stream 會被終止且發出結束事件(onDone)
  • where((){})
    • 事實上他就是過濾器, 專門過濾不符合匿名韓式裡寫的條件.
      • true : 保留該 event
      • false : 忽略該 event
    • 創建新的 stream 並將 event 塞入

猜字遊戲 : 當猜到第四次仍沒有猜中就不可以再猜了XXD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 'dart:html';

void main() {

final ButtonElement button = querySelector('button');

final InputElement input = querySelector('input');

button.onClick
.take(4) //限制stream可以流進event的次數
// event在這裡就只是表達按下按鈕的事件
// 如果正確就將event傳下去, 反之就忽略該事件
.where((event) {
var test = input.value == 'banana';
input.value = '';
return test;
}) //創建一個stream
.listen(
(event) => print('You win!!!'),
// 當原始的stream已經傳入四次的event, 就終止該stream並傳遞結束事件
onDone: () => print('Nope, bad guess')
);

}

另外一個例子更可以展現 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import 'dart:html';
import 'dart:async';

void main() {
final InputElement input = querySelector('input');

final DivElement div = querySelector('div');

// div.innerHtml = 'Enter a valid email';

final validator = StreamTransformer.fromHandlers(
handleData: (email, sink) {
if (email.contains('@')) {
sink.add(email);
} else {
sink.addError('Enter a valid email');
}
}
);

input.onInput
//不管input event這個物件, 只抽取其中需要的資訊(email資料)
.map((dynamic event) => event.target.value)
//判斷event是否為正確的email, 將對或錯的結果分別寫入sink中
.transform(validator)
.listen(
(event) => div.innerHtml = '' ,
onError: (err) => div.innerHtml = err
);

}

BLOC’s vs Stateful widgets


前情提要


場景 : BLOC 對於資訊共享於不同 widget 之間做出很好的處理

  • 整個 app 的 widget 結構
  • 當 LoginScreen 和 UserPreferences 都要共享下圖紅色圈起來的資料, 如果單純使用 stateful widgets 來寫會很複雜. 但是 bloc 可以輕易的解決 widget 和 widget 之間共享資料的難度

BLOC : Business logic component

  1. 用來儲存所有資料(data)和狀態(state)在一個實體當中

  2. bloc component 並不需要app裡頭哪一個特定的widget做綁定

  3. widget 是分離的, 並不會在 widget tree 的結構當中


streams 在 bloc 裡頭的意義

下圖是bloc和stream一起運作的概念 :

  • bloc 可以看作是物流中心, 用來管理 app 所有數據和狀態的
  • stream 可以看作是各式各樣需要物流的產品, 在哪裡得到資訊? 將資訊送往哪裡?

下圖是app的結構 :

Bloc 的應用

TextFieldbloc 互相作用 :

bloc 擁有兩個屬性 :

  1. Email StreamController
  2. Password StreamController

對於如何應用 bloc 到 app 裏頭, 在 state management 尚未有一個統一的說法 :

1. Single Global Instance : 適合用於在較小的項目當中

  • (中間) appwidget 結構

  • (右邊) bloc.dart 檔按裡面有一個 Bloc 類以及一個 bloc 實體

  • 在整個 app 中只有一份 (final)bloc 實體, 所以裡面所有的變數和狀態是共享的在所有使用到的 widget 當中

  • 開發上會非常方便且簡易但是會難以控制

2. Scoped Instance : 適合在更大型且更複雜的項目底下開發

  • 不把 bloc 的實體放在 bloc.dart 檔案下來當作全域變數
  • 下面的 CustomWidget1~3 是任意的
  • 將 bloc 放在需要的 widget 裡面, 而其子widget都可以存取到該 bloc
  • 不需將 bloc 實體暴露給整個 app 做使用
  • 這樣開發明顯會複雜許多但會得到更好的控制

StreamBuilder - consuming stream

  • 定義 : 是一個可以將使用者定義的物件的串流轉成widget的widget

  • 兩個參數 :

    1. stream : 要監測事件的 stream
    2. builder : 將串流裡的元素轉換成 widget (builder function)
      • 觸發條件為當 stream 改變

StreamBuilder 概述

  1. Flutter 所提供的一個 widget
  2. 將需要的 StreamStreamBuilder 做綁定
  3. StreamBuiler 偵測到一個新的事件從 stream 進來, 就調用 builder function (返回新的widget )並且重新渲染

通常應用來更新(渲染) widget 取代原有使用 statefulWidget 的 setState()


context

概念

  1. 每一個widget都有自己獨立的 context

  2. context 是一種標示符, 將該 widget 放置在正確的 widget 階層中(如上圖)

  3. 每一個 widget 都可以在 widget 階層中向上尋找任意的 widget

  4. 每一個 widget 都知道自己的 context 以及以上的 context

  5. context 就像鏈結一樣, 該 context 知道他的上級而上級的 context 又知道他的上級…

來看一段程式碼 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:flutter/material.dart';
import 'bloc.dart';

class Provider extends InheritedWidget {

final bloc = Bloc();

bool updateShouldNotify(_) => true;

//該方法簡單來說就是讓該widget底下的任意widget都可以連結到Provider
//, 來對Bloc做存取
static Bloc of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Provider) as Provider).bloc;
}

}

程式碼概述 :

  1. 在該 widget(Provider) 底下 widget tree 的任意 widget , 都可以藉由該 of(BuilderContext) 將自己的 context 傳入

  2. context 有一個 inheritFromWidgetOfExactType(Type typeTarget) 方法, 用來向上尋找 widget 階層中類型為 typeTarget 的 widget.

  3. 找到後會返回一個 InheritedWidget 類型的 widget(物件) 並將他向下轉型為 typeTarget

How to merge two stream ?

Case : 監聽兩個 stream 當達到一定的條件作出適當的響應

solution to merge stream

有三種解決的辦法 :

下面有對RxDart有做詳細的解說

RxDart

Rx家族介紹

  1. Rx 全名叫做 ReactiveX

  2. 有多種不同的版本套件

  3. 依據不同的程式語言去量身定做

  4. 架構在不同版本間基本上都是一樣

  5. 不同的術語在 RxDart

    • Stream 會變成 Observable
      • A wrapper class that extends to Stream
    • StreamController會變成Subject
      • A wrapper class that extends to StreamController



知識補給站
BehaviorSubjectStreamController 的一種.

  1. 默認是 Broadcast controller , 所以他的 stream 是可以被監聽多次的

  2. 會紀錄最新加入 sink 的 event , 以便之後可以做存去使用.

  3. Stream 的處理方面加入許多延伸的功能

學習連結

完全RxDart教學手冊


回到正題 => 我們將用RxDart的 combineLatest2() 方法讓兩個 stream 做出合併處理

combineLatest2() 的機制圖解

  • 觀念圖示連結
    • Observale 直到要合併的兩個 stream 都各至少發出一個 event 才會發出 event

Bloc 總結