iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
2
Mobile Development

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

【Flutter基礎概念與實作】 Day15–實作Register Bloc、Firebase Authentication

  • 分享至 

  • xImage
  •  

昨天完成了LoginBloc和登入的介面,但其實還有幾個步驟需要進行設定才能使用Google Auth,所以今天的前半部先把它設定完吧。

Android build.gradle

開啟android/app/build.gradle
在dependencies處加上implementation 'com.google.firebase:firebase-core:17.0.0'
並在最下方加上apply plugin: 'com.google.gms.google-services'

Migrating to AndroidX

AndroidX replaces the original support library APIs with packages in the androidx namespace.
https://developer.android.com/jetpack/androidx/migrate

開啟android/gradle.properties加上

android.useAndroidX=true
android.enableJetifier=true

開啟android/build.gradle把

dependencies {
    classpath 'com.android.tools.build:gradle:3.2.1'
}

換成

dependencies {
    classpath 'com.android.tools.build:gradle:3.3.0'
}

申請OAuth2憑證

  1. 申請網頁,登入的Google帳號要和設定Firebase時用的一樣
  2. 在上方選擇你的專案(和Firebase內的專案名稱一樣)
  3. 到「OAuth同意畫面」填入所有表格資訊,圖片可以隨便上傳,最下方三個欄位要用http或https開頭
  4. 填完按下儲存就可以了,不須送交驗證
  5. 到「憑證」確認API金鑰和OAuth2用戶端都有自動被產生

以上步驟設定完後,你應該就能夠使用Google帳號登入App了。
今天後半部要做的我想大家都猜的到就是要把最後的RegisterBloc完成,使用者就可以選擇要用信箱進行註冊登入或是直接用Google帳號登入啦。


Register Bloc

老樣子,在register資料夾下新增「bloc」資料夾並用bloc generator產生RegisterBloc的模板。

fluttube
└───lib
│   └───login
│   └───register
│   │   └───bloc
│   │   │   └───bloc.dart
│   │   │   └───register_bloc.dart
│   │   │   └───register_event.dart
│   │   │   └───register_state.dart
│   │   └───register_page.dart
│   ...
│   └─── validators.dart

Register State

註冊的State和登入的一樣

import 'package:meta/meta.dart';

@immutable
class RegisterState {
  final bool isEmailValid;
  final bool isPasswordValid;
  final bool isSubmitting;
  final bool isSuccess;
  final bool isFailure;

  bool get isFormValid => isEmailValid && isPasswordValid;

  RegisterState({
    @required this.isEmailValid,
    @required this.isPasswordValid,
    @required this.isSubmitting,
    @required this.isSuccess,
    @required this.isFailure,
  });

  factory RegisterState.empty() {
    return RegisterState(
      isEmailValid: true,
      isPasswordValid: true,
      isSubmitting: false,
      isSuccess: false,
      isFailure: false,
    );
  }

  factory RegisterState.loading() {
    return RegisterState(
      isEmailValid: true,
      isPasswordValid: true,
      isSubmitting: true,
      isSuccess: false,
      isFailure: false,
    );
  }

  factory RegisterState.failure() {
    return RegisterState(
      isEmailValid: true,
      isPasswordValid: true,
      isSubmitting: false,
      isSuccess: false,
      isFailure: true,
    );
  }

  factory RegisterState.success() {
    return RegisterState(
      isEmailValid: true,
      isPasswordValid: true,
      isSubmitting: false,
      isSuccess: true,
      isFailure: false,
    );
  }

  RegisterState update({
    bool isEmailValid,
    bool isPasswordValid,
  }) {
    return copyWith(
      isEmailValid: isEmailValid,
      isPasswordValid: isPasswordValid,
      isSubmitting: false,
      isSuccess: false,
      isFailure: false,
    );
  }

  RegisterState copyWith({
    bool isEmailValid,
    bool isPasswordValid,
    bool isSubmitting,
    bool isSuccess,
    bool isFailure,
  }) {
    return RegisterState(
      isEmailValid: isEmailValid ?? this.isEmailValid,
      isPasswordValid: isPasswordValid ?? this.isPasswordValid,
      isSubmitting: isSubmitting ?? this.isSubmitting,
      isSuccess: isSuccess ?? this.isSuccess,
      isFailure: isFailure ?? this.isFailure,
    );
  }

  @override
  String toString() {
    return '''RegisterState {
      isEmailValid: $isEmailValid,
      isPasswordValid: $isPasswordValid,
      isSubmitting: $isSubmitting,
      isSuccess: $isSuccess,
      isFailure: $isFailure,
    }''';
  }
}

Register Event

同樣的註冊的Event和登入的一樣。

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

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

class EmailChanged extends RegisterEvent {
  final String email;

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

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

class PasswordChanged extends RegisterEvent {
  final String password;

  PasswordChanged({@required this.password}) : super([password]);

  @override
  String toString() => 'PasswordChanged { password: $password }';
}

class Submitted extends RegisterEvent {
  final String email;
  final String password;

  Submitted({@required this.email, @required this.password})
      : super([email, password]);

  @override
  String toString() {
    return 'Submitted { email: $email, password: $password }';
  }
}

Register Bloc

一樣需要實作initialStatemapEventToState,基本上都和LoginBloc長的一樣。

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import '../../firebase/user_repository.dart';
import 'bloc.dart';
import '../../validators.dart';

class RegisterBloc extends Bloc<RegisterEvent, RegisterState> {
  final UserRepository _userRepository;

  RegisterBloc({@required UserRepository userRepository})
      : assert(userRepository != null),
        _userRepository = userRepository;

  @override
  RegisterState get initialState => RegisterState.empty();

  @override
  Stream<RegisterState> transformEvents(
      Stream<RegisterEvent> events,
      Stream<RegisterState> Function(RegisterEvent event) next,
      ) {
    final observableStream = events as Observable<RegisterEvent>;
    final nonDebounceStream = observableStream.where((event) {
      return (event is! EmailChanged && event is! PasswordChanged);
    });
    final debounceStream = observableStream.where((event) {
      return (event is EmailChanged || event is PasswordChanged);
    }).debounceTime(Duration(milliseconds: 300));
    return super.transformEvents(nonDebounceStream.mergeWith([debounceStream]), next);
  }

  @override
  Stream<RegisterState> mapEventToState(
      RegisterEvent event,
      ) async* {
    if (event is EmailChanged) {
      yield* _mapEmailChangedToState(event.email);
    } else if (event is PasswordChanged) {
      yield* _mapPasswordChangedToState(event.password);
    } else if (event is Submitted) {
      yield* _mapFormSubmittedToState(event.email, event.password);
    }
  }

  Stream<RegisterState> _mapEmailChangedToState(String email) async* {
    yield currentState.update(
      isEmailValid: Validators.isValidEmail(email),
    );
  }

  Stream<RegisterState> _mapPasswordChangedToState(String password) async* {
    yield currentState.update(
      isPasswordValid: Validators.isValidPassword(password),
    );
  }

  Stream<RegisterState> _mapFormSubmittedToState(
      String email,
      String password,
      ) async* {
    yield RegisterState.loading();
    try {
      await _userRepository.signUp(
        email: email,
        password: password,
      );
      yield RegisterState.success();
    } catch (_) {
      yield RegisterState.failure();
    }
  }
}

Register Page

修改register_page.dart,和LoginPage一樣使用BlocProvider讓其他widget也能使用到registerbloc。

import 'package:flutter/material.dart';
import '../firebase/user_repository.dart';
import 'bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class RegisterPage extends StatelessWidget {
  final UserRepository _userRepository;
  RegisterPage({Key key, @required UserRepository userRepository})
      : assert(userRepository != null),
        _userRepository = userRepository,
        super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: BlocProvider(
      builder: (BuildContext content) =>
          RegisterBloc(userRepository: _userRepository),
      child: RegisterForm(),
    ));
  }
}

Register Form

新增register_form.dart
同樣使用TextEditingController監聽使用者的輸入,檢查是否符合規定。要注意最後要呼叫dispose()將它關閉。

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../authentication_bloc/bloc.dart';
import 'register.dart';

class RegisterForm extends StatefulWidget {
  State<RegisterForm> createState() => _RegisterFormState();
}

class _RegisterFormState extends State<RegisterForm> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  RegisterBloc _registerBloc;

  bool get isPopulated =>
      _emailController.text.isNotEmpty && _passwordController.text.isNotEmpty;

  bool isRegisterButtonEnabled(RegisterState state) {
    return state.isFormValid && isPopulated && !state.isSubmitting;
  }

  @override
  void initState() {
    super.initState();
    _registerBloc = BlocProvider.of<RegisterBloc>(context);
    _emailController.addListener(_onEmailChanged);
    _passwordController.addListener(_onPasswordChanged);
  }

  @override
  Widget build(BuildContext context) {
    return BlocListener<RegisterBloc, RegisterState>(
      listener: (context, state) {
        if (state.isSubmitting) {
          Scaffold.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(
              SnackBar(
                content: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('Registering...'),
                    CircularProgressIndicator(),
                  ],
                ),
              ),
            );
        }
        if (state.isSuccess) {
          BlocProvider.of<AuthenticationBloc>(context).dispatch(LoggedIn());
          Navigator.of(context).pop();
        }
        if (state.isFailure) {
          Scaffold.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(
              SnackBar(
                content: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('Registration Failure'),
                    Icon(Icons.error),
                  ],
                ),
                backgroundColor: Colors.red,
              ),
            );
        }
      },
      child: BlocBuilder<RegisterBloc, RegisterState>(
        builder: (context, state) {
          return Padding(
            padding: EdgeInsets.all(20),
            child: Form(
              child: ListView(
                children: <Widget>[
                  Padding(
                    padding: EdgeInsets.symmetric(vertical: 5.0),
                    child: Image.asset(
                      'assets/logo.png',
                      height: 200,
                    ),
                  ),
                  Padding(
                      padding: EdgeInsets.only(bottom: 30.0),
                      child: Center(
                          child: Text(
                        "註 冊 帳 號",
                        style: TextStyle(
                            fontSize: 40,
                            color: Colors.brown,
                            fontStyle: FontStyle.italic),
                      ))),
                  TextFormField(
                    controller: _emailController,
                    decoration: InputDecoration(
                      icon: Icon(Icons.email),
                      labelText: 'Email',
                    ),
                    autocorrect: false,
                    autovalidate: true,
                    validator: (_) {
                      return !state.isEmailValid ? 'Invalid Email' : null;
                    },
                  ),
                  TextFormField(
                    controller: _passwordController,
                    decoration: InputDecoration(
                      icon: Icon(Icons.lock),
                      labelText: 'Password',
                    ),
                    obscureText: true,
                    autocorrect: false,
                    autovalidate: true,
                    validator: (_) {
                      return !state.isPasswordValid ? 'Invalid Password' : null;
                    },
                  ),
                  RegisterButton(
                    onPressed: isRegisterButtonEnabled(state)
                        ? _onFormSubmitted
                        : null,
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _onEmailChanged() {
    _registerBloc.dispatch(
      EmailChanged(email: _emailController.text),
    );
  }

  void _onPasswordChanged() {
    _registerBloc.dispatch(
      PasswordChanged(password: _passwordController.text),
    );
  }

  void _onFormSubmitted() {
    _registerBloc.dispatch(
      Submitted(
        email: _emailController.text,
        password: _passwordController.text,
      ),
    );
  }
}

Register Button

新增register_button.dart實作註冊按鈕。
除了文字外都和LoginButton相同。

import 'package:flutter/material.dart';

class RegisterButton extends StatelessWidget {
  final VoidCallback _onPressed;

  RegisterButton({Key key, VoidCallback onPressed})
      : _onPressed = onPressed,
        super(key: key);
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(30.0),
      ),
      onPressed: _onPressed,
      child: Text('Register'),
    );
  }
}

引入整個register資料夾

新增register.dart
貼上以下程式碼:

export 'bloc/bloc.dart';
export 'register_form.dart';
export 'register_page.dart';
export 'register_button.dart';

以上將RegisterBloc和RegisterPage都實作完成,之後使用者可以用他的信箱註冊App,使用者的資料都交由Firebase幫我們處理。你可以在Firebase的專案主控台管理使用者,非常簡單吧。

HomePage

目前登入進去的頁面只有顯示Home Page幾個字,為了測試方便先簡單做登出功能的按鈕吧。
新增home資料夾並新增home_page.dart
貼上以下程式碼:

import 'package:flutter/material.dart';
import '../authentication_bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () =>
            BlocProvider.of<AuthenticationBloc>(context).dispatch(LoggedOut()),
        label: Text("Logout"),
        icon: Icon(Icons.arrow_back),
      ),
    );
  }
}

修改main

開啟main.dart引入home_page.dart
把BlocBuilder修改成以下:

BlocBuilder(
    bloc: _authenticationBloc,
    builder: (context, state) {
    	if (state is Authenticated) {
        	return HomePage();
    	} else if (state is Unauthenticated) {
        	return LoginPage(userRepository: _userRepository,);
    	}
        	return SplashPage();
    },
          )

今天的目錄架構

fluttube
└───lib
│   └───home
│   │   └───home_page.dart
│   └───register
│   │   └───bloc
│   │   │   └───bloc.dart
│   │   │   └───register_bloc.dart
│   │   │   └───register_event.dart
│   │   │   └───register_state.dart
│   │   └───register.dart
│   │   └───register_button.dart
│   │   └───register_form.dart
│   │   └───register_page.dart
│   └───main.dart
│   ...
│   └─── validators.dart

今日總結

花了幾天時間把基本的登入註冊流程使用bloc design pattern實作出來,由於Firebase提供的方便服務讓我們可以專注在介面以及功能開發,不用思考建立管理資料庫的部分。

實作bloc的程式碼都是參考至felangel的bloc教學,他還有提供其他有趣的範例,非常推薦可以去試試。

明天來介紹TMDb Api,看看裡面有哪些資料是可以應用在FlutTube的。

完整程式碼在這裡-> FlutTube Github


上一篇
【Flutter基礎概念與實作】 Day14–實作Login Bloc、Firebase Authentication
下一篇
【Flutter基礎概念與實作】 Day16–使用SharedPreference記下帳號、接上TMDb API
系列文
Flutter---Google推出的跨平台框架,Android、iOS一起搞定30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言