Slide 1

Slide 1 text

台北 & ⾼雄 來體驗 bloc ⼩⽅塊 的神奇魔法 Johnny Sung (宋岡諺)
 Full stack developer

Slide 2

Slide 2 text

⼤綱 •Bloc 介紹 •舉個例⼦寫寫看 •怎麼測試?

Slide 3

Slide 3 text

☕ 咖啡準備好了嗎?

Slide 4

Slide 4 text

https://www.redbubble.com/i/ipad-case/I-turn-co ff ee-into-code-by-DutchArt/23986463.MNKGF

Slide 5

Slide 5 text

BLoC stands for Business Logic Component. Bloc is a design pattern created by Google to help separate business logic from the presentation layer and enable a developer to reuse code more efficiently.

Slide 6

Slide 6 text

https://pub.dev/packages/ fl utter_bloc

Slide 7

Slide 7 text

Bloc 元件 •Cubit •Bloc •BlocBuilder

Slide 8

Slide 8 text

Bloc 元件 •BlocBuilder •BlocSelector •BlocProvider •MultiBlocProvider •BlocListener •MultiBlocListener •BlocConsumer •RepositoryProvider •MultiRepositoryProvider

Slide 9

Slide 9 text

Bloc

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

Cubit

Slide 13

Slide 13 text

https://www.getreligion.org/getreligion/2019/12/12/bible-trivia-time-for-hard-working-scribes-what-is-a-cubit-a-shekel-an-ephah cubit /kj’ubɪt/ 腕尺 (Noun.): an ancient unit of length based on the length of the forearm. 古時⼀種量度,⾃⼿肘⾄中指端,長約 18 英⼨ https://cdict.net/q/cubit

Slide 14

Slide 14 text

跟這個⼀點關係都沒有。

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

簡單來說, Cubit 算是 Bloc 的簡化版。

Slide 19

Slide 19 text

(1) (2)

Slide 20

Slide 20 text

發想步驟 •列出所有可能的 事件 (Events) •列出所有可能的 狀態 (States)

Slide 21

Slide 21 text

舉個例⼦ (\_/) ( •_•) />~ 🌰

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

// 抽象事件 abstract class MyBlocEvent {} // 抓成績單 class GetScoringDataEvent extends MyBlocEvent {}

Slide 26

Slide 26 text

// 抽象狀態 abstract class MyBlocState {} 
 // 微笑狀態 class SmileState extends MyBlocState {} // 哭泣狀態 class CryingState extends MyBlocState {} // 初始狀態 class InitState extends MyBlocState {} // 錯誤狀態 class ErrorState extends MyBlocState { Error error; ErrorState(this.error); }

Slide 27

Slide 27 text

import 'package:bloc/bloc.dart'; class MyBloc extends Bloc { MyBloc() : super(InitState()) { on((event, emit) { emit(SmileState()); }); } }

Slide 28

Slide 28 text

class ScoreRepository { Future getScore() async { await Future.delayed(const Duration(seconds: 1)); // 等待 1 秒,模擬網路延遲 var rand = Random(); var score = rand.nextInt(100); return score; } } 取成績單

Slide 29

Slide 29 text

import 'dart:convert'; import 'package:http/http.dart' as http; Future getScore() async { final response = await http.get( Uri.parse(constServerDomain + "/score"), headers: defaultHeaders()); final map = json.decode(response.body); return MyScoreResponseModel(map); } (未來可⽤ API 取成績單)

Slide 30

Slide 30 text

class MyBloc extends Bloc { late ScoreRepository _scoreRepo; MyBloc(MyBlocState initialState) : super(initialState) { _scoreRepo = ScoreRepository(); } @override Stream mapEventToState(MyBlocEvent event) async* { if (event is GetScoringDataEvent) { yield InitState(); try { int score = await _scoreRepo.getScore(); if (score >= 60) { yield SmileState(); } else { yield CryingState(); } } catch (e) { yield ErrorState(); } } } } bloc: ^7.2.0 flutter_bloc: ^7.2.0

Slide 31

Slide 31 text

import 'package:bloc/bloc.dart'; import 'package:bloc_demo/score_repository.dart'; import 'my_bloc_event.dart'; import 'my_bloc_state.dart'; class MyBloc extends Bloc { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on((event, emit) async { try { var value = await _scoreRepo.getScore(); if (value >= 60) { emit(SmileState()); } else { emit(CryingState()); } }catch(error){ emit(ErrorState()); } }); } } bloc: ^8.1.0 flutter_bloc: ^8.1.1

Slide 32

Slide 32 text

https://github.com/felangel/bloc/issues/2526

Slide 33

Slide 33 text

https://bloclibrary.dev/#/migration

Slide 34

Slide 34 text

import 'package:bloc/bloc.dart'; import 'package:bloc_demo/score_repository.dart'; import 'my_bloc_event.dart'; import 'my_bloc_state.dart'; class MyBloc extends Bloc { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on((event, emit) { _scoreRepo.getScore().then((value) { if (value >= 60) { emit(SmileState()); } else { emit(CryingState()); } }).catchError((error) { emit(ErrorState()); }); }); } } import 'package:bloc/bloc.dart'; import 'package:bloc_demo/score_repository.dart'; import 'my_bloc_event.dart'; import 'my_bloc_state.dart'; class MyBloc extends Bloc { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on((event, emit) async { try { var value = await _scoreRepo.getScore(); if (value >= 60) { emit(SmileState()); } else { emit(CryingState()); } }catch(error){ emit(ErrorState()); } }); } }

