UserRepository
的地方,那麼今天就來把驗證帳號邏輯的「AuthenticationBloc」完成吧。首先來安裝前天有提到的擴充套件(如果你是使用VS Code可以到這裡下載)。使用Android Studio的人直接在Plugin的地方搜尋就找的到囉。
這個套件讓我們只需輸入Bloc的名稱它就能產生bloc
、event
、state
三個模板可以更快的把bloc pattern 實作出來。
新增一個authentication_bloc
資料夾。
對資料夾按右鍵 -> New -> Bloc Generator -> New Bloc
輸入authentication
,勾選下方使用Equatable按下OK,它會幫你產生4個檔案,其中三個相信你已經知道他們的用途了,剩下的bloc.dart
是讓引入檔案能更加方便,只要import bloc.dart,就能把其餘的三個一起引入。
fluttube
└───lib
│ └───authentication_bloc
│ │ └───authentication_bloc.dart
│ │ └───authentication_event.dart
│ │ └───authentication_state.dart
│ │ └───bloc.dart
│ └───firebase
│ │ └───user_repository.dart
│ └───login
│ │ └───login_page.dart
│ └───main.dart
│ ...
│
在開始實作state前要先思考在這個Bloc中會有哪些State。在驗證階段使用者會遇到的State應該會有以下三種:
確定好有哪些State後就開始吧。
開啟authentication_state.dart
初始的程式碼會如下
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@immutable
abstract class AuthenticationState extends Equatable {
AuthenticationState([List props = const []]) : super(props);
}
class InitialAuthenticationState extends AuthenticationState {}
這邊定義了一個抽象類別,繼承Equatable是為了能比較兩個物件是否相同(Equatable會幫我們比較兩個實體的屬性值是否全都相同)。
詳細的說明和例子可參考同個作者的medium文章
接著來建立3個State類別
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@immutable
abstract class AuthenticationState extends Equatable {
AuthenticationState([List props = const []]) : super(props);
}
class Uninitialized extends AuthenticationState {
@override
String toString() {
return 'Uninitialized';
}
}
class Authenticated extends AuthenticationState {
final String userName;
Authenticated(this.userName) : super([userName]);
@override
String toString() {
return 'Authenticated {UserName: $userName}';
}
}
class Unauthenticated extends AuthenticationState {
@override
String toString() {
return 'Unauthenticated';
}
}
Override toString
是為了當onTransition
觸發時能夠清楚了解現在State的轉換是如何。
開始實作前一樣思考在驗證流程中,使用者會觸發哪些event以及每個event觸發後需要執行哪些商業邏輯。
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
@immutable
abstract class AuthenticationEvent extends Equatable{
AuthenticationEvent([List props = const []]) : super(props);
}
class AppStarted extends AuthenticationEvent {
@override
String toString() => 'AppStarted';
}
class LoggedIn extends AuthenticationEvent {
@override
String toString() => 'LoggedIn';
}
class LoggedOut extends AuthenticationEvent {
@override
String toString() => 'LoggedOut';
}
Override toString
一樣是為了當onEvent
觸發時能顯示更多資訊。
state和event都定義好了,就換bloc吧。
初始的程式碼如下
import 'dart:async';
import 'package:bloc/bloc.dart';
import './bloc.dart';
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
@override
AuthenticationState get initialState => InitialAuthenticationState();
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
// TODO: Add Logic
}
}
assert
會檢查傳入的userRepository是不是null,是的話會報錯。final UserRepository _userRepository;
AuthenticationBloc({UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository;
initialState
是哪一個。InitialAuthenticationState
更改成Uninitialized
@override
AuthenticationState get initialState => Uninitialized();
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
if (event is AppStarted) {
yield* _mapAppStartedToState();
} else if (event is LoggedIn) {
yield* _mapLoggedInToState();
} else if (event is LoggedOut) {
yield* _mapLoggedOutToState();
}
}
Authenticated
或Unauthenticated
。Stream<AuthenticationState> _mapAppStartedToState() async* {
try {
final bool isSigned = await _userRepository.isSignedIn();
if (isSigned) {
final String name = await _userRepository.getUser();
yield Authenticated(name);
}
else{
yield Unauthenticated();
}
} catch (_) {
yield Unauthenticated();
}
}
Stream<AuthenticationState> _mapLoggedInToState() async* {
yield Authenticated(await _userRepository.getUser());
}
Stream<AuthenticationState> _mapLoggedOutToState() async* {
yield Unauthenticated();
_userRepository.signOut();
}
有關於async*
與yield
的用法可以參考這裡。
重點在於yield
會回傳Stream<dynamic>
,如此一來UI使用的BlocBuilder或BlocListener就可以接收到新的State並更新介面。
如此一來AuthenticationBloc就完成了,回顧一下建立Bloc的整個過程,
fluttube
└───lib
│ └───authentication_bloc
│ │ └───authentication_bloc.dart
│ │ └───authentication_event.dart
│ │ └───authentication_state.dart
│ │ └───bloc.dart
│ └───firebase
│ │ └───user_repository.dart
│ └───login
│ │ └───login_page.dart
│ └───main.dart
│ └───simple_bloc_delegate.dart
│ ...
貼上以下程式碼。
import 'package:bloc/bloc.dart';
class SimpleBlocDelegate extends BlocDelegate {
@override
void onEvent(Bloc bloc, Object event) {
super.onEvent(bloc, event);
print(event);
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}
@override
void onError(Bloc bloc, Object error, StackTrace stacktrace) {
super.onError(bloc, error, stacktrace);
print('$error, $stacktrace');
}
}
設定delegate,把bloc的資訊和error log出來,細節可以參考Day11的介紹。
void main(){
BlocSupervisor.delegate = SimpleBlocDelegate();
runApp(MyApp());
}
把原本的StatelessWidget轉成StatefulWidget。
只要打「stful」縮寫就可以產生StatefulWidget的template
在_MyAppState裡建立UserRepository和AuthenticationBloc物件
class _MyAppState extends State<MyApp> {
final UserRepository _userRepository = UserRepository();
AuthenticationBloc _authenticationBloc;
@override
void initState() {
_authenticationBloc = AuthenticationBloc(userRepository: _userRepository);
super.initState();
}
@override
Widget build(BuildContext context) {
...
}
}
最後來設計界面,原本是直接顯示SplashPage,這邊改成使用BlocProvider
讓subTree Widget都能使用AuthenticationBloc並用BlocBuilder
來監聽State的變化。
@override
Widget build(BuildContext context) {
return BlocProvider(
builder: (BuildContext context) => _authenticationBloc,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocBuilder(
bloc: _authenticationBloc,
builder: (context, state){
if (state is Authenticated){
return Container();
}else if (state is Unauthenticated) {
return LoginPage();
}
return SplashPage();
},
),
)
);
}
你可能會產生疑問:诶~怎麼都沒看到觸發事件用的dispatch
,這樣State不就永遠停留在Uninitialized()
了嗎?畫面也只會顯示SplashPage
。
別急別急其實玄機就在SplashPage
裡,所以待會要來改寫它。
完整main.dart程式碼
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc/bloc.dart';
import 'splash_page.dart';
import 'simple_bloc_delegate.dart';
import 'firebase/user_repository.dart';
import 'authentication_bloc/bloc.dart';
import 'login/login_page.dart';
void main(){
BlocSupervisor.delegate = SimpleBlocDelegate();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final UserRepository _userRepository = UserRepository();
AuthenticationBloc _authenticationBloc;
@override
void initState() {
_authenticationBloc = AuthenticationBloc(userRepository: _userRepository);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
builder: (BuildContext context) => _authenticationBloc,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocBuilder(
bloc: _authenticationBloc,
builder: (context, state){
if (state is Authenticated){
return Container();
}else if (state is Unauthenticated) {
return LoginPage();
}
return SplashPage();
},
),
)
);
}
}
開啟SplashPage完成最後一個步驟吧。
直接來看更改後的程式碼。
import 'package:flutter/material.dart';
import 'package:flare_splash_screen/flare_splash_screen.dart';
import 'authentication_bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SplashPage extends StatefulWidget {
@override
_SplashPageState createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
AuthenticationBloc _authenticationBloc;
@override
void initState() {
super.initState();
// 從Provider取得bloc
_authenticationBloc = BlocProvider.of<AuthenticationBloc>(context);
}
@override
Widget build(BuildContext context) {
// 先前是使用SplashScreen.navigate
// 現在改用callback,因為可以在onSuccess設定動畫結束後要做的事
return SplashScreen.callback(
name: 'assets/splash.flr', // flr動畫檔路徑
onSuccess: (_){_authenticationBloc.dispatch(AppStarted());}, // 動畫結束後觸發AppStarted事件
until: () => Future.delayed(Duration(seconds: 3)), //等待3秒
startAnimation: 'rotate_scale_color', // 動畫名稱
);
}
}
執行看看你的App吧,你會發現開頭一樣會有開場動畫,動畫結束會跳到LoginPage,但是背後都變成由Bloc來控制了。
在Logcat你也可以看見Transition,State確實是從Uninitialized
->Unauthenticated
。
今天花了很多的時間來實作AuthenticationBloc,對Bloc應該有更深的認識了,另外也使用到BlocProvider、BlocBuilder協助我們觸發event和監聽State的變化,希望你能感受到它們的簡單和方便!
完整程式碼在這裡-> FlutTube Github
明天繼續實作LoginBloc,大家再見。
想請問一下,我執行時,state狀態好像沒有改變,動畫跑完之後就卡住了,想問一下如何觀看state狀態,謝謝
你好,如果你有設定 SimpleBlocDelegate
的話,當state有變動應該就會有log。
不過我的這份code已經是一年多前的版本,Bloc這款套件也已經有做改版,寫法和許多細節都有所不同。
我建議你可以直接參考他的網站文件會比較適合唷
另外我在今年8月有嘗試用新版本寫,你可以參考https://github.com/JEN-YC/flutter-practice ,不過這部分就沒有教學了
好的唷~我再理解對比一下,謝謝