iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
0
自我挑戰組

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

Day15 Onboarding、Login、Sign Up (三)

  • 分享至 

  • xImage
  •  

今天我們來做忘記密碼頁和註冊頁,若是之前程式碼都是用複製貼上的,這次建議可以慢慢自己做看看,模式都差不多,這次的頁面也不會有新功能或新widget,就讓我們一起來試試看吧

新手小技巧分享:

  1. 還記得我們對屬性、類別的命名皆是駝峰式的,IDE (Android Studio)在我們從鍵盤輸入時,能夠依輸入的文字去搜尋建議的屬性或類別

    https://ithelp.ithome.com.tw/upload/images/20200930/20118479znRlKLQrzq.png

  2. 快速建立 StatelessWidget

    https://ithelp.ithome.com.tw/upload/images/20200930/20118479UpERDSF0wZ.png

  3. 快速建立 StatefulWidget

    https://ithelp.ithome.com.tw/upload/images/20200930/20118479mceGTuFq6G.png

  4. 快速import 資源,當程式碼出現未import的問題時,會在未import的類別等等文字下方出現紅色抖線,此時游標移到該位置並按下option鍵與enter鍵,就會出現推薦的解決方法,包括推薦的import

    https://ithelp.ithome.com.tw/upload/images/20200930/20118479GnlJiPBLL3.png

忘記密碼頁

首先一樣構想一下忘記密碼頁 :由上而下 AppBar,標題,描述文字,email 輸入框,送出的按鈕,註冊連結

整體而言都跟登入頁差不多,而且我們發現註冊連結,在我們做登入頁時,並沒有拉出來當共用widget,我們這邊補上

lib資料夾下的components資料夾中建立no_account_text.dart

no_account_text.dart

import 'package:flutter/material.dart';

import '../constants.dart';
import '../size_config.dart';

class NoAccountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          "Don’t have an account? ",
          style: TextStyle(fontSize: getProportionateScreenWidth(16)),
        ),
        GestureDetector(
          onTap: () {}, //導入SignUpScreen
          child: Text(
            "Sign Up",
            style: TextStyle(
                fontSize: getProportionateScreenWidth(16),
                decoration: TextDecoration.underline,
                color: kPrimaryColor),
          ),
        ),
      ],
    );
  }
}

記得登入頁的註冊也可以修改成 NoAccountText()

我們在lib資料夾下的screen資料夾建立我們的 forgot_password資料夾,用來放我們忘記密碼頁的程式,分別建forgot_password_screen.dart,再在 forgot_password資料夾建一個components資料夾放我們忘記密碼頁的自定義widget

而我們忘記密碼頁內容主要有我們剛剛想的:標題,描述文字,email 輸入框,送出的按鈕,註冊連結

其中比較複雜的應該是 email 輸入框以及送出的按鈕,它們需要幫我們在按送出時,檢查欄位是否正確,錯誤時還要顯示錯誤訊息,我們新建一個forgot_password_form.dart forgot_passwordcomponents裡,來把它拉出來另外做處理,可以參考登入頁的範例,試試看自己做

forgot_password_form.dart

import 'package:flutter/material.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/constants.dart';
import 'package:travel_note/size_config.dart';

class ForgotPasswordForm extends StatefulWidget {
  @override
  _ForgotPasswordFormState createState() => _ForgotPasswordFormState();
}

class _ForgotPasswordFormState extends State<ForgotPasswordForm> {
  final _formKey = GlobalKey<FormState>();
  List<String> errors = [];
  String email;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            keyboardType: TextInputType.emailAddress,
            onSaved: (newValue) => email = newValue,
            onChanged: (value) {
              if (value.isNotEmpty && errors.contains(kEmailNullError)) {
                setState(() {
                  errors.remove(kEmailNullError);
                });
              }
              if (emailValidatorRegExp.hasMatch(value) &&
                  errors.contains(kInvalidEmailError)) {
                setState(() {
                  errors.remove(kInvalidEmailError);
                });
              }
            },
            validator: (value) {
              if (value.isEmpty && !errors.contains(kEmailNullError)) {
                setState(() {
                  errors.add(kEmailNullError);
                  if (errors.contains(kInvalidEmailError)) {
                    errors.remove(kInvalidEmailError);
                  }
                });
                return "";
              } else if (value.isNotEmpty &&
                  !emailValidatorRegExp.hasMatch(value) &&
                  !errors.contains(kInvalidEmailError)) {
                setState(() {
                  errors.add(kInvalidEmailError);
                });
                return "";
              }
              return null;
            },
            decoration: InputDecoration(
              labelText: "Email",
              hintText: "Enter your email",
              floatingLabelBehavior: FloatingLabelBehavior.always,
              suffixIcon: Icon(
                MdiIcons.fromString("email-outline"),
              ),
            ),
          ),
          VerticalSpacing(of: 40),
          FormError(errors: errors),
          VerticalSpacing(of: 25),
          DefaultButton(
            text: "Submit",
            press: () {
              if (_formKey.currentState.validate()) {
                _formKey.currentState.save();
                // 傳送密碼至mail 驗證等等
              }
            },
          )
        ],
      ),
    );
  }
}

