iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 13
1
Mobile Development

Flutter---Google推出的跨平台框架,Android、iOS一起搞定系列 第 13

【Flutter基礎概念與實作】 Day13–實作Authentication Bloc

回到實作的專案中,上次的進度是停在建立UserRepository的地方,那麼今天就來把驗證帳號邏輯的「AuthenticationBloc」完成吧。

Bloc Code Generator Plugin

首先來安裝前天有提到的擴充套件(如果你是使用VS Code可以到這裡下載)。使用Android Studio的人直接在Plugin的地方搜尋就找的到囉。
這個套件讓我們只需輸入Bloc的名稱它就能產生bloceventstate三個模板可以更快的把bloc pattern 實作出來。

Authentication Bloc

新增一個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
│     ...
│

Authentication State

在開始實作state前要先思考在這個Bloc中會有哪些State。在驗證階段使用者會遇到的State應該會有以下三種:

  • uninitialized - App初始化階段
  • authenticated - 成功驗證成功者
  • unauthenticated - 未驗證

確定好有哪些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的轉換是如何。

Authentication Event

開始實作前一樣思考在驗證流程中,使用者會觸發哪些event以及每個event觸發後需要執行哪些商業邏輯。

  • AppStarted event - 檢查使用者現在驗證了沒
  • LoggedIn event - 進行登入的行為
  • LoggedOut 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觸發時能顯示更多資訊。

Authentication Bloc

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
  }
}

  1. 設定建構子
    由於登入登出要使用到UserRepository內的函式,所以要在建構子的地方傳入UserRepository的物件。
    assert會檢查傳入的userRepository是不是null,是的話會報錯。
final UserRepository _userRepository;
  AuthenticationBloc({UserRepository userRepository})
      : assert(userRepository != null),
        _userRepository = userRepository;
  1. 設定initialState是哪一個。
    把原本的InitialAuthenticationState更改成Uninitialized
 @override
  AuthenticationState get initialState => Uninitialized();
  1. Event和State的對應
@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();
  }
}
  1. 商業邏輯
    依照觸發的event執行對應的動作,例如AppStarted觸發時,檢查使用者是否登入了,並回傳AuthenticatedUnauthenticated
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的整個過程,

  1. 決定可能的State
  2. 會觸發哪些Event
  3. 設定初始State以及完成mapEventToState,將觸發的Event轉換到對應的State。

新增SimpleBlocDelegate

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');
  }
}

修改main

設定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

開啟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,大家再見。


上一篇
【Flutter基礎概念與實作】 Day12–Flutter Bloc 套件介紹 (2) BlocBuilder、BlocProvider和BlocListener
下一篇
【Flutter基礎概念與實作】 Day14–實作Login Bloc、Firebase Authentication
系列文
Flutter---Google推出的跨平台框架,Android、iOS一起搞定30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
曉楓
iT邦新手 5 級 ‧ 2020-12-04 19:10:22

想請問一下,我執行時,state狀態好像沒有改變,動畫跑完之後就卡住了,想問一下如何觀看state狀態,謝謝

你好,如果你有設定 SimpleBlocDelegate的話,當state有變動應該就會有log。
不過我的這份code已經是一年多前的版本,Bloc這款套件也已經有做改版,寫法和許多細節都有所不同。
我建議你可以直接參考他的網站文件會比較適合唷

另外我在今年8月有嘗試用新版本寫,你可以參考https://github.com/JEN-YC/flutter-practice ,不過這部分就沒有教學了

曉楓 iT邦新手 5 級 ‧ 2020-12-07 13:17:43 檢舉

好的唷~我再理解對比一下,謝謝

我要留言

立即登入留言