延續上一篇未完成的,首先我們來修改登入頁
我們使用Cubit
,首先建立lib/screens/login/cubit
資料夾
在其建立login_state.dart
:
part of 'login_cubit.dart';
class LoginState {
final bool isSubmitting;
final bool isSuccess;
final bool isFailure;
final String errorMessage;
LoginState({
@required this.isSubmitting,
@required this.isSuccess,
@required this.isFailure,
this.errorMessage,
});
factory LoginState.empty() {
return LoginState(
isSubmitting: false,
isSuccess: false,
isFailure: false,
errorMessage: null,
);
}
factory LoginState.loading() {
return LoginState(
isSubmitting: true,
isSuccess: false,
isFailure: false,
errorMessage: null,
);
}
factory LoginState.failure(String message) {
return LoginState(
isSubmitting: false,
isSuccess: false,
isFailure: true,
errorMessage: message,
);
}
factory LoginState.success() {
return LoginState(
isSubmitting: false,
isSuccess: true,
isFailure: false,
errorMessage: null,
);
}
}
以及login_cubit.dart
:
import 'package:authentication_repository/authentication_repository.dart';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:travel_note/constants.dart';
part 'login_state.dart';
class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._authenticationRepository)
: assert(_authenticationRepository != null),
super(LoginState.empty());
final AuthenticationRepository _authenticationRepository;
Future<void> logInWithCredentials({
@required String email,
@required String password,
}) async {
emit(LoginState.loading());
try {
await _authenticationRepository.logInWithEmailAndPassword(
email: email,
password: password,
);
emit(LoginState.success());
} catch (e) {
emit(LoginState.failure(handleAuthError(e.message)));
}
}
Future<void> logInWithGoogle() async {
emit(LoginState.loading());
try {
await _authenticationRepository.logInWithGoogle();
emit(LoginState.success());
} catch (e) {
emit(LoginState.failure(handleAuthError(e.message)));
}
}
Future<void> logInWithFacebook() async {
emit(LoginState.loading());
try {
await _authenticationRepository.logInWithFacebook();
emit(LoginState.success());
} catch (e) {
emit(LoginState.failure(handleAuthError(e.message)));
}
}
}
之後我們先更新一下constants.dart
:
import 'package:flutter/material.dart';
import 'package:travel_note/size_config.dart';
const kPrimaryColor = Color(0xFF3E4067);
const kPrimaryLightColor = Color(0xFF3E5067);
const kTextColor = Color(0xFF757575);
const kAnimationDuration = Duration(milliseconds: 200);
final headingStyle = TextStyle(
fontSize: getProportionateScreenWidth(24),
fontWeight: FontWeight.bold,
color: Colors.black,
height: 1.5,
);
final contentStyle = TextStyle(
color: kTextColor,
height: 1.5,
fontSize: getProportionateScreenWidth(16),
);
// Form Error
Pattern pattern =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
final RegExp emailValidatorRegExp = new RegExp(pattern);
const String kEmailNullError = "Please Enter your email";
const String kInvalidEmailError = "Please Enter Valid Email";
const String kPasswordNullError = "Please Enter your password";
const String kShortPasswordError = "Password is too short";
const String kMatchPasswordError = "Passwords don't match";
const String kNameNullError = "Please Enter your name";
const String kPhoneNumberNullError = "Please Enter your phone number";
const String kAddressNullError = "Please Enter your address";
const String kConfirmPasswordNullError = "Please Confirm your password";
final otpInputDecoration = InputDecoration(
contentPadding:
EdgeInsets.symmetric(vertical: getProportionateScreenWidth(15)),
border: outlineInputBorder(),
focusedBorder: outlineInputBorder(),
enabledBorder: outlineInputBorder(),
);
OutlineInputBorder outlineInputBorder() {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(getProportionateScreenWidth(15)),
borderSide: BorderSide(color: kTextColor),
);
}
String handleAuthError(String message) {
String errorMessage;
switch (message) {
case 'There is no user record corresponding to this identifier. The user may have been deleted.':
errorMessage = "No corresponding identifier";
break;
case 'The password is invalid or the user does not have a password.':
errorMessage = "Invalid Password";
break;
case 'A network error (such as timeout, interrupted connection or unreachable host) has occurred.':
errorMessage = "Please check your network";
break;
default:
print("Unknown error:$message");
errorMessage = "Unknown error";
break;
}
return errorMessage;
}
main.dart
:
import 'package:authentication_repository/authentication_repository.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_note/routes.dart';
import 'package:travel_note/screens/splash/splash_screen.dart';
import 'package:travel_note/theme.dart';
import 'authentication/authentication_bloc.dart';
import 'screens/home/home_screen.dart';
import 'screens/login/login_screen.dart';
import 'simple_bloc_observer.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
Bloc.observer = SimpleBlocObserver();
runApp(MyApp(authenticationRepository: AuthenticationRepository()));
}
class MyApp extends StatelessWidget {
const MyApp({
Key key,
@required this.authenticationRepository,
}) : assert(authenticationRepository != null),
super(key: key);
final AuthenticationRepository authenticationRepository;
@override
Widget build(BuildContext context) {
return RepositoryProvider.value(
value: authenticationRepository,
child: BlocProvider(
create: (_) => AuthenticationBloc(
authenticationRepository: authenticationRepository,
),
child: AppView(),
),
);
}
}
class AppView extends StatefulWidget {
@override
_AppViewState createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState;
@override
Widget build(BuildContext context) {
return MaterialApp(
//拿掉畫面右上角的debug
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme(),
navigatorKey: _navigatorKey,
builder: (context, child) {
return BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
switch (state.status) {
case AuthenticationStatus.authenticated:
_navigator.pushNamed(HomeScreen.routeName);
break;
case AuthenticationStatus.unauthenticated:
_navigator.pushNamed(LoginScreen.routeName);
break;
default:
_navigator.pushNamed(SplashScreen.routeName);
break;
}
},
child: child,
);
},
/*
當底下的頁面有很多的時候,需要在 MaterialApp 中定義Routes 並且
同時設定 initialRoute,這樣進入 App 的時候,就會先進入 initRoutes,
再利用 Navigator 切換不同的頁面(Route)
initialRoute 是啓動APP的初始頁面,也就是用戶看到的第一個頁面
*/
initialRoute: SplashScreen.routeName,
routes: routes,
);
}
}
login_screen,dart
:
import 'package:authentication_repository/authentication_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_note/screens/login/components/body.dart';
import 'cubit/login_cubit.dart';
class LoginScreen extends StatelessWidget {
static String routeName = "/login";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Login'),
),
body: BlocProvider(
create: (_) => LoginCubit(
context.repository<AuthenticationRepository>(),
),
child: Body(),
),
);
}
}
login的body.dart
:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:travel_note/components/no_account_text.dart';
import 'package:travel_note/screens/login/cubit/login_cubit.dart';
import 'package:travel_note/size_config.dart';
import '../../../constants.dart';
import 'login_form.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state.isFailure) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(content: Text('Authentication Failure')),
);
}
},
child: SingleChildScrollView(
child: Padding(
padding:
EdgeInsets.symmetric(horizontal: getProportionateScreenWidth(25)),
child: Column(
children: [
Text(
"Welcome to Travel Note",
style: headingStyle,
),
VerticalSpacing(of: 16),
Text(
'Log in with your email and password \nor continue with social media',
textAlign: TextAlign.left,
style: contentStyle,
),
VerticalSpacing(of: 25),
LoginForm(),
VerticalSpacing(of: 25),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(MdiIcons.fromString("google")),
iconSize: 24,
onPressed: () =>
context.bloc<LoginCubit>().logInWithGoogle(),
),
IconButton(
icon: Icon(MdiIcons.fromString("facebook")),
iconSize: 24,
onPressed: () =>
context.bloc<LoginCubit>().logInWithFacebook(),
),
],
),
VerticalSpacing(of: 25),
NoAccountText(),
VerticalSpacing(of: 25),
],
),
),
),
);
}
}
login_form.dart
:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:travel_note/components/default_button.dart';
import 'package:travel_note/components/form_error.dart';
import 'package:travel_note/screens/forgot_password/forgot_password_screen.dart';
import 'package:travel_note/screens/login/cubit/login_cubit.dart';
import '../../../constants.dart';
import '../../../size_config.dart';
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
String email;
String password;
bool remember = false;
final List<String> errors = [];
void addError({String error}) {
if (!errors.contains(error))
setState(() {
errors.add(error);
});
}
void removeError({String error}) {
if (errors.contains(error))
setState(() {
errors.remove(error);
});
}
void removeAuthError() {
if (errors.isEmpty) {
return;
}
if (errors.contains("No corresponding identifier")) {
setState(() {
errors.remove("No corresponding identifier");
});
} else if (errors.contains("Invalid Password")) {
setState(() {
errors.remove("Invalid Password");
});
} else if (errors.contains("Please check your network")) {
setState(() {
errors.remove("Please check your network");
});
} else if (errors.contains("Unknown error")) {
setState(() {
errors.remove("Unknown error");
});
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
buildEmailFormField(),
VerticalSpacing(of: 25),
buildPasswordFormField(),
VerticalSpacing(of: 25),
Row(
children: [
Checkbox(
value: remember,
activeColor: kPrimaryColor,
onChanged: (value) {
setState(() {
remember = value;
});
},
),
Text("Remember me"),
Spacer(),
GestureDetector(
onTap: () {
Navigator.pushNamed(context, ForgotPasswordScreen.routeName);
},
child: Text(
"Forgot Password",
style: TextStyle(decoration: TextDecoration.underline),
),
)
],
),
BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state.isFailure) {
addError(error: state.errorMessage);
}
},
child: FormError(errors: errors),
),
VerticalSpacing(of: 25),
BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) {
return state.isSubmitting
? const CircularProgressIndicator()
: DefaultButton(
text: "Login",
press: () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
context.bloc<LoginCubit>().logInWithCredentials(
email: email, password: password);
}
},
);
},
),
],
),
);
}
TextFormField buildPasswordFormField() {
return TextFormField(
obscureText: true,
onSaved: (newValue) => password = newValue,
onChanged: (value) {
if (value.isNotEmpty) {
removeError(error: kPasswordNullError);
}
if (value.length >= 8) {
removeError(error: kShortPasswordError);
}
},
validator: (value) {
if (value.isEmpty) {
addError(error: kPasswordNullError);
removeError(error: kShortPasswordError);
removeAuthError();
return "";
} else if (value.length < 8) {
addError(error: kShortPasswordError);
removeAuthError();
return "";
}
removeAuthError();
return null;
},
decoration: InputDecoration(
labelText: "Password",
hintText: "Enter your password",
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: Icon(
MdiIcons.fromString("lock-outline"),
),
),
);
}
TextFormField buildEmailFormField() {
return TextFormField(
keyboardType: TextInputType.emailAddress,
onSaved: (newValue) => email = newValue,
onChanged: (value) {
if (value.isNotEmpty) {
removeError(error: kEmailNullError);
}
if (emailValidatorRegExp.hasMatch(value)) {
removeError(error: kInvalidEmailError);
}
},
validator: (value) {
if (value.isEmpty) {
addError(error: kEmailNullError);
removeError(error: kInvalidEmailError);
removeAuthError();
return "";
} else if (!emailValidatorRegExp.hasMatch(value)) {
addError(error: kInvalidEmailError);
removeAuthError();
return "";
}
removeAuthError();
return null;
},
decoration: InputDecoration(
labelText: "Email",
hintText: "Enter your email",
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: Icon(
MdiIcons.fromString("email-outline"),
),
),
);
}
}
其他註冊頁、忘記密碼頁,動作都類似,可以自己嘗試看看。
Bloc 的介紹就先告一段落了,多練習幾次,會慢慢掌握它的模式的,接下來我們將去對Provider做介紹