iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0
自我挑戰組

從零開始的Flutter世界系列 第 21

Day21 Flutter 的狀態管理 BLoC (五) Firebase Login

  • 分享至 

  • xImage
  •  

延續上一篇未完成的,首先我們來修改登入頁

登入頁:

我們使用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做介紹


上一篇
Day20 Flutter 的狀態管理 BLoC (四) Firebase Login
下一篇
Day22 Flutter 的狀態管理 Provider (一)
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言