Simple BLoC Tutorial - Firebase Login
26 Jun 2020 |Tutorial: Firebase Login
Firebase와 연동시켜 회원가입, 로그인 등을 구현한 코드이다.
firebase 연동과 같은 부분은 제외하고 코드 설명
Setup
Pubspec.yaml
name: flutter_firebase_login
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.6.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
firebase_core: ^0.4.0+8
google_sign_in: ^4.0.0
firebase_auth: ^0.15.0+1
flutter_bloc: ^4.0.0
equatable: ^1.0.0
meta: ^1.1.6
rxdart: ^0.23.1
font_awesome_flutter: ^8.4.0
flutter:
uses-material-design: true
assets:
- assets/
User repository
Authentication과 User의 정보에 대해 다루는 부분이다. Bloc과 큰 관계는 없는 부분.
user_repository.dart
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
class UserRepository {
final FirebaseAuth _firebaseAuth;
final GoogleSignIn _googleSignIn;
UserRepository({FirebaseAuth firebaseAuth, GoogleSignIn googleSignin})
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
_googleSignIn = googleSignin ?? GoogleSignIn();
Future<FirebaseUser> signInWithGoogle() async {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final AuthCredential credential = GoogleAuthProvider.getCredential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await _firebaseAuth.signInWithCredential(credential);
return _firebaseAuth.currentUser();
}
Future<void> signInWithCredentials(String email, String password) {
return _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
}
Future<void> signUp({String email, String password}) async {
return await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
}
Future<void> signOut() async {
return Future.wait([
_firebaseAuth.signOut(),
_googleSignIn.signOut(),
]);
}
Future<bool> isSignedIn() async {
final currentUser = await _firebaseAuth.currentUser();
return currentUser != null;
}
Future<String> getUser() async {
return (await _firebaseAuth.currentUser()).email;
}
}
Authentication
Authentication에는 다음과 같은 파일이 필요하다.
authentication_bloc
폴더를 만들어 아래의 파일들을 만들어 주자.
├── authentication_bloc
│ ├── App(main.dart)
│ ├── simple_bloc_delegate.dart
│ ├── splash_screen.dart
│ ├── home_screen.dart
│ ├── authentication_bloc.dart
│ ├── authentication_event.dart
│ └── authentication_state.dart
authentication_state.dart
Authentication의 event에 따른 각각의 state에 대해 설정하는 곳이다.
Equatable은 AuthenticationState2개가 서로 같을 때에만 true를 return하는 package다.
part of 'authentication_bloc.dart';
abstract class AuthenticationState extends Equatable {
const AuthenticationState();
@override
List<Object> get props => [];
}
class AuthenticationInitial extends AuthenticationState {}
class AuthenticationSuccess extends AuthenticationState {
final String displayName;
const AuthenticationSuccess(this.displayName);
@override
List<Object> get props => [displayName];
@override
String toString() => 'AuthenticationSuccess { displayName: $displayName }';
}
class AuthenticationFailure extends AuthenticationState {}
authentication_event.dart
Authentication에서 일어날 수 있는 event들에 대해 선언하고 각 event class들에 대해 설정하는 부분이다.(변수나 함수 등)
part of 'authentication_bloc.dart';
abstract class AuthenticationEvent extends Equatable {
@override
List<Object> get props => [];
}
class AuthenticationStarted extends AuthenticationEvent {}
class AuthenticationLoggedIn extends AuthenticationEvent {}
class AuthenticationLoggedOut extends AuthenticationEvent {}
authentication_bloc.dart
Authentication에서 event가 발생하면 그 event에 맞게 state와 연결하는 부분이다.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_firebase_login/user_repository.dart';
part 'authentication_event.dart';
part 'authentication_state.dart';
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
final UserRepository _userRepository;
AuthenticationBloc({@required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository;
@override
AuthenticationState get initialState => AuthenticationInitial();
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
if (event is AuthenticationStarted) {
yield* _mapAuthenticationStartedToState();
} else if (event is AuthenticationLoggedIn) {
yield* _mapAuthenticationLoggedInToState();
} else if (event is AuthenticationLoggedOut) {
yield* _mapAuthenticationLoggedOutToState();
}
}
Stream<AuthenticationState> _mapAuthenticationStartedToState() async* {
final isSignedIn = await _userRepository.isSignedIn();
if (isSignedIn) {
final name = await _userRepository.getUser();
yield AuthenticationSuccess(name);
} else {
yield AuthenticationFailure();
}
}
Stream<AuthenticationState> _mapAuthenticationLoggedInToState() async* {
yield AuthenticationSuccess(await _userRepository.getUser());
}
Stream<AuthenticationState> _mapAuthenticationLoggedOutToState() async* {
yield AuthenticationFailure();
_userRepository.signOut();
}
}
main.dart
기본 of 기본인 곳이다. 여기서 authentication bloc을 만든다. bloc부분은 따로 분리해도 괜찮다.
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/authentication_bloc/authentication_bloc.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/home_screen.dart';
import 'package:flutter_firebase_login/login/login.dart';
import 'package:flutter_firebase_login/splash_screen.dart';
import 'package:flutter_firebase_login/simple_bloc_delegate.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
BlocSupervisor.delegate = SimpleBlocDelegate();
final UserRepository userRepository = UserRepository();
runApp(
BlocProvider(
create: (context) => AuthenticationBloc(userRepository: userRepository)
..add(AuthenticationStarted()),
child: App(userRepository: userRepository),
),
);
}
class App extends StatelessWidget {
final UserRepository _userRepository;
App({Key key, @required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository,
super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
if (state is AuthenticationInitial) {
return SplashScreen();
}
if (state is AuthenticationFailure) {
return LoginScreen(userRepository: _userRepository);
}
if (state is AuthenticationSuccess) {
return HomeScreen(name: state.displayName);
}
},
),
);
}
}
simple_bloc_delegate.dart
bloc에서 일어날 수 있는 일들에 대해 한꺼번에 다룬다. Event, Error, Transition이 일어날 때 마다 print만 시키는 간단한 일이다. BlocDelegate에 대해 어느정도 이해하고 감을 잡을 수 있다.
import 'package:bloc/bloc.dart';
class SimpleBlocDelegate extends BlocDelegate {
@override
void onEvent(Bloc bloc, Object event) {
print('Event');
print(event);
super.onEvent(bloc, event);
}
@override
void onError(Bloc bloc, Object error, StackTrace stackTrace) {
print('Error');
print(error);
super.onError(bloc, error, stackTrace);
}
@override
void onTransition(Bloc bloc, Transition transition) {
print('Transition');
print(transition);
super.onTransition(bloc, transition);
}
}
splash_screen.dart
authtication의 inital screen이다.
import 'package:flutter/material.dart';
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Splash Screen')),
);
}
}
home_screen.dart
authentication이 잘 되었을 시 나타나는 페이지이다. 로그인이 완료되면 여기로 온다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/authentication_bloc/authentication_bloc.dart';
class HomeScreen extends StatelessWidget {
final String name;
HomeScreen({Key key, @required this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () {
BlocProvider.of<AuthenticationBloc>(context).add(
AuthenticationLoggedOut(),
);
},
)
],
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Center(child: Text('Welcome $name!')),
],
),
);
}
}
Login
Login에는 다음과 같은 파일이 필요하다.
├── lib
│ ├── login
│ │ ├── bloc
│ │ │ ├── bloc.dart
│ │ │ ├── login_bloc.dart
│ │ │ ├── login_event.dart
│ │ │ └── login_state.dart
│ │ ├── login.dart
│ │ ├── login_screen.dart
│ │ ├── login_form.dart
│ │ ├── login_button.dart
│ │ ├── google_login_button.dart
│ │ └── create_account_button.dart
│ └── validator.dart
bloc.dart
Barrel file로 사용된다. Barrel file은 import를 해놓은 파일인데 폴더처럼 하나에다 넣고 관리하는 느낌을 생각하면 된다.
예를 들어 login_screen.dart에서 login_bloc.dart, login_event.dart를 import해야 하고 login_form.dart에서는 login_*.dart 파일 3개를 다 import해야한다 했을 때, bloc.dart만 import 시키면 bloc.dart에서 export 시킨 애들을 다 import하는 것과 같은 효과다.(사실 확실하지는… 않다ㅎ)
export 'login_bloc.dart';
export 'login_event.dart';
export 'login_state.dart';
login_state.dart
Login의 event에 따른 각각의 state에 대해 설정하는 곳이다.
import 'package:meta/meta.dart';
@immutable
class LoginState {
final bool isEmailValid;
final bool isPasswordValid;
final bool isSubmitting;
final bool isSuccess;
final bool isFailure;
bool get isFormValid => isEmailValid && isPasswordValid;
LoginState({
@required this.isEmailValid,
@required this.isPasswordValid,
@required this.isSubmitting,
@required this.isSuccess,
@required this.isFailure,
});
factory LoginState.initial() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: false,
isFailure: false,
);
}
factory LoginState.loading() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: true,
isSuccess: false,
isFailure: false,
);
}
factory LoginState.failure() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: false,
isFailure: true,
);
}
factory LoginState.success() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: true,
isFailure: false,
);
}
LoginState update({
bool isEmailValid,
bool isPasswordValid,
}) {
return copyWith(
isEmailValid: isEmailValid,
isPasswordValid: isPasswordValid,
isSubmitting: false,
isSuccess: false,
isFailure: false,
);
}
LoginState copyWith({
bool isEmailValid,
bool isPasswordValid,
bool isSubmitEnabled,
bool isSubmitting,
bool isSuccess,
bool isFailure,
}) {
return LoginState(
isEmailValid: isEmailValid ?? this.isEmailValid,
isPasswordValid: isPasswordValid ?? this.isPasswordValid,
isSubmitting: isSubmitting ?? this.isSubmitting,
isSuccess: isSuccess ?? this.isSuccess,
isFailure: isFailure ?? this.isFailure,
);
}
@override
String toString() {
return '''LoginState {
isEmailValid: $isEmailValid,
isPasswordValid: $isPasswordValid,
isSubmitting: $isSubmitting,
isSuccess: $isSuccess,
isFailure: $isFailure,
}''';
}
}
login_event.dart
Login에서 일어날 수 있는 event들에 대해 선언하고 각 event class들에 대해 설정하는 부분이다.
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
abstract class LoginEvent extends Equatable {
const LoginEvent();
@override
List<Object> get props => [];
}
class LoginEmailChanged extends LoginEvent {
final String email;
const LoginEmailChanged({@required this.email});
@override
List<Object> get props => [email];
@override
String toString() => 'LoginEmailChanged { email :$email }';
}
class LoginPasswordChanged extends LoginEvent {
final String password;
const LoginPasswordChanged({@required this.password});
@override
List<Object> get props => [password];
@override
String toString() => 'LoginPasswordChanged { password: $password }';
}
class LoginWithGooglePressed extends LoginEvent {}
class LoginWithCredentialsPressed extends LoginEvent {
final String email;
final String password;
const LoginWithCredentialsPressed({
@required this.email,
@required this.password,
});
@override
List<Object> get props => [email, password];
@override
String toString() {
return 'LoginWithCredentialsPressed { email: $email, password: $password }';
}
}
login_bloc.dart
Login에서 event가 발생하면 그 event에 맞게 state와 연결하는 부분이다.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import 'package:flutter_firebase_login/login/login.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/validators.dart';
class LoginBloc extends Bloc<LoginEvent, LoginState> {
UserRepository _userRepository;
LoginBloc({
@required UserRepository userRepository,
}) : assert(userRepository != null),
_userRepository = userRepository;
@override
LoginState get initialState => LoginState.initial();
@override
Stream<Transition<LoginEvent, LoginState>> transformEvents(
Stream<LoginEvent> events,
TransitionFunction<LoginEvent, LoginState> transitionFn,
) {
final nonDebounceStream = events.where((event) {
return (event is! LoginEmailChanged && event is! LoginPasswordChanged);
});
final debounceStream = events.where((event) {
return (event is LoginEmailChanged || event is LoginPasswordChanged);
}).debounceTime(Duration(milliseconds: 300));
return super.transformEvents(
nonDebounceStream.mergeWith([debounceStream]),
transitionFn,
);
}
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginEmailChanged) {
yield* _mapLoginEmailChangedToState(event.email);
} else if (event is LoginPasswordChanged) {
yield* _mapLoginPasswordChangedToState(event.password);
} else if (event is LoginWithGooglePressed) {
yield* _mapLoginWithGooglePressedToState();
} else if (event is LoginWithCredentialsPressed) {
yield* _mapLoginWithCredentialsPressedToState(
email: event.email,
password: event.password,
);
}
}
Stream<LoginState> _mapLoginEmailChangedToState(String email) async* {
yield state.update(
isEmailValid: Validators.isValidEmail(email),
);
}
Stream<LoginState> _mapLoginPasswordChangedToState(String password) async* {
yield state.update(
isPasswordValid: Validators.isValidPassword(password),
);
}
Stream<LoginState> _mapLoginWithGooglePressedToState() async* {
try {
await _userRepository.signInWithGoogle();
yield LoginState.success();
} catch (_) {
yield LoginState.failure();
}
}
Stream<LoginState> _mapLoginWithCredentialsPressedToState({
String email,
String password,
}) async* {
yield LoginState.loading();
try {
await _userRepository.signInWithCredentials(email, password);
yield LoginState.success();
} catch (_) {
yield LoginState.failure();
}
}
}
login.dart
Login bloc을 제외한 부분(presentation layer)의 barrel file이다.
export './create_account_button.dart';
export './bloc/bloc.dart';
export './login_form.dart';
export './google_login_button.dart';
export './login_button.dart';
export './login_screen.dart';
validators.dart
Email은 email형식을 따랐는지, Password는 비밀번호 조건을 만족하는지 확인하는 곳이다.
class Validators {
static final RegExp _emailRegExp = RegExp(
r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$',
);
static final RegExp _passwordRegExp = RegExp(
r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$',
);
static isValidEmail(String email) {
return _emailRegExp.hasMatch(email);
}
static isValidPassword(String password) {
return _passwordRegExp.hasMatch(password);
}
}
login_screen.dart
login 화면을 나타내는 부분이다. 여기서 LoginBloc을 만들어 준다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/login/login.dart';
class LoginScreen extends StatelessWidget {
final UserRepository _userRepository;
LoginScreen({Key key, @required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository,
super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
// LoginBloc을 만들고 LoginForm에 userRepository를 넘겨줌.
body: BlocProvider<LoginBloc>(
create: (context) => LoginBloc(userRepository: _userRepository),
child: LoginForm(userRepository: _userRepository),
),
);
}
}
login_form.dart
login을 할 때 필요한 form에 대한 부분이다. email, password에 대한 입력을 받고 이를 state로 bloc에 넘겨준다. 그리고 state 변화에 따라 UI가 같이 변하는 부분이다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/authentication_bloc/authentication_bloc.dart';
import 'package:flutter_firebase_login/login/login.dart';
class LoginForm extends StatefulWidget {
final UserRepository _userRepository;
LoginForm({Key key, @required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository,
super(key: key);
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
LoginBloc _loginBloc;
UserRepository get _userRepository => widget._userRepository;
bool get isPopulated =>
_emailController.text.isNotEmpty && _passwordController.text.isNotEmpty;
bool isLoginButtonEnabled(LoginState state) {
return state.isFormValid && isPopulated && !state.isSubmitting;
}
@override
void initState() {
super.initState();
// LoginScreen에서 만든 LoginBloc을 사용하겠다고 선언
_loginBloc = BlocProvider.of<LoginBloc>(context);
_emailController.addListener(_onLoginEmailChanged);
_passwordController.addListener(_onLoginPasswordChanged);
}
@override
Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
// State의 변화에 따른 현재 상태의 변화에 대한 알림. void 함수느낌 print를 하거나 widget에서 할 수 없는 일들을 처리
listener: (context, state) {
if (state.isFailure) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text('Login Failure'), Icon(Icons.error)],
),
backgroundColor: Colors.red,
),
);
}
if (state.isSubmitting) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Logging In...'),
CircularProgressIndicator(),
],
),
),
);
}
if (state.isSuccess) {
BlocProvider.of<AuthenticationBloc>(context).add(AuthenticationLoggedIn());
}
},
// State 변화에 따른 widget의 변화
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Form(
child: ListView(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Image.asset('assets/flutter_logo.png', height: 200),
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
icon: Icon(Icons.email),
labelText: 'Email',
),
keyboardType: TextInputType.emailAddress,
autovalidate: true,
autocorrect: false,
validator: (_) {
return !state.isEmailValid ? 'Invalid Email' : null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: 'Password',
),
obscureText: true,
autovalidate: true,
autocorrect: false,
validator: (_) {
return !state.isPasswordValid ? 'Invalid Password' : null;
},
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
LoginButton(
onPressed: isLoginButtonEnabled(state)
? _onFormSubmitted
: null,
),
GoogleLoginButton(),
CreateAccountButton(userRepository: _userRepository),
],
),
),
],
),
),
);
},
),
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _onLoginEmailChanged() {
_loginBloc.add(
LoginEmailChanged(email: _emailController.text),
);
}
void _onLoginPasswordChanged() {
_loginBloc.add(
LoginPasswordChanged(password: _passwordController.text),
);
}
void _onFormSubmitted() {
_loginBloc.add(
LoginWithCredentialsPressed(
email: _emailController.text,
password: _passwordController.text,
),
);
}
}
login_button.dart
그냥 button만 구현되어 있다. 눌렀을 때 login_form에 선언되어 있는 함수를 따른다.
import 'package:flutter/material.dart';
class LoginButton extends StatelessWidget {
final VoidCallback _onPressed;
LoginButton({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('Login'),
);
}
}
google_login_ button.dart
위랑 별 차이 없다. 하나 있다면 요놈은 form을 안거치고 여기서 바로 bloc으로 간다는 점.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/login/login.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class GoogleLoginButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton.icon(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
icon: Icon(FontAwesomeIcons.google, color: Colors.white),
onPressed: () {
// state가 다음과 같이 변할 것이다. login_bloc.dart에서 해당하는 event와 대응되는 transition이 생길 것.
BlocProvider.of<LoginBloc>(context).add(
LoginWithGooglePressed(),
);
},
label: Text('Sign in with Google', style: TextStyle(color: Colors.white)),
color: Colors.redAccent,
);
}
}
create_account_button.dart
역시 버튼일 뿐인데 register와 연결이 되는 것이 차이점이다.
import 'package:flutter/material.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/register/register.dart';
class CreateAccountButton extends StatelessWidget {
final UserRepository _userRepository;
CreateAccountButton({Key key, @required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository,
super(key: key);
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text(
'Create an Account',
),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return RegisterScreen(userRepository: _userRepository);
}),
);
},
);
}
}
Register
Register에는 다음과 같은 파일들이 필요하다.
├── lib
│ ├── register
│ │ ├── bloc
│ │ │ ├── bloc.dart
│ │ │ ├── register_bloc.dart
│ │ │ ├── register_event.dart
│ │ │ └── register_state.dart
│ │ ├── register.dart
│ │ ├── register_screen.dart
│ │ ├── register_form.dart
│ │ └── regitser_button.dart
bloc.dart
login에서의 bloc.dart와 역할의 차이가 없다.
export 'register_bloc.dart';
export 'register_event.dart';
export 'register_state.dart';
register_state.dart
Register의 event에 따른 각각의 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.initial() {
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 isSubmitEnabled,
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.dart
Register에서 일어날 수 있는 event들에 대해 선언하고 각 event class들에 대해 설정하는 부분이다.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
// Equatable은 AuthenticationState2개가 서로 같을 때에만 true를 return함.
abstract class RegisterEvent extends Equatable {
const RegisterEvent();
@override
List<Object> get props => [];
}
class RegisterEmailChanged extends RegisterEvent {
final String email;
const RegisterEmailChanged({@required this.email});
@override
List<Object> get props => [email];
@override
String toString() => 'RegisterEmailChanged { email :$email }';
}
class RegisterPasswordChanged extends RegisterEvent {
final String password;
const RegisterPasswordChanged({@required this.password});
@override
List<Object> get props => [password];
@override
String toString() => 'RegisterPasswordChanged { password: $password }';
}
class RegisterSubmitted extends RegisterEvent {
final String email;
final String password;
const RegisterSubmitted({
@required this.email,
@required this.password,
});
@override
List<Object> get props => [email, password];
@override
String toString() {
return 'RegisterSubmitted { email: $email, password: $password }';
}
}
register_bloc.dart
Register에서 event가 발생하면 그 event에 맞게 state와 연결하는 부분이다.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/register/register.dart';
import 'package:flutter_firebase_login/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.initial();
@override
Stream<Transition<RegisterEvent, RegisterState>> transformEvents(
Stream<RegisterEvent> events,
TransitionFunction<RegisterEvent, RegisterState> transitionFn,
) {
final nonDebounceStream = events.where((event) {
return (event is! RegisterEmailChanged && event is! RegisterPasswordChanged);
});
final debounceStream = events.where((event) {
return (event is RegisterEmailChanged || event is RegisterPasswordChanged);
}).debounceTime(Duration(milliseconds: 300));
return super.transformEvents(
nonDebounceStream.mergeWith([debounceStream]),
transitionFn,
);
}
// 여기서는 state의 변화를 줄 뿐 state의 변화에 의한 행동은 register_form과 같은 presentation layer에서 담당
@override
Stream<RegisterState> mapEventToState(
RegisterEvent event,
) async* {
if (event is RegisterEmailChanged) {
yield* _mapRegisterEmailChangedToState(event.email);
} else if (event is RegisterPasswordChanged) {
yield* _mapRegisterPasswordChangedToState(event.password);
} else if (event is RegisterSubmitted) {
yield* _mapRegisterSubmittedToState(event.email, event.password);
}
}
Stream<RegisterState> _mapRegisterEmailChangedToState(String email) async* {
yield state.update(
// validators.dart에 있는 함수의 return값이 isEmailValid안에 들어감.
isEmailValid: Validators.isValidEmail(email),
);
}
Stream<RegisterState> _mapRegisterPasswordChangedToState(String password) async* {
yield state.update(
isPasswordValid: Validators.isValidPassword(password),
);
}
Stream<RegisterState> _mapRegisterSubmittedToState(
String email,
String password,
) async* {
yield RegisterState.loading();
try {
await _userRepository.signUp(
email: email,
password: password,
);
yield RegisterState.success();
} catch (_) {
yield RegisterState.failure();
}
}
}
register.dart
presentation layer에 해당하는 부분의 barrel file이다.
export './bloc/bloc.dart';
export './register_button.dart';
export './register_screen.dart';
export './register_form.dart';
register_screen.dart
Scaffold를 만들어서 screen 화면을 띄우고 Bloc을 만드는 부분이다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/user_repository.dart';
import 'package:flutter_firebase_login/register/register.dart';
class RegisterScreen extends StatelessWidget {
final UserRepository _userRepository;
RegisterScreen({Key key, @required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository,
super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Register')),
body: Center(
// userRepository로 RegisterBloc을 생성하고 이를 RegisterForm에 넘겨줌
child: BlocProvider<RegisterBloc>(
create: (context) => RegisterBloc(userRepository: _userRepository),
child: RegisterForm(),
),
),
);
}
}
register_form.dart
email과 password에 대한 정보를 state로 RegisterScreen에서 만든 bloc에 전달해주고 state에 따른 widget과 상태의 변화를 적용하는 곳이다.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_firebase_login/authentication_bloc/authentication_bloc.dart';
import 'package:flutter_firebase_login/register/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();
// screen에서 만든 RegisterBloc을 child인 RegisterForm에서 사용하기 위해 선언
_registerBloc = BlocProvider.of<RegisterBloc>(context);
// RegisterBloc에 현재 text controller에 있는 state를 전달
// register_bloc.dart에 있는 event중 RegisterEmailChanged가 발생되었고 그 내용을 전달.
_emailController.addListener(_onEmailChanged);
_passwordController.addListener(_onPasswordChanged);
}
@override
Widget build(BuildContext context) {
// State의 변화가 생기면 state를 check. state에 따라 각기 다른 행동을 하게 함.
return BlocListener<RegisterBloc, RegisterState>(
// Listener가 state의 변화에 따라 특정 일을 하는 곳 함수느낌
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).add(AuthenticationLoggedIn());
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,
),
);
}
},
// RegisterBloc의 state에 따라 widget을 return
child: BlocBuilder<RegisterBloc, RegisterState>(
builder: (context, state) {
return Padding(
padding: EdgeInsets.all(20),
child: Form(
child: ListView(
children: <Widget>[
TextFormField(
controller: _emailController,
decoration: InputDecoration(
icon: Icon(Icons.email),
labelText: 'Email',
),
keyboardType: TextInputType.emailAddress,
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.add(
RegisterEmailChanged(email: _emailController.text),
);
}
void _onPasswordChanged() {
_registerBloc.add(
RegisterPasswordChanged(password: _passwordController.text),
);
}
void _onFormSubmitted() {
_registerBloc.add(
RegisterSubmitted(
email: _emailController.text,
password: _passwordController.text,
),
);
}
}
register_button.dart
그냥 버튼하나 딸랑
import 'package:flutter/material.dart';
// 그냥 버튼을 누르면 생긴 event를 전해줄 뿐.
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'),
);
}
}
이 글은 아래의 글을 참고하여 작성되었습니다.