Slide 35

Slide 35 text

import 'package:bloc/bloc.dart'; import 'package:bloc_demo/score_repository.dart'; import 'my_bloc_event.dart'; import 'my_bloc_state.dart'; class MyBloc extends Bloc { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on((event, emit) async { try { var value = await _scoreRepo.getScore(); if (value >= 60) { emit(SmileState()); } else { emit(CryingState()); } }catch(error){ emit(ErrorState()); } }); } } Bloc 主邏輯

Slide 36

Slide 36 text

import 'package:bloc_demo/my_bloc.dart'; import 'package:bloc_demo/score_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { late MyBloc _myBloc; @override void initState() { super.initState(); _myBloc = MyBloc(scoreRepo: ScoreRepository()); } @override void dispose() { _myBloc.close(); super.dispose(); } @override Widget build(BuildContext context) { // 內容在下⼀⾴ } } @override Widget build(BuildContext context) { return BlocBuilder( bloc: _myBloc, builder: (context, state) { if (state is SmileState) { return const Text('😄'); } else if (state is CryingState) { return const Text('😭'); } else if (state is ErrorState) { return Text(state.error.toString()); } return const CircularProgressIndicator(); }); } ⽤ BlocBuilder 來顯⽰

Slide 37

Slide 37 text

換⼀個真實例⼦

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

abstract class DataBlocEvent {} class MyBlocLoadEvent extends DataBlocEvent {}

Slide 41

Slide 41 text

abstract class DataBlocState {} class DataBlocInitialState extends DataBlocState {} class DataBlocLoadingState extends DataBlocState {} class DataBlocLoadedState extends DataBlocState { final String data; DataBlocLoadedState(this.data); } class DataBlocNoDataState extends DataBlocState {} class DataBlocErrorState extends DataBlocState { final String error; DataBlocErrorState(this.error); }

Slide 42

Slide 42 text

class MyDataBloc extends Bloc { MyDataBloc() : super(DataBlocInitialState()) { on((event, emit) async { if (state is DataBlocLoadingState) { return; } emit(DataBlocLoadingState()); try { final data = await fetchData(); if (data == '') { emit(DataBlocNoDataState()); } else { emit(DataBlocLoadedState(data)); } } catch (e) { emit(DataBlocErrorState(e.toString())); } }); } } bloc: ^8.1.0 flutter_bloc: ^8.1.1

Slide 43

Slide 43 text

bloc: ^7.2.0 flutter_bloc: ^7.2.0 class MyDataBloc extends Bloc { MyDataBloc() : super(DataBlocInitialState()) { } @override Stream mapEventToState(DataBlocEvent event) async* { if (state is DataBlocLoadingState) { return; } yield DataBlocLoadingState(); try { final data = await _fetchData(); if (data == '') { yield DataBlocNoDataState(); } else { yield DataBlocLoadedState(data); } } catch (e) { yield DataBlocErrorState(e.toString()); } } }

Slide 44

Slide 44 text

Unit test

Slide 45

Slide 45 text

怎麼測試?

Slide 46

Slide 46 text

•Arrange – 準備,準備輸入資料與期待值 •Act – 執⾏,執⾏測試對象 •Assert – 驗證,驗證結果 單元測試原則 3A 原則

Slide 47

Slide 47 text

import 'package:flutter_test/flutter_test.dart'; int sum(int a, int b) { return a + b; } void main() { test('sum(1, 1) value should be 2', () { // Arrange int a = 1; int b = 1; // Act int result = sum(a, b); // Assert expect(result, 2); }); } 主程式

Slide 48

Slide 48 text

(待測物)

Slide 49

Slide 49 text

Bloc/Cubit 怎麼測試?

Slide 50

Slide 50 text

https://pub.dev/packages/bloc_test

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

https://pub.dev/packages/bloc_test 官⽅範例

Slide 53

Slide 53 text

思考邏輯 •實作 == 與 hashCode •或者⽤ equatable 套件 https://pub.dev/packages/equatable

Slide 54

Slide 54 text

思考邏輯 •實作 == 與 hashCode https://pub.dev/packages/equatable

Slide 55

Slide 55 text

思考邏輯 •改⽤ isA() 只判斷型態 https://pub.dev/packages/bloc_test

Slide 56

Slide 56 text

void main() { blocTest( 'emits [SmileState] when GetScoringDataEvent is added', build: () { return MyBloc(scoreRepo: ScoreRepository()); }, act: (bloc) => bloc.add(GetScoringDataEvent()), expect: () => >[isA(), isA()], ); } https://open.spotify.com/album/0y3CDnUguGkRr3lnjD8rrV

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

測試時好時壞?

Slide 60

Slide 60 text

我說球,並不是這麼踢的 https://static-arc.appledaily.com.tw/20140622/UHJ6DEJUQCOXDNKXDRORCRCLWA/img/VRMCXKN4CGZUWQQ4TWL3NYF6TQ.jpg

Slide 61

Slide 61 text

http://newsimg.5054399.com/uploads/userup/1904/041A2194415.jpg 哦,那要怎麼踢?

Slide 62

Slide 62 text

因為你沒有辦法控制外部依賴。

Slide 63

Slide 63 text

怎麼辦?

Slide 64

Slide 64 text

把它換掉。

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

https://img.eservice-hk.net/upload/2018/09/27/162655_d80410cf055dac9ce4ac0dbfd0bbc976.jpg

Slide 69

Slide 69 text

(待測物)

Slide 70

Slide 70 text

Test Double 測試替⾝

Slide 71

Slide 71 text

Test Double •Dummy objects are passed around but never actually used. Usually they are just used to fi ll parameter lists. •Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production. •Stubs provide canned answers to calls made during the test. •Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent. •Mocks: objects pre-programmed with expectations which form a speci fi cation of the calls they are expected to receive. https://martinfowler.com/articles/mocksArentStubs.html#RegularTests

Slide 72

Slide 72 text

Test Double •Dummy object: 為了拿來填參數所使⽤的其實⽤不到它的空物件 
 (null 也算是⼀種 Dummy object)。 •Fake object: 擁有類似正式程式碼的邏輯,只是簡化實作。 •Stub: 回應固定罐頭訊息的物件。 •Spy: 是 Stub 的⼀種,會紀錄怎麼被呼叫的,⽤來驗證待測物的⾏為是 否正確。 •Mocks: 使⽤ Mock Library 動態產⽣,提供 Stub, Spy, Dummy 等功能 http://teddy-chen-tw.blogspot.com/2014/09/test-double2.html https://martinfowler.com/articles/mocksArentStubs.html#RegularTests

Slide 73

Slide 73 text

Dependency Injection https://abdelmajid-baco.medium.com/understanding-dependency-injection-with-c-7da4ad9986e9 依賴注入

Slide 74

Slide 74 text

Dependency Injection •Constructor Injection •Property Injection •Method Injection (DI, 依賴注入)

Slide 75

Slide 75 text

https://ae01.alicdn.com/kf/Hd669ca14ba1c40809de41fc53bcc96c4r/PCI-Express-to-PCI-Adapter-Card-PCIe-to-Dual-Pci-Slot-Expansion-Card-USB-3-0.jpg_Q90.jpg_.webp 主邏輯 插槽 正式程式

Slide 76

Slide 76 text

https://ae01.alicdn.com/kf/Hd669ca14ba1c40809de41fc53bcc96c4r/PCI-Express-to-PCI-Adapter-Card-PCIe-to-Dual-Pci-Slot-Expansion-Card-USB-3-0.jpg_Q90.jpg_.webp 主邏輯 假的程式 插槽 https://shopee.tw/PCI-%E6%95%B8%E5%AD%97%E7%87%88%E8%99%9F- %E9%9B%BB%E8%85%A6%E4%B8%BB%E6%9D%BF%E6%95%85%E9%9A%9C%E5%B0%8F%E8%A8%BA%E6%96%B7%E5%8D%A1-%E6%B8%AC%E8%A9%A6%E5%8D%A1- %E9%9B%BB%E8%85%A6%E9%99%A4%E9%8C%AF%E5%8D%A1-%E9%99%84%E8%AA%AA%E6%98%8E%E6%9B%B8-i.45286897.10679723263? sp_atk=50331a4e-4049-4d31-8e57-5d0dde855783&xptdk=50331a4e-4049-4d31-8e57-5d0dde855783 https://www.ruten.com.tw/item/show?21926902268524

Slide 77

Slide 77 text

https://ae01.alicdn.com/kf/Hd669ca14ba1c40809de41fc53bcc96c4r/PCI-Express-to-PCI-Adapter-Card-PCIe-to-Dual-Pci-Slot-Expansion-Card-USB-3-0.jpg_Q90.jpg_.webp 假的程式 插槽 測試程式

Slide 78

Slide 78 text

import 'dart:math'; abstract class ScoreRepoInterface { Future getScore(); } class ScoreRepository implements ScoreRepoInterface { @override Future getScore() async { await Future.delayed(const Duration(seconds: 1)); var rand = Random(); var score = rand.nextInt(100); return score; } } class StubScoreRepository implements ScoreRepoInterface { int score; StubScoreRepository({required this.score}); @override Future getScore() async { return score; } } 插槽 正式程式 假的程式 (Stub)

Slide 79

Slide 79 text

https://img.ltn.com.tw/Upload/news/600/2021/07/22/3612749_1_1.jpg

Slide 80

Slide 80 text

class MyBloc extends Bloc { ScoreRepoInterface scoreRepo; MyBloc({required this.scoreRepo}) : super(InitState()) { on((event, emit) async { emit(InitState()); int score = await scoreRepo.getScore(); if (score >= 60) { emit(SmileState()); } else { emit(CryingState()); } }); } } 製作插槽

Slide 81

Slide 81 text

blocTest( 'emits [SmileState] when GetScoringDataEvent is added', build: () { ScoreRepoInterface scoreRepo = StubScoreRepository(score: 60); return MyBloc(scoreRepo: scoreRepo); }, act: (cubit) => cubit.add(GetScoringDataEvent()), expect: () => >[isA(), isA()], ); 測試 Bloc (1)

Slide 82

Slide 82 text

blocTest( 'emits [CryingState] when GetScoringDataEvent is added', build: () { ScoreRepoInterface scoreRepo = StubScoreRepository(score: 40); return MyBloc(scoreRepo: scoreRepo); }, act: (cubit) => cubit.add(GetScoringDataEvent()), expect: () => >[isA(), isA()], ); 測試 Bloc (2)

Slide 83

Slide 83 text

還好嗎?

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

Stream

Slide 86

Slide 86 text

stream /str'im/ ⽔流,⼩河 (Noun.) a natural body of running water flowing on or under the earth. https://techcrunch.com/2009/04/27/facebook-opens-up-its-stream-api-to-developers/ https://cdict.net/q/stream 串流

Slide 87

Slide 87 text

Stream Stream sampleOfStream() async* { yield 1; yield 2; yield 3; yield 4; // Do something await Future.delayed(const Duration(seconds: 3)); yield 5; }

Slide 88

Slide 88 text

再舉個例⼦ https://is4-ssl.mzstatic.com/image/thumb/Podcasts125/v4/a3/a6/b5/a3a6b53f-ac3b-26f5-8d4e-094aea97f5ed/mza_17261924418676775272.jpg/500x500bb.jpg

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

abstract class MyBlocState {} class InitState extends MyBlocState {} class NumberState extends MyBlocState { final int number; NumberState(this.number); } 定義狀態 (States) •初始狀態 •取得數字狀態

Slide 91

Slide 91 text

abstract class MyBlocEvent {} class StartGettingNumberEvent extends MyBlocEvent {} class StopGettingNumberEvent extends MyBlocEvent {} 定義事件 (Events) •開始取數字 •停⽌取數字

Slide 92

Slide 92 text

class MyBloc extends Bloc { var numberLoop = NumberLoop(); MyBloc() : super(InitState()) { on((event, emit) async { if (numberLoop.isRunning()) { return; } Stream stream = numberLoop.numberLoop(); await for (var event in stream) { emit(NumberState(event)); } }); on((event, emit) { numberLoop.cancel(); emit(InitState()); }); } }

Slide 93

Slide 93 text

import 'dart:math'; class NumberLoop { bool _isCancelled = true; Stream numberLoop() async* { _isCancelled = false; var rnd = Random(); while (!_isCancelled) { yield rnd.nextInt(10000); await Future.delayed(const Duration(seconds: 1)); } } cancel() { _isCancelled = true; } bool isRunning() { return !_isCancelled; } } 1. 產⽣⼀個數字
 2. 等待⼀秒
 3. 重複 (1) 步

Slide 94

Slide 94 text

Stream numberLoop() { _isCancelled = false; var rnd = Random(); return Stream.periodic( const Duration(seconds: 1), (x) => rnd.nextInt(10000)) .takeWhile((element) => !_isCancelled); } Stream numberLoop() async* { _isCancelled = false; var rnd = Random(); while (!_isCancelled) { yield rnd.nextInt(10000); await Future.delayed(const Duration(seconds: 1)); } }

Slide 95

Slide 95 text

怎麼測試?

Slide 96

Slide 96 text

import 'dart:math'; abstract class NumberLoopInterface { void cancel(); bool isRunning(); Stream numberLoop(); } class NumberLoop implements NumberLoopInterface { bool _isCancelled = true; @override void cancel() { _isCancelled = true; } @override bool isRunning() { // ...略 return !_isCancelled; } @override Stream numberLoop() async* { // ...略 } } 製作插槽 (1)

Slide 97

Slide 97 text

import 'package:bloc_demo2/number_loop.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyBloc extends Bloc { NumberLoopInterface numberLoop; MyBloc({required this.numberLoop}) : super(InitState()) { on((event, emit) async { // ... 略 }); on((event, emit) { numberLoop.cancel(); }); } } 製作插槽 (2)

Slide 98

Slide 98 text

import 'package:bloc_demo2/number_loop.dart'; class StubNumberLoop implements NumberLoopInterface { List numbers; StubNumberLoop({required this.numbers}); @override Stream numberLoop() { return Stream.fromIterable(numbers); } @override void cancel() { } @override bool isRunning() { return true; } } 製作 Stubs

Slide 99

Slide 99 text

import 'package:bloc_demo2/number_loop.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { test('test NumberLoop', () { var loop = StubNumberLoop(numbers: [1, 2, 3]); expect(loop.numberLoop(), emitsInOrder([1, 2, 3])); }); } 測試 Stubs

Slide 100

Slide 100 text

import 'package:bloc_demo2/my_bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { blocTest('test MyBloc', build: () { return MyBloc(numberLoop: StubNumberLoop(numbers: [1, 2, 3])); }, act: (bloc) => bloc.add(StartGettingNumberEvent()), expect: () => [NumberState(1), NumberState(2), NumberState(3)]); } 測試 Bloc

Slide 101

Slide 101 text

總結回顧 •學到 Bloc 元件怎麼使⽤ •定義所有的事件 (Events) 與狀態 (States) •測試 3A 原則:Arrange, Act, Assert •善⽤ Dependency Injection 製作插槽 •外部依賴要⽤ Test double 把它換掉

Slide 102

Slide 102 text

Q & A

Slide 103

Slide 103 text

@override Widget build(BuildContext context) { return FutureBuilder( future: fetchData(), initialData: 0, builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data! >= 60) { return const Text('😄'); } else { return const Text('😭'); } } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } else { return const CircularProgressIndicator(); } }); } @override Widget build(BuildContext context) { return BlocBuilder( bloc: _myBloc, builder: (context, state) { if (state is SmileState) { return const Text('😄'); } else if (state is CryingState) { return const Text('😭'); } else if (state is ErrorState) { return Text(state.error.toString()); } return const CircularProgressIndicator(); }); } BlocBuilder FutureBuilder Q1: BlocBuilder 跟 FutureBuilder 的差異?

Slide 104

Slide 104 text

@override Widget build(BuildContext context) { return BlocBuilder( bloc: _myBloc, builder: (context, state) { if (state is NumberState) { return Text(state.number.toString()); } return const Text('- - - -'); }); } @override Widget build(BuildContext context) { return StreamBuilder(initialData: null, stream: _numberLoop.numberLoop(), builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data.toString()); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } return const Text('- - - -'); }); } BlocBuilder StreamBuilder Q2: BlocBuilder 跟 StreamBuilder 的差異?

Slide 105

Slide 105 text

https://github.com/j796160836/bloc_demo 範例 1

Slide 106

Slide 106 text

https://github.com/j796160836/bloc_demo2 範例 2