之後我們來完成忘記密碼頁的內容, 在 forgot_passwordcomponents裡新建body.dart

依序分別為剛剛構想的設計畫面

body.dart

import 'package:flutter/material.dart';
import 'package:travel_note/components/no_account_text.dart';
import 'package:travel_note/constants.dart';
import 'package:travel_note/screens/forgot_password/components/forgot_password_form.dart';
import 'package:travel_note/size_config.dart';

class Body extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Padding(
        padding:
            EdgeInsets.symmetric(horizontal: getProportionateScreenWidth(25)),
        child: Column(
          children: [
            Text(
              "Forgot Password",
              style: TextStyle(
                fontSize: getProportionateScreenWidth(24),
                color: Colors.black,
                fontWeight: FontWeight.bold,
              ),
            ),
            VerticalSpacing(of: 16),
            Text(
              "Please enter your email and we will send you a link to return to your account",
              textAlign: TextAlign.left,
              style: TextStyle(
                color: kTextColor,
                height: 1.5,
                fontSize: getProportionateScreenWidth(16),
              ),
            ),
            VerticalSpacing(of: 25),
            ForgotPasswordForm(),
            VerticalSpacing(of: 25),
            NoAccountText(),
            VerticalSpacing(of: 25),
          ],
        ),
      ),
    );
  }
}

之後在forgot_password_screen.dart裡串上就完成我們忘記密碼頁

forgot_password_screen.dart

import 'package:flutter/material.dart';
import 'package:travel_note/screens/forgot_password/components/body.dart';

class ForgotPasswordScreen extends StatelessWidget {
  static String routeName = "/forgot_password";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Forgot Password"),
      ),
      body: Body(),
    );
  }
}

最後記得在routes.dart補上連結,還有在登入頁,補上點擊忘記密碼時的事件,就都完成了!

接下來就來完成我們的註冊頁,這樣我們app 專案的入口畫面也就完成了

註冊頁

首先我們要的畫面有:

由上而下 AppBar,標題,描述文字,email 輸入框,密碼輸入框,確認密碼輸入框,送出的按鈕,透過社群軟體登入的UI

我們一樣發現有重複的widget 未被提出來共用 (透過社群軟體登入的UI),故拉出來在/lib/components,新建

social_media.dart

import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';

class SocialMedia extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        IconButton(
            icon: Icon(MdiIcons.fromString("google")),
            iconSize: 24,
            onPressed: () {}),
        IconButton(
            icon: Icon(MdiIcons.fromString("facebook")),
            iconSize: 24,
            onPressed: () {}),
        IconButton(
            icon: Icon(MdiIcons.fromString("twitter")),
            iconSize: 24,
            onPressed: () {}),
      ],
    );
  }
}

登入頁的社群軟體登入的UI也可以修改成 SocialMedia()

我們建立註冊頁要用的資料夾:lib/screens/sign_uplib/screens/sign_up/components

並新建註冊的lib/screens/sign_up/sign_up_screen.dartlib/screens/sign_up/components/body.dartlib/screens/sign_up/components/sign_up_form.dart

sign_up_form.dart

import 'package:flutter/material.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/constants.dart';
import 'package:travel_note/size_config.dart';

class SignUpForm extends StatefulWidget {
  @override
  _SignUpFormState createState() => _SignUpFormState();
}

class _SignUpFormState extends State<SignUpForm> {
  final _formKey = GlobalKey<FormState>();
  String email;
  String password;
  String confirmPassword;
  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);
      });
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          buildEmailFormField(),
          VerticalSpacing(of: 25),
          buildPasswordFormField(),
          VerticalSpacing(of: 25),
          buildConfirmPasswordFormField(),
          VerticalSpacing(of: 40),
          FormError(errors: errors),
          VerticalSpacing(of: 25),
          DefaultButton(
            text: "Sign Up",
            press: () {
              if (_formKey.currentState.validate()) {
                _formKey.currentState.save();
                // if all are valid then go to success screen
                // Navigator.pushNamed(context, LoginSuccessScreen.routeName);
              }
            },
          ),
        ],
      ),
    );
  }

  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);
          return "";
        } else if (!emailValidatorRegExp.hasMatch(value)) {
          addError(error: kInvalidEmailError);
          return "";
        }
        return null;
      },
      decoration: InputDecoration(
        labelText: "Email",
        hintText: "Enter your email",
        floatingLabelBehavior: FloatingLabelBehavior.always,
        suffixIcon: Icon(
          MdiIcons.fromString("email-outline"),
        ),
      ),
    );
  }

  TextFormField buildPasswordFormField() {
    return TextFormField(
      obscureText: true,
      onSaved: (newValue) => password = newValue,
      onChanged: (value) {
        password = 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);
          return "";
        } else if (value.length < 8) {
          addError(error: kShortPasswordError);
          return "";
        }
        return null;
      },
      decoration: InputDecoration(
        labelText: "Password",
        hintText: "Enter your password",
        floatingLabelBehavior: FloatingLabelBehavior.always,
        suffixIcon: Icon(
          MdiIcons.fromString("lock-outline"),
        ),
      ),
    );
  }

  TextFormField buildConfirmPasswordFormField() {
    return TextFormField(
      obscureText: true,
      onSaved: (newValue) => confirmPassword = newValue,
      onChanged: (value) {
        if (value.isNotEmpty) {
          removeError(error: kConfirmPasswordNullError);
        }
        print("value $value, password $password");
        if (value == password) {
          removeError(error: kMatchPasswordError);
        }
      },
      validator: (value) {
        if (value.isEmpty) {
          addError(error: kConfirmPasswordNullError);
          removeError(error: kMatchPasswordError);
          return "";
        } else if (value != password) {
          addError(error: kMatchPasswordError);
          return "";
        }
        return null;
      },
      decoration: InputDecoration(
        labelText: "Confirm Password",
        hintText: "Re-enter your password",
        floatingLabelBehavior: FloatingLabelBehavior.always,
        suffixIcon: Icon(
          MdiIcons.fromString("lock-outline"),
        ),
      ),
    );
  }
}

body.dart

import 'package:flutter/material.dart';
import 'package:travel_note/components/social_media.dart';
import 'package:travel_note/constants.dart';
import 'package:travel_note/screens/sign_up/components/sign_up_form.dart';
import 'package:travel_note/size_config.dart';

class Body extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Padding(
        padding:
            EdgeInsets.symmetric(horizontal: getProportionateScreenWidth(25)),
        child: Column(
          children: [
            Text(
              "Register Account",
              style: headingStyle,
            ),
            VerticalSpacing(of: 16),
            Text(
              "Sign up with your email or continue with social media",
              style: contentStyle,
            ),
            VerticalSpacing(of: 25),
            SignUpForm(),
            VerticalSpacing(of: 25),
            SocialMedia(),
            VerticalSpacing(of: 25),
          ],
        ),
      ),
    );
  }
}

sign_up_screen.dart

import 'package:flutter/material.dart';
import 'package:travel_note/screens/sign_up/components/body.dart';

class SignUpScreen extends StatelessWidget {
  static String routeName = "/sign_up";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sign Up"),
      ),
      body: Body(),
    );
  }
}

最後記得一樣在routes.dart補上連結,還有在登入頁、忘記密碼頁,補上點擊註冊時的事件,就都完成了!

我們完成了一個app 的登入、註冊等的畫面,但功能我們都還沒串上,為了之後要使用 Firebase Authentication,來讓我們完成一個簡單的會員系統,下一篇我們先來介紹 Firebase


上一篇
Day14 Onboarding、Login、Sign Up (二)
下一篇
Day16 Firebase
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言