什麼是Bloc Design Pattern

Bloc 全名是「Business Logic Component」,用來分離UI層和商業邏輯層,有助於程式碼的維護、重複利用以及測試。

Bloc 的流程

flutter_2_1

Bloc 層會接收使用者的行為 (例如點擊Button),根據不同的事件(Event)Bloc會去呼叫相應的函式(可能是去資料庫抓資料或使用 api )。 之後根據結果回傳相應的狀態 (State) 給UI層,UI則根據狀態做變化。

使用Flutter Bloc 套件

Flutter Bloc 套件是由Felix Angelov所開發的套件,能夠幫助開發人員實作 Bloc pattern。 他將 Bloc 包裝成更容易理解和維護的框架,而且把許多細節都幫我們做好了(例如關閉Stream、Log等等)。

Bloc Event


    abstract class LoginEvent extends Equatable {
        LoginEvent([List props = const []]) : super(props);
    }

    class EmailChanged extends LoginEvent {
        final String email;

        EmailChanged({@required this.email}) : super([email]);

        @override
        String toString() => 'EmailChanged { email :$email }';
    }

Bloc State


    class LoginState {
        bool isEmailValid;
        final bool isPasswordValid;

        LoginState({
            @required this.isEmailValid,
            @required this.isPasswordValid,
        });

        factory LoginState.empty() {
            return LoginState(
                isEmailValid: true,
                isPasswordValid: true,
            );
        }
    }

Bloc

定義 Bloc 物件需先繼承套件提供的 Bloc class,並給予定義好的Event物件和State物件,和定義 Bloc 初始的State。
另外一定要實作的是mapEventToState,將接收到的Event用對應的商業邏輯(Business Logic)做處理,最後回傳包裝成Stream的State


    class LoginBloc extends Bloc<LoginEvent, LoginState> {
        @override
        LoginState get initialState => LoginState.init();

        @override
        Stream<LoginState> mapEventToState(LoginEvent event) async* {
            // 依照接收到的Event執行對應的方法
            if (event is LoginWithCredentialsPressed) {
                yield* _mapLoginWithCredentialsPressedToState(
                email: event.email,
                password: event.password,
                );
            }
        }

        Stream<LoginState> _mapLoginWithCredentialsPressedToState({
            String email,
            String password,
        }) async* {
            // 使用帳號和密碼進行登入,成功就回傳sucess的State
            // 失敗則回傳Failure的State
            try {
                await _userRepository.signInWithCredentials(email, password);
                yield LoginState.success();
            } catch (_) {
                yield LoginState.failure();
            }
        }
    }

Dispatch

那麼該如何觸發Event呢?
Bloc內提供dispatch 方法可以用Event作為參數,它會觸發mapEventToState,接著就是執行Event與State的轉換。


    // 示意用程式碼
    void main() {
        LoginBloc bloc = LoginBloc();
        // 略...
        RaisedButton(
            onPressed: (){
                bloc.dispatch(LoginWithGooglePressed);
            },
            child: Text('Google Login'),
        )
    }

BlocProvider

在 Bloc package中,Widget 本身並不會直接擁有、或是直接參照 Bloc,而是透過 BlocProvider 將 Bloc、
以及用到這個 Bloc 的 Widget 綁起來,Widget 要在 Widget Tree 當中往上找到 BlocProvider 之後,再跟 BlocProvider 詢問 Bloc。

建立 BlocProvider 的方式像這樣。我們有一個上層的 Widget,裡頭包含了一個與登入相關的 AuthenticationBloc,
而我們 App 中的主要畫面都在 PageWidget 裡頭的話:


    var _bloc = AuthenticationBloc();
    Widget build(BuildContext context) {
        return BlocProvider(
            bloc: _bloc,
            child: PageWidget()),
        );
    }

在 PageWidget,以及 PageWidget 以下任何一層的所有 children,都可以往上、在 build context 中找到 AuthenticationBloc:


    final authenticationBloc = BlocProvider.of<Bloc<AuthenticationEvent, AuthenticationState>>(context);

BlocBuilder

我們會希望 Bloc 在狀態更新之後,自動更新相關的 UI,而不是我們自己再去呼叫 setState(),
而 Bloc package 提供了 BlocBuilder,只要是這個 Bloc 發生變動,就會執行我們所指定的 WidgetBuilder。


    final authenticationBloc = BlocProvider.of<Bloc<AuthenticationEvent, AuthenticationState>>(context);
    return BlocBuilder<AuthenticationEvent, AuthenticationState>(
        bloc: authenticationBloc,
        builder: (context, state) {
            if (state is AuthenticatedState) {
                return Text('已登入 ${state.email}');
            } else {
                return Text('尚未登入');
            }
        });
An unhandled error has occurred. Reload 🗙