Nghệ thuật Flutter: Kiến trúc Bloc và sử dụng BlocProvider
BloC (Business Logic Controller)
Được tạo bởi Google và ra mắt 2018, được viết dựa trên Stream và Reactive Programing.
Kiến trúc Bloc
Về kiến trúc Bloc trong Flutter, có hai dạng mà bạn thường gặp đó là :
+ Xây dựng Bloc với RxDart
+ Xây dựng BLoC với Event – State
Nhưng dù dùng kiểu nào đi nữa, thì cấu trúc cũng theo một mô hình như bên dưới:
Trong kiến trúc BloC, chia làm 4 layers chính là : UI, BloC, Respository, DataSources:
• UI (User Interface ) : Là những phần của Ứng dụng để hiển thị với người dùng.
• BloC: Luôn lắng nghe các sự kiện pass qua nó, ví dụ luôn lắng nghe data pass qua stream.
• Repository : Dùng để fetching data từ Data sources. Đầu ra của lớp này sẽ là đầu vào của khối Bloc, khi đó data sẽ được đặt trong các Stream
• Data Sources: Là khối cung cấp data cho ứng dụng như network, sqflite, shared_preferences
Xây dựng BLoC với Stream
– Trong kiểu kiến trúc này thì Bloc được tạo ra cùng với RxDart với các khái niệm như Stream, Sink.
Kiểu kiến trúc này thường được dùng với RestAPI.
Để hiểu hơn về các dùng này bạn có thể xem lại bài viết có hướng dẫn cụ thể của tôi về REST API.
Xây dựng BLoC với Event-State
Cấu trúc BLoC dạng này như bên dưới :
* Events là những sự kiện được gửi từ UI như : touch, nhấn nút.. . Events này sẽ được tiếp nhận bởi khối BloC.
* States là những trạng thái được gửi lên bởi khối Bloc, khi nhận được State thì UI sẽ thay đổi theo các State đó.
* Transition : Thể hiện sự thay đổi state, nó bao gồm : current State, next State, event
Khi xây dựng kiến trúc Bloc theo kiểu event – state; bạn cần nhớ :
+ Một Bloc được tạo ra từ việc kế thừa class Bloc của bloc package
+ Bloc phải được khai báo state ban đầu : initial State
+ Khi tạo bloc với sự kế thừa Bloc của Bloc package, bạn cần implement function mapEventToState, với tham số là event , đầu ra là stream của state.
+ Sử dụng thuộc tính currentState để truy cập current state.
+ Mỗi bloc cần phải có thuộc tính dispatch, hàm dispatch nhận event , trigger mapEventToState. Dispatch có vai trò như lớp presentation
+ hàm onTransition được gọi trước khi update state bloc
+ Để bắt exception của bloc , chúng ta có thể override hàm onError
Các bước tạo một khối bloc và sử dụng bloc:
+ Tạo Events
+ Tạo State
+ Tạo Bloc
+ Tạo Bloc Provider
+ Sử dụng Bloc với state, event
Bạn có thể tham khảo ví dụ : login, trong bloclibrary.dev để hiểu hơn.
Khai báo thư viện như bloc, flutter_bloc:
1 2 |
bloc: ^6.0.0 flutter_bloc: ^6.0.0 |
Tạo Events
Trong Authentication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
part of 'authentication_bloc.dart'; abstract class AuthenticationEvent extends Equatable { const AuthenticationEvent(); @override List<Object> get props => []; } class AuthenticationStatusChanged extends AuthenticationEvent { const AuthenticationStatusChanged(this.status); final AuthenticationStatus status; @override List<Object> get props => [status]; } class AuthenticationLogoutRequested extends AuthenticationEvent {} |
Về Login Event:
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 |
part of 'login_bloc.dart'; abstract class LoginEvent extends Equatable { const LoginEvent(); @override List<Object> get props => []; } class LoginUsernameChanged extends LoginEvent { const LoginUsernameChanged(this.username); final String username; @override List<Object> get props => [username]; } class LoginPasswordChanged extends LoginEvent { const LoginPasswordChanged(this.password); final String password; @override List<Object> get props => [password]; } class LoginSubmitted extends LoginEvent { const LoginSubmitted(); } |
Tạo State
Trong Authentication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
part of 'authentication_bloc.dart'; class AuthenticationState extends Equatable { const AuthenticationState._({ this.status = AuthenticationStatus.unknown, this.user, }); const AuthenticationState.unknown() : this._(); const AuthenticationState.authenticated(User user) : this._(status: AuthenticationStatus.authenticated, user: user); const AuthenticationState.unauthenticated() : this._(status: AuthenticationStatus.unauthenticated); final AuthenticationStatus status; final User user; @override List<Object> get props => [status, user]; } |
Tạo State trong Login
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 |
part of 'login_bloc.dart'; class LoginState extends Equatable { const LoginState({ this.status = FormzStatus.pure, this.username = const Username.pure(), this.password = const Password.pure(), }); final FormzStatus status; final Username username; final Password password; LoginState copyWith({ FormzStatus status, Username username, Password password, }) { return LoginState( status: status ?? this.status, username: username ?? this.username, password: password ?? this.password, ); } @override List<Object> get props => [status, username, password]; } |
Tạo BLoC
Trong Authentication:
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'package:user_repository/user_repository.dart'; part 'authentication_event.dart'; part 'authentication_state.dart'; class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> { AuthenticationBloc({ @required AuthenticationRepository authenticationRepository, @required UserRepository userRepository, }) : assert(authenticationRepository != null), assert(userRepository != null), _authenticationRepository = authenticationRepository, _userRepository = userRepository, super(const AuthenticationState.unknown()) { _authenticationStatusSubscription = _authenticationRepository.status.listen( (status) => add(AuthenticationStatusChanged(status)), ); } final AuthenticationRepository _authenticationRepository; final UserRepository _userRepository; StreamSubscription<AuthenticationStatus> _authenticationStatusSubscription; @override Stream<AuthenticationState> mapEventToState( AuthenticationEvent event, ) async* { if (event is AuthenticationStatusChanged) { yield await _mapAuthenticationStatusChangedToState(event); } else if (event is AuthenticationLogoutRequested) { _authenticationRepository.logOut(); } } @override Future<void> close() { _authenticationStatusSubscription?.cancel(); _authenticationRepository.dispose(); return super.close(); } Future<AuthenticationState> _mapAuthenticationStatusChangedToState( AuthenticationStatusChanged event, ) async { switch (event.status) { case AuthenticationStatus.unauthenticated: return const AuthenticationState.unauthenticated(); case AuthenticationStatus.authenticated: final user = await _tryGetUser(); return user != null ? AuthenticationState.authenticated(user) : const AuthenticationState.unauthenticated(); default: return const AuthenticationState.unknown(); } } Future<User> _tryGetUser() async { try { final user = await _userRepository.getUser(); return user; } on Exception { return null; } } } |
Tạo khối BLoC trong Login:
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_login/login/login.dart'; import 'package:formz/formz.dart'; import 'package:meta/meta.dart'; part 'login_event.dart'; part 'login_state.dart'; class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc({ @required AuthenticationRepository authenticationRepository, }) : assert(authenticationRepository != null), _authenticationRepository = authenticationRepository, super(const LoginState()); final AuthenticationRepository _authenticationRepository; @override Stream<LoginState> mapEventToState( LoginEvent event, ) async* { if (event is LoginUsernameChanged) { yield _mapUsernameChangedToState(event, state); } else if (event is LoginPasswordChanged) { yield _mapPasswordChangedToState(event, state); } else if (event is LoginSubmitted) { yield* _mapLoginSubmittedToState(event, state); } } LoginState _mapUsernameChangedToState( LoginUsernameChanged event, LoginState state, ) { final username = Username.dirty(event.username); return state.copyWith( username: username, status: Formz.validate([state.password, username]), ); } LoginState _mapPasswordChangedToState( LoginPasswordChanged event, LoginState state, ) { final password = Password.dirty(event.password); return state.copyWith( password: password, status: Formz.validate([password, state.username]), ); } Stream<LoginState> _mapLoginSubmittedToState( LoginSubmitted event, LoginState state, ) async* { if (state.status.isValidated) { yield state.copyWith(status: FormzStatus.submissionInProgress); try { await _authenticationRepository.logIn( username: state.username.value, password: state.password.value, ); yield state.copyWith(status: FormzStatus.submissionSuccess); } on Exception catch (_) { yield state.copyWith(status: FormzStatus.submissionFailure); } } } } |
Sử dụng BlocProvider
BlocProvider được sử dụng dựa trên Provider, Bạn có thể tham khảo cách dùng như bên dưới :
1 2 3 4 5 6 7 8 |
BlocProvider<AuthenticationBloc>( create: (context) { return AuthenticationBloc(userRepository: userRepository) ..add(AuthenticationStarted()); }, child: App(userRepository: userRepository), ), |
hoặc :
1 2 3 4 5 6 7 |
BlocProvider( create: (_) => AuthenticationBloc( authenticationRepository: authenticationRepository, userRepository: userRepository, ), child: AppView(), ), |
Tham khảo example: Login của bloclibrary để biết thêm.
Như vậy, các bạn đã biết kiến trúc Bloc và biết sơ qua về kiến trúc Bloc event-state.
Hi vọng các bạn có được nhiều kiến thức từ Báo Flutter.