Flutter : ทำระบบสมาชิกและ Login ด้วย Firebase Auth
สวัสดีผู้อ่านครับ หากได้ติดตามบล็อก งานเขียนของผม จะเห็นว่าช่วงนี้มีแต่เรื่อง Flutter เยอะมาก นั่นก็เพราะว่าผมลองเขียน Flutter มาสักพักแล้ว ยังมีเรื่องให้ลองเล่นอีกหลายเรื่องเลย บล็อกนี้ได้เวลาของ Firebase ซึ่งนับว่าเป็นเรื่องที่ mobile dev สมัยใหม่ล้วนต้องรู้ทุกคน โดยผมจะเริ่มจาก Firebase Auth ก่อน
รู้จักกับ Firebase Auth
Firebase Auth เป็นบริการหนึ่งใน Firebase ที่รวบรวมบริการ backend สำหรับแอพและเว็บแอพเอาไว้ มีความสามารถเยอะมาก ช่วยให้นักพัฒนาไม่ต้องทำ backend เอง ซึ่งในบล็อกนี่จะพาไปลองเล่นบริการนึงของ Firebase คือ Firebase Auth นี่แหละ โดยตัวมันช่วยให้การทำระบบสมาชิก ล็อคอินเป็นเรื่องง่าย ซึ่งการใช้งาน Firebase Auth ใน Flutter ก็ไม่ได้ยากเลย
สามารถล็อคอินโดยใช้ gmail ได้เลย แล้วก็สร้างโปรเจค
https://firebase.google.com/
ติดตั้ง Firebase ใน Flutter project
ติดตั้ง Firebase ใน project ของเรา ขั้นตอนคือ เอาไฟล์ของ firebase ไปใส่ใน project android และ iOS แล้วก็เพิ่ม plugin บางอย่าง ซึ่งสามารถอ่านได้ที่
https://firebase.google.com/docs/android/setup
https://firebase.google.com/docs/ios/setup
ของ Android ทำถึงแค่ step3 นะ
เปิดใช้งาน Firebase Auth
หลังจากที่เราล็อคอินเข้ามาในเว็บ firebase แล้ว ไปที่เมนู authentication ให้ enable email provider ที่ sign-in method
ลองสร้าง user มา 1 user
เพิ่ม dependencies
กลับมาที่ Flutter project เพิ่ม dependencies ของ Firebase Auth เข้ามาใน project ที่ไฟล์ pubspec.yaml
dependencies:
firebase_core: 0.3.1+1
firebase_auth: 0.8.1+4
..
สร้างหน้าล็อคอิน
สร้างหน้าจอ UI หน้าล็อคอิน ที่มีช่องกรอก email , password แล้วก็มีปุ่ม login ด้วย บล็อกนี้ขอแต่งให้ดูดีกว่าปกตินิดนึงละกัน
class MyLoginPage extends StatefulWidget {
MyLoginPage({Key key}) : super(key: key);
@override
_MyLoginPageState createState() => _MyLoginPageState();
}
class _MyLoginPageState extends State<MyLoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My Firebase App", style: TextStyle(color: Colors.white)),
),
body: Container(
color: Colors.green[50],
child: Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.yellow[100], Colors.green[100]])),
margin: EdgeInsets.all(32),
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
buildTextFieldEmail(),
buildTextFieldPassword(),
buildButtonSignIn(),
],
)),
)));
}
Container buildButtonSignIn() {
return Container(
constraints: BoxConstraints.expand(height: 50),
child: Text("Sign in",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.white)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), color: Colors.green[200]),
margin: EdgeInsets.only(top: 16),
padding: EdgeInsets.all(12));
}
Container buildTextFieldEmail() {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.yellow[50], borderRadius: BorderRadius.circular(16)),
child: TextField(
decoration: InputDecoration.collapsed(hintText: "Email"),
style: TextStyle(fontSize: 18)));
}
Container buildTextFieldPassword() {
return Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.yellow[50], borderRadius: BorderRadius.circular(16)),
child: TextField(
obscureText: true,
decoration: InputDecoration.collapsed(hintText: "Password"),
style: TextStyle(fontSize: 18)));
}
}
ผมตกแต่งคร่าวๆ ได้ประมาณนี้
การใช้งาน FirebaseAuth
พระเอกของเราคือ class ที่ชื่อว่า FirebaseAuth ซึ่งเราต้องสร้าง instance ก่อนใช้งาน
class _MyLoginPageState extends State<MyLoginPage> {
final FirebaseAuth _auth = FirebaseAuth.instance;
..
Sign in ด้วย email
เขียร method สำหรับ signIn() โดยจะลอง hardcode ค่า email , password ไปก่อน
signIn(){
_auth.signInWithEmailAndPassword(
email: "panuwat.developer@gmail.com",
password: "123456"
).then((user) {
print("signed in ${user.email}");
}).catchError((error) {
print(error);
});
}
เพิ่ม InkWell ให้กับปุ่ม โดยมันคือ widget ที่ช่วยให้เมื่อเรากดจะมี animation เล็กๆ พร้อมกับมี onTap ให้ด้วย ซึ่ง onTap ให้กดแล้วไปเรียก signIn()
Widget buildButtonSignIn() {
return InkWell(
child: ... ,
onTap: () {
signIn();
},
);
}
ลองรันแล้วกดที่ปุ่ม sign in จะเห็นว่าค่า email ของเรามาแสดงแล้ว
สร้างหน้า Home
สร้างหน้า home ที่จะแสดงหลังจาก login โดยรับพารามิเตอร์ คือ FirebaseUser (หรือจะไป get เอาจาก FirebaseAuth ในหน้านี้ก็ได้นะ)
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
final FirebaseUser user;
MyHomePage(this.user, {Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My Firebase App", style: TextStyle(color: Colors.white)),
),
body: Container(
child: Center(
child:
Column(mainAxisSize: MainAxisSize.min,children: <Widget>[
Text("Hello", style: TextStyle(fontSize: 26)),
Text(widget.user.email, style: TextStyle(fontSize: 16)),
]))));
}
}
ตรวจสอบสถานะ
ตรวจสอบว่า ถ้าล็อคอินแล้ว ให้เปลี่ยนหน้าไปที่หน้า home แทน
Future checkAuth(BuildContext context) async {
FirebaseUser user = await _auth.currentUser();
if (user != null) {
print("Already singed-in with");
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (context) => MyHomePage(user)));
}
}
เรียก method checkAuth() ที่ initState() เพื่อให้เปิดแอปมาแล้วก็เช็คเลย
class _MyLoginPageState extends State<MyLoginPage> {
final FirebaseAuth _auth = FirebaseAuth.instance;
@override
void initState() {
super.initState();
checkAuth(context);
..
หลังจากนั้น พอเราเข้าแอปมาอีกครั้ง ถ้าเคย sign in แล้วก็จะเปลี่ยนไปหน้า home อัตโนมัติ
ปรับให้กดปุ่มล็อคอินแล้ว ถ้าสำเร็จก็เปลี่ยนไปหน้า home
signIn(){
_auth.signInWithEmailAndPassword(
..
).then((user) {
print("signed in ${user.email}");
checkAuth(context); // add here
}).catchError((error) {
print(error);
});
}
Sign-out
เพิ่มไอคอน action สำหรับ sign out ไปที่ AppBar
class _MyHomePageState extends State<MyHomePage> {
..
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: .. ,
actions: <Widget>[
IconButton(
icon: Icon(Icons.exit_to_app),
color: Colors.white,
onPressed: () {
signOut(context)
})
],
)
เขียน method signOut() วิธีการแค่เรียกจาก .signOut() ผ่าน FirebaseAuth ได้เลย หลังจากนั้นก็ให้ route ไปที่หน้า sign in ของเรา
void signOut(BuildContext context) {
_auth.signOut();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => MyLoginPage()),
ModalRoute.withName('/'));
}
ลองรัน ตอนนี้เราสามารถ sign-in และ sign-out ได้แล้ว
กำหนด TextEditingController หน้า Login
ตอนนี้เรายัง hardcode ค่า email,password เอาไว้ ถึงเวลาเปลี่ยนมาใช้ค่าจาก user กันแล้ว โดยการดึงค่าจาก TextField จะใช้ TextEditingController ดังนั้นเราต้องสร้าง
TextEditingController สำหรับ email และ password
class _MyLoginPageState extends State<MyLoginPage> {
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
..
จากนั้นก็เพิ่ม controller ให้กับ TextField ทั้ง email และ password
Container buildTextFieldEmail() {
..
child: TextField(
controller: emailController,
ตอนเรา sign in ก็ใช้วิธีดึงค่าจาก TextEditingController
Future<FirebaseUser> signIn() async {
final FirebaseUser user = await _auth.signInWithEmailAndPassword(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
..
แสดง error กรณี sign-in ไม่ได้
มาลองทดสอบกรณี error กันบ้าง โดยการแสดง error ผมจะใช้ SnackBar แสดง error ซึ่ง Firebase ก็ส่งข้อความ error มาให้
SnackBar นั้นจะอยู่ใน Scaffold ซึ่งเราต้องเก็บค่า State ของ scaffold ไว้ก่อน โดยสร้าง GlobalKey<ScaffoldState> เอาไว้เรียกใช้ scaffold จากที่อื่นๆ
class _MyLoginPageState extends State<MyLoginPage> {
..
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
..
เอา scaffoldKey ของเราไปกำหนดใน key ของ Scaffold
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
..
ทีนี้ตอนจะแสดง error ก็เรียก scaffoldKey.currentState.showSnackBar(..) ได้เลย
signIn() {
..
.catchError((error) {
print(error.message);
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(error.message, style: TextStyle(color: Colors.white)),
backgroundColor: Colors.red,
));
});
}
ข้อความ error มาแล้ว
เพิ่มปุ่มสมัครสมาชิก
ต่อมาทำหน้าสมัครสมาชิก ก็ต้องมีปุ่มให้เปลี่ยนหน้าไปยังหน้าสมัครสมาชิก ดังนั้นต้องเพิ่มปุ่ม sign up
@override
Widget build(BuildContext context) {
...
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
...
buildOtherLine(),
buildButtonRegister()
],
)),
]))));
}
..
Widget buildOtherLine() {
return Container(
margin: EdgeInsets.only(top: 16),
child: Row(children: <Widget>[
Expanded(child: Divider(color: Colors.green[800])),
Padding(
padding: EdgeInsets.all(6),
child: Text("Don’t have an account?",
style: TextStyle(color: Colors.black87))),
Expanded(child: Divider(color: Colors.green[800])),
]));
}
Container buildButtonRegister() {
return Container(
constraints: BoxConstraints.expand(height: 50),
child: Text("Sign up",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.white)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), color: Colors.orange[200]),
margin: EdgeInsets.only(top: 12),
padding: EdgeInsets.all(12));
}
สร้างหน้าสมัครสมาชิก
สร้างหน้า sign up เพิ่ม มันก็คล้ายๆหน้า login แค่เพิ่มช่องให้ใส่รหัสยืนยันอีกช่องนึง
class MySignUpPage extends StatefulWidget {
MySignUpPage({Key key}) : super(key: key);
@override
_MySignUpPageState createState() => _MySignUpPageState();
}
class _MySignUpPageState extends State<MySignUpPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sign up", style: TextStyle(color: Colors.white)),
),
body: Container(
color: Colors.green[50],
child: Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.yellow[100], Colors.green[100]])),
margin: EdgeInsets.all(32),
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
buildTextFieldEmail(),
buildTextFieldPassword(),
buildTextFieldPasswordConfirm(),
buildButtonSignUp(context)
],
)),
)));
}
Container buildButtonSignUp(BuildContext context) {
return Container(
constraints: BoxConstraints.expand(height: 50),
child: Text("Sign up",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.white)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), color: Colors.green[200]),
margin: EdgeInsets.only(top: 16),
padding: EdgeInsets.all(12));
}
Container buildTextFieldEmail() {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.yellow[50], borderRadius: BorderRadius.circular(16)),
child: TextField(
decoration: InputDecoration.collapsed(hintText: "Email"),
keyboardType: TextInputType.emailAddress,
style: TextStyle(fontSize: 18)));
}
Container buildTextFieldPassword() {
return Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.yellow[50], borderRadius: BorderRadius.circular(16)),
child: TextField(
obscureText: true,
decoration: InputDecoration.collapsed(hintText: "Password"),
style: TextStyle(fontSize: 18)));
}
Container buildTextFieldPasswordConfirm() {
return Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.yellow[50], borderRadius: BorderRadius.circular(16)),
child: TextField(
obscureText: true,
decoration: InputDecoration.collapsed(hintText: "Re-password"),
style: TextStyle(fontSize: 18)));
}
}
แล้วเพิ่ม onTap ให้ปุ่ม sign up ให้ route ไปหน้าใหม่
Widget buildButtonRegister(BuildContext context) {
return InkWell(
child: ...
onTap: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => MySignUpPage()));
});
}
กำหนด TextEditingController หน้า Sign up
ประกาศ instance ของ FirebaseAuth
class _MySignUpPageState extends State<MySignUpPage> {
FirebaseAuth _auth = FirebaseAuth.instance;
..
แล้วก็ TextEditingController ของ ทั้ง 3 TextField
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController confirmController = TextEditingController();
..
กำหนด controller ให้ TextFieldแต่ละตัว
Container buildTextFieldEmail() {
return Container(
..
child: TextField(
controller: emailController,
ส่ง sign up ไปยัง Firebase
หน้า sign up คือสมัครสมาชิกใหม่ เมื่อกดที่ปุ่มก็ต้องไปเพิ่ม user ใน firebase ซึ่งวิธีการง่ายมาก แค่เรียกคำสั่ง createUserWithEmailAndPassword()
signUp() {
String email = emailController.text.trim();
String password = passwordController.text.trim();
String confirmPassword = confirmController.text.trim();
if (password == confirmPassword && password.length >= 6) {
_auth
.createUserWithEmailAndPassword(email: email, password: password)
.then((user) {
print("Sign up user successful.");
}).catchError((error) {
print(error.message);
});
} else {
print("Password and Confirm-password is not match.");
}
}
ใส่ onTap ให้ปุ่ม sign up ให้ไปเรียก method signUp()
Widget buildButtonSignUp(BuildContext context) {
return InkWell(
child: ..
onTap: () => signUp());
}
ลองสมัครสมาชิกกันดู
มาแล้ว user ใหม่ใน firebase
เพิ่มอีกนิดนึงละกัน คือ เมื่อสมัครสมาชิกสำเร็จแล้วก็ให้ route ไปที่หน้า home
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => MyHomePage(user)),
ModalRoute.withName('/'));
แบบนี้
ลืมรหัสผ่าน
อีกหนึ่งปัญหาที่มาคู่กับระบบลงทะเบียน คือ การลืมรหัสผ่าน ที่ user ต้องการจะ reset password ของพวกเขา ถ้าเราทำระบบพวกนี้เองจะต้องยุ่งมากแน่ๆ แต่ Firebase ทำให้หมดแล้ว
เพิ่มปุ่ม ลืมรหัสผ่านที่หน้า login ของเรา
buildButtonForgotPassword(BuildContext context) {
return InkWell(
child: Container(
constraints: BoxConstraints.expand(height: 50),
child: Text("Forgot password",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.white)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.red[300]),
margin: EdgeInsets.only(top: 12),
padding: EdgeInsets.all(12)),
onTap: () => navigateToResetPasswordPage(context));
}
navigateToResetPasswordPage(BuildContext context) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => MyResetPasswordPage()));
}
ได้แบบนี้
สร้างหน้า สำหรับให้ user กรอกอีเมลลงไป
class MyResetPasswordPage extends StatefulWidget {
MyResetPasswordPage({Key key}) : super(key: key);
@override
_MyResetPasswordPageState createState() => _MyResetPasswordPageState();
}
class _MyResetPasswordPageState extends State<MyResetPasswordPage> {
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
FirebaseAuth _auth = FirebaseAuth.instance;
TextEditingController emailController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: Text("Reset password", style: TextStyle(color: Colors.white)),
iconTheme: IconThemeData(
color: Colors.white, //change your color here
),
),
body: Container(
color: Colors.green[50],
child: Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.yellow[100], Colors.green[100]])),
margin: EdgeInsets.all(32),
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
buildTextFieldEmail(),
buildButtonSignUp(context)
],
)),
)));
}
Widget buildButtonSignUp(BuildContext context) {
return InkWell(
child: Container(
constraints: BoxConstraints.expand(height: 50),
child: Text("Reset password",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.white)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.green[200]),
margin: EdgeInsets.only(top: 16),
padding: EdgeInsets.all(12)),
onTap: () => resetPassword());
}
Container buildTextFieldEmail() {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.yellow[50], borderRadius: BorderRadius.circular(16)),
child: TextField(
controller: emailController,
decoration: InputDecoration.collapsed(hintText: "Email"),
keyboardType: TextInputType.emailAddress,
style: TextStyle(fontSize: 18)));
}
resetPassword() {
//
}
}
ขั้นตอนการ reset password คือ เรียก auth.sendPasswordResetEmail() แค่นี้เลยจริงๆ จากนั้น Firebase ก็จะไปเช็คแล้วส่ง email รายละเอียดการ reset password ให้อัตโนมัติ
resetPassword() {
String email = emailController.text.trim();
_auth.sendPasswordResetEmail(email: email);
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text("We send the detail to $email successfully.",
style: TextStyle(color: Colors.white)),
backgroundColor: Colors.green[300],
));
}
ลองแกล้งกดลืมรหัสผ่าน ใส่อีเมลของเราที่เคยล็อคอินลงไป
ไปดูที่อีเมล จะพบว่ามีอีเมลสำหรับ reset password มาแล้วจ้า
โดยเราสามารถแก้ไขหน้าตา template ของอีเมลได้ที่ Firebase auth > Template > Password reset
สรุป
Firebase Auth เป็นบริการที่ช่วยให้เราไม่ต้องเขียน ระบบ authen อย่างพวก login สมัครสมาชิกเอง อาจจะมียุ่งยากก็แค่ตอนแรกคือการติดตั้งในโปรเจค แต่การใช้งานใน Flutter นับว่าง่ายมาก
ตอนต่อไป
ในตอนหน้าของ Firebase Auth ใน Flutter ผมจะพาไปลองเล่น Facebook Login กันครับ ซึ่งนับว่าเป็นระบบ authen ใช้กันบ่อยมากๆแบบนึงในแอพครับ
เครดิต
https://medium.com/flutterpub/flutter-how-to-do-user-login-with-firebase-a6af760b14d5
ยังมีบทความเรื่อง Flutter ที่ผมเขียนไว้ อีกเยอะเลยในบล็อกนี้ ไม่จำเป็นต้องไปเรียนที่ไหนแล้วละ แค่พยายามศึกษาด้วยตนเองเหมือนผมก็เขียนได้
https://benzneststudios.com/blog/category/flutter/