Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Johnny Sung
December 03, 2022

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

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

#flutter
#bloc
#devfest2022

Johnny Sung

December 03, 2022
Tweet

More Decks by Johnny Sung

Other Decks in Programming

Transcript

  1. 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.
  2. 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
  3. // 抽象狀態 abstract class MyBlocState {} 
 // 微笑狀態 class

    SmileState extends MyBlocState {} // 哭泣狀態 class CryingState extends MyBlocState {} // 初始狀態 class InitState extends MyBlocState {} // 錯誤狀態 class ErrorState extends MyBlocState { Error error; ErrorState(this.error); }
  4. import 'package:bloc/bloc.dart'; class MyBloc extends Bloc<MyBlocEvent, MyBlocState> { MyBloc() :

    super(InitState()) { on<GetScoringDataEvent>((event, emit) { emit(SmileState()); }); } }
  5. class ScoreRepository { Future<int> getScore() async { await Future.delayed(const Duration(seconds:

    1)); // 等待 1 秒,模擬網路延遲 var rand = Random(); var score = rand.nextInt(100); return score; } } 取成績單
  6. 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 取成績單)
  7. 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
  8. 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
  9. 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()); } }); } }
  10. 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 主邏輯
  11. 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 來顯⽰
  12. 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); }
  13. 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
  14. 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()); } } }
  15. 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); }); } 主程式
  16. 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
  17. 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
  18. 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
  19. 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)
  20. 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()); } }); } } 製作插槽
  21. 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)
  22. 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)
  23. 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 串流
  24. Stream Stream<int> sampleOfStream() async* { yield 1; yield 2; yield

    3; yield 4; // Do something await Future.delayed(const Duration(seconds: 3)); yield 5; }
  25. abstract class MyBlocState {} class InitState extends MyBlocState {} class

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

    StopGettingNumberEvent extends MyBlocEvent {} 定義事件 (Events) •開始取數字 •停⽌取數字
  27. 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()); }); } }
  28. 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) 步
  29. 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)); } }
  30. 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)
  31. 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)
  32. 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
  33. 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
  34. 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
  35. 總結回顧 •學到 Bloc 元件怎麼使⽤ •定義所有的事件 (Events) 與狀態 (States) •測試 3A

    原則:Arrange, Act, Assert •善⽤ Dependency Injection 製作插槽 •外部依賴要⽤ Test double 把它換掉
  36. @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 的差異?
  37. @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 的差異?