開啟android/app/build.gradle
在dependencies處加上implementation 'com.google.firebase:firebase-core:17.0.0'
並在最下方加上apply plugin: 'com.google.gms.google-services'
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'
}
以上步驟設定完後,你應該就能夠使用Google帳號登入App了。
今天後半部要做的我想大家都猜的到就是要把最後的RegisterBloc完成,使用者就可以選擇要用信箱進行註冊登入或是直接用Google帳號登入啦。
老樣子,在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
註冊的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,
}''';
}
}
同樣的註冊的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 }';
}
}
一樣需要實作initialState
和mapEventToState
,基本上都和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.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.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.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.dart
貼上以下程式碼:
export 'bloc/bloc.dart';
export 'register_form.dart';
export 'register_page.dart';
export 'register_button.dart';
以上將RegisterBloc和RegisterPage都實作完成,之後使用者可以用他的信箱註冊App,使用者的資料都交由Firebase幫我們處理。你可以在Firebase的專案主控台管理使用者,非常簡單吧。
目前登入進去的頁面只有顯示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.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