Ce diaporama a bien été signalé.
Le téléchargement de votre SlideShare est en cours. ×

[Flutter] 來體驗 bloc 小方塊的神奇魔法 @Devfest 2022

Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Chargement dans…3
×

Consultez-les par la suite

1 sur 106 Publicité

[Flutter] 來體驗 bloc 小方塊的神奇魔法 @Devfest 2022

Télécharger pour lire hors ligne

bloc 是什麼? bloc 是一個狀態管理模組,圖示是一個小方塊。
這個小小方塊可是個關鍵元件呢!
用淺顯易懂的方式,來看看 bloc 的寫法會怎麼幫助到你的程式碼吧!

#flutter
#bloc
#devfest2022

bloc 是什麼? bloc 是一個狀態管理模組,圖示是一個小方塊。
這個小小方塊可是個關鍵元件呢!
用淺顯易懂的方式,來看看 bloc 的寫法會怎麼幫助到你的程式碼吧!

#flutter
#bloc
#devfest2022

Publicité
Publicité

Plus De Contenu Connexe

Plus par Johnny Sung (20)

Plus récents (20)

Publicité

[Flutter] 來體驗 bloc 小方塊的神奇魔法 @Devfest 2022

  1. 1. 台北 & ⾼雄 來體驗 bloc ⼩⽅塊 的神奇魔法 Johnny Sung (宋岡諺) Full stack developer
  2. 2. ⼤綱 •Bloc 介紹 •舉個例⼦寫寫看 •怎麼測試?
  3. 3. ☕ 咖啡準備好了嗎?
  4. 4. https://www.redbubble.com/i/ipad-case/I-turn-co ff ee-into-code-by-DutchArt/23986463.MNKGF
  5. 5. 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.
  6. 6. https://pub.dev/packages/ fl utter_bloc
  7. 7. Bloc 元件 •Cubit •Bloc •BlocBuilder
  8. 8. Bloc 元件 •BlocBuilder •BlocSelector •BlocProvider •MultiBlocProvider •BlocListener •MultiBlocListener •BlocConsumer •RepositoryProvider •MultiRepositoryProvider
  9. 9. Bloc
  10. 10. Cubit
  11. 11. 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
  12. 12. 跟這個⼀點關係都沒有。
  13. 13. 簡單來說, Cubit 算是 Bloc 的簡化版。
  14. 14. (1) (2)
  15. 15. 發想步驟 •列出所有可能的 事件 (Events) •列出所有可能的 狀態 (States)
  16. 16. 舉個例⼦ (_/) ( •_•) />~ 🌰
  17. 17. // 抽象事件 abstract class MyBlocEvent {} // 抓成績單 class GetScoringDataEvent extends MyBlocEvent {}
  18. 18. // 抽象狀態 abstract class MyBlocState {} // 微笑狀態 class SmileState extends MyBlocState {} // 哭泣狀態 class CryingState extends MyBlocState {} // 初始狀態 class InitState extends MyBlocState {} // 錯誤狀態 class ErrorState extends MyBlocState { Error error; ErrorState(this.error); }
  19. 19. import 'package:bloc/bloc.dart'; class MyBloc extends Bloc<MyBlocEvent, MyBlocState> { MyBloc() : super(InitState()) { on<GetScoringDataEvent>((event, emit) { emit(SmileState()); }); } }
  20. 20. class ScoreRepository { Future<int> getScore() async { await Future.delayed(const Duration(seconds: 1)); // 等待 1 秒,模擬網路延遲 var rand = Random(); var score = rand.nextInt(100); return score; } } 取成績單
  21. 21. import 'dart:convert'; import 'package:http/http.dart' as http; Future<MyScoreResponseModel> getScore() async { final response = await http.get( Uri.parse(constServerDomain + "/score"), headers: defaultHeaders()); final map = json.decode(response.body); return MyScoreResponseModel(map); } (未來可⽤ API 取成績單)
  22. 22. class MyBloc extends Bloc<MyBlocEvent, MyBlocState> { late ScoreRepository _scoreRepo; MyBloc(MyBlocState initialState) : super(initialState) { _scoreRepo = ScoreRepository(); } @override Stream<MyBlocState> 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
  23. 23. 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<MyBlocEvent, MyBlocState> { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on<GetScoringDataEvent>((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
  24. 24. https://github.com/felangel/bloc/issues/2526
  25. 25. https://bloclibrary.dev/#/migration
  26. 26. 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<MyBlocEvent, MyBlocState> { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on<GetScoringDataEvent>((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<MyBlocEvent, MyBlocState> { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on<GetScoringDataEvent>((event, emit) async { try { var value = await _scoreRepo.getScore(); if (value >= 60) { emit(SmileState()); } else { emit(CryingState()); } }catch(error){ emit(ErrorState()); } }); } }
  27. 27. 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<MyBlocEvent, MyBlocState> { late ScoreRepository _scoreRepo; MyBloc() : super(InitState()) { _scoreRepo = ScoreRepository(); on<GetScoringDataEvent>((event, emit) async { try { var value = await _scoreRepo.getScore(); if (value >= 60) { emit(SmileState()); } else { emit(CryingState()); } }catch(error){ emit(ErrorState()); } }); } } Bloc 主邏輯
  28. 28. 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<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { 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<MyBloc, MyBlocState>( 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 來顯⽰
  29. 29. 換⼀個真實例⼦
  30. 30. abstract class DataBlocEvent {} class MyBlocLoadEvent extends DataBlocEvent {}
  31. 31. 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); }
  32. 32. class MyDataBloc extends Bloc<DataBlocEvent, DataBlocState> { MyDataBloc() : super(DataBlocInitialState()) { on<MyBlocLoadEvent>((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
  33. 33. bloc: ^7.2.0 flutter_bloc: ^7.2.0 class MyDataBloc extends Bloc<DataBlocEvent, DataBlocState> { MyDataBloc() : super(DataBlocInitialState()) { } @override Stream<DataBlocState> 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()); } } }
  34. 34. Unit test
  35. 35. 怎麼測試?
  36. 36. •Arrange – 準備,準備輸入資料與期待值 •Act – 執⾏,執⾏測試對象 •Assert – 驗證,驗證結果 單元測試原則 3A 原則
  37. 37. 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); }); } 主程式
  38. 38. (待測物)
  39. 39. Bloc/Cubit 怎麼測試?
  40. 40. https://pub.dev/packages/bloc_test
  41. 41. https://pub.dev/packages/bloc_test 官⽅範例
  42. 42. 思考邏輯 •實作 == 與 hashCode •或者⽤ equatable 套件 https://pub.dev/packages/equatable
  43. 43. 思考邏輯 •實作 == 與 hashCode https://pub.dev/packages/equatable
  44. 44. 思考邏輯 •改⽤ isA() 只判斷型態 https://pub.dev/packages/bloc_test
  45. 45. void main() { blocTest<MyBloc, MyBlocState>( 'emits [SmileState] when GetScoringDataEvent is added', build: () { return MyBloc(scoreRepo: ScoreRepository()); }, act: (bloc) => bloc.add(GetScoringDataEvent()), expect: () => <TypeMatcher<MyBlocState>>[isA<InitState>(), isA<SmileState>()], ); } https://open.spotify.com/album/0y3CDnUguGkRr3lnjD8rrV
  46. 46. 測試時好時壞?
  47. 47. 我說球,並不是這麼踢的 https://static-arc.appledaily.com.tw/20140622/UHJ6DEJUQCOXDNKXDRORCRCLWA/img/VRMCXKN4CGZUWQQ4TWL3NYF6TQ.jpg
  48. 48. http://newsimg.5054399.com/uploads/userup/1904/041A2194415.jpg 哦,那要怎麼踢?
  49. 49. 因為你沒有辦法控制外部依賴。
  50. 50. 怎麼辦?
  51. 51. 把它換掉。
  52. 52. https://img.eservice-hk.net/upload/2018/09/27/162655_d80410cf055dac9ce4ac0dbfd0bbc976.jpg
  53. 53. (待測物)
  54. 54. Test Double 測試替⾝
  55. 55. 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
  56. 56. 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
  57. 57. Dependency Injection https://abdelmajid-baco.medium.com/understanding-dependency-injection-with-c-7da4ad9986e9 依賴注入
  58. 58. Dependency Injection •Constructor Injection •Property Injection •Method Injection (DI, 依賴注入)
  59. 59. 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 主邏輯 插槽 正式程式
  60. 60. 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
  61. 61. 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 假的程式 插槽 測試程式
  62. 62. import 'dart:math'; abstract class ScoreRepoInterface { Future<int> getScore(); } class ScoreRepository implements ScoreRepoInterface { @override Future<int> 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<int> getScore() async { return score; } } 插槽 正式程式 假的程式 (Stub)
  63. 63. https://img.ltn.com.tw/Upload/news/600/2021/07/22/3612749_1_1.jpg
  64. 64. class MyBloc extends Bloc<MyBlocEvent, MyBlocState> { ScoreRepoInterface scoreRepo; MyBloc({required this.scoreRepo}) : super(InitState()) { on<GetScoringDataEvent>((event, emit) async { emit(InitState()); int score = await scoreRepo.getScore(); if (score >= 60) { emit(SmileState()); } else { emit(CryingState()); } }); } } 製作插槽
  65. 65. blocTest<MyBloc, MyBlocState>( 'emits [SmileState] when GetScoringDataEvent is added', build: () { ScoreRepoInterface scoreRepo = StubScoreRepository(score: 60); return MyBloc(scoreRepo: scoreRepo); }, act: (cubit) => cubit.add(GetScoringDataEvent()), expect: () => <TypeMatcher<MyBlocState>>[isA<InitState>(), isA<SmileState>()], ); 測試 Bloc (1)
  66. 66. blocTest<MyBloc, MyBlocState>( 'emits [CryingState] when GetScoringDataEvent is added', build: () { ScoreRepoInterface scoreRepo = StubScoreRepository(score: 40); return MyBloc(scoreRepo: scoreRepo); }, act: (cubit) => cubit.add(GetScoringDataEvent()), expect: () => <TypeMatcher<MyBlocState>>[isA<InitState>(), isA<CryingState>()], ); 測試 Bloc (2)
  67. 67. 還好嗎?
  68. 68. Stream
  69. 69. 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 串流
  70. 70. Stream Stream<int> sampleOfStream() async* { yield 1; yield 2; yield 3; yield 4; // Do something await Future.delayed(const Duration(seconds: 3)); yield 5; }
  71. 71. 再舉個例⼦ https://is4-ssl.mzstatic.com/image/thumb/Podcasts125/v4/a3/a6/b5/a3a6b53f-ac3b-26f5-8d4e-094aea97f5ed/mza_17261924418676775272.jpg/500x500bb.jpg
  72. 72. abstract class MyBlocState {} class InitState extends MyBlocState {} class NumberState extends MyBlocState { final int number; NumberState(this.number); } 定義狀態 (States) •初始狀態 •取得數字狀態
  73. 73. abstract class MyBlocEvent {} class StartGettingNumberEvent extends MyBlocEvent {} class StopGettingNumberEvent extends MyBlocEvent {} 定義事件 (Events) •開始取數字 •停⽌取數字
  74. 74. class MyBloc extends Bloc<MyBlocEvent, MyBlocState> { var numberLoop = NumberLoop(); MyBloc() : super(InitState()) { on<StartGettingNumberEvent>((event, emit) async { if (numberLoop.isRunning()) { return; } Stream<int> stream = numberLoop.numberLoop(); await for (var event in stream) { emit(NumberState(event)); } }); on<StopGettingNumberEvent>((event, emit) { numberLoop.cancel(); emit(InitState()); }); } }
  75. 75. import 'dart:math'; class NumberLoop { bool _isCancelled = true; Stream<int> 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) 步
  76. 76. Stream<int> numberLoop() { _isCancelled = false; var rnd = Random(); return Stream.periodic( const Duration(seconds: 1), (x) => rnd.nextInt(10000)) .takeWhile((element) => !_isCancelled); } Stream<int> numberLoop() async* { _isCancelled = false; var rnd = Random(); while (!_isCancelled) { yield rnd.nextInt(10000); await Future.delayed(const Duration(seconds: 1)); } }
  77. 77. 怎麼測試?
  78. 78. import 'dart:math'; abstract class NumberLoopInterface { void cancel(); bool isRunning(); Stream<int> numberLoop(); } class NumberLoop implements NumberLoopInterface { bool _isCancelled = true; @override void cancel() { _isCancelled = true; } @override bool isRunning() { // ...略 return !_isCancelled; } @override Stream<int> numberLoop() async* { // ...略 } } 製作插槽 (1)
  79. 79. import 'package:bloc_demo2/number_loop.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyBloc extends Bloc<MyBlocEvent, MyBlocState> { NumberLoopInterface numberLoop; MyBloc({required this.numberLoop}) : super(InitState()) { on<StartGettingNumberEvent>((event, emit) async { // ... 略 }); on<StopGettingNumberEvent>((event, emit) { numberLoop.cancel(); }); } } 製作插槽 (2)
  80. 80. import 'package:bloc_demo2/number_loop.dart'; class StubNumberLoop implements NumberLoopInterface { List<int> numbers; StubNumberLoop({required this.numbers}); @override Stream<int> numberLoop() { return Stream.fromIterable(numbers); } @override void cancel() { } @override bool isRunning() { return true; } } 製作 Stubs
  81. 81. 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
  82. 82. 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
  83. 83. 總結回顧 •學到 Bloc 元件怎麼使⽤ •定義所有的事件 (Events) 與狀態 (States) •測試 3A 原則:Arrange, Act, Assert •善⽤ Dependency Injection 製作插槽 •外部依賴要⽤ Test double 把它換掉
  84. 84. Q & A
  85. 85. @override Widget build(BuildContext context) { return FutureBuilder<int>( 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<MyBloc, MyBlocState>( 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 的差異?
  86. 86. @override Widget build(BuildContext context) { return BlocBuilder<MyBloc, MyBlocState>( bloc: _myBloc, builder: (context, state) { if (state is NumberState) { return Text(state.number.toString()); } return const Text('- - - -'); }); } @override Widget build(BuildContext context) { return StreamBuilder<int>(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 的差異?
  87. 87. https://github.com/j796160836/bloc_demo 範例 1
  88. 88. https://github.com/j796160836/bloc_demo2 範例 2

×