เนื้อหาตอนต่อไปนี้ เราจะมาประยุกต์การใช้งาน plugin ที่ใช้
สำหรับเก็บข้อมูลในรูปแบบ key-value ไว้ในเครื่อง โดยเรา
จะจำลองระบบสมาชิก เก็บข้อมูลเท่าที่จำเป็นของสมาชิกที่กำลัง
ล็อกอินใช้งาน app อยู่ ในเนื้อหาจะเป็นรูปแบบจำลองการสมัคร
สมาชิก การล็อกอิน และการเข้าใช้งานเสมือนเป็นระบบสมาชิกหนึ่งๆ
เพียงแต่ว่า ข้อมูลจะเป็นลักษณะกำหนดแบบตายตัว เพื่อจำลอง
การทำงานเท่านั้น อย่างไรก็ตามเนื้อหานี้ก็เป็นแนวทาง สำหรับเนื้อหา
การประยุกต์ต่อไปด้วย
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/1050
สิ่งที่เราจะจำลองการทำงาน
เราจะจำลองเสมือนว่าใน app ของเรามีบางส่วนของข้อมูล ที่ใช้งานเฉพาะสมาชิกเท่านั้น
ในที่นี้ก็คือส่วนของ profile เมื่้อยังไม่ได้เข้าสู่ระบบหรือยังไม่ได้เป็นสมาชิก ก็มีลิ้งค์ให้ผู้ใช้เลือก
ทำการล็อกอินหรือสมัครสมาชิก และเมื่อผู้ใช้สมัครสมาชิกและทำการล็อกอินเข้าใช้งานแล้ว ก็ให้
แสดงเนื้อหาส่วนของสมาชิกในหน้า profile ผู้ใช้สามารถล็อกเอาท์หรืออกจากระบบถ้าต้องการได้
เป็นระบบสมาชิกคร่าวๆ เพื่อเป็นแนวทางไปประยุตก์ใช้งาน
สิ่งที่ต้องเตรียม
ติดตั้ง shared_preferences
ให้เราทำการติดตั้ง shared_preferences สำหรับใช้งานในไฟล์ pubspec.yaml ดังนี้
dependencies: shared_preferences: ^2.2.3
จากนั้น import ไปใช้งานในหน้าที่ต้องการด้วยคำสั่ง
import 'package:shared_preferences/shared_preferences.dart';
เนื้อหานี้เรายังไม่ใช้ Data model สำหรับข้อมูลสมาชิก เรายังไม่ใช้ Provider สำหรับจัดการ
การทำงาน เราจะใช้แค่เพียง shared_preferences เก็บข้อมูลแบบง่ายที่สุด เพื่อให้เห็นการทำงาน
รวมเท่านั้น เพื่อให้เนื้อหากระซับและเจาะจงเฉพาะส่วนการใช้งาน
ตัวอย่างผลลัพธ์การทำงาน
ลำดับการทำงาน เมื่อมากดแท็บมายังหน้า profile ซึ่งมีการจำกัดการใช้งานเฉพาะสมาชิกที่ล็อกอินเท่านั้น
เมื่อกดไปหน้าล็อกอิน ยังไม่ได้เป็นสมาชิก ก็สามารถกดต่อไปยังหน้าสมัครสมาชิก หลังจากสมัครสมาชิกเสร็จ
ก็กลับมาหน้าล็อกอิน ทำการทดสอบล็อกอินด้วยข้อมูลที่ไม่ถูกต้อง ล็อกอินไม่่ผ่าน ทำการล็อกอินใหม่อีกครั้ง
ด้วยข้อมูลที่ถูกต้อง ล็อกอินผ่านกลับมายังหน้า profile แสดงข้อความต้อนรับ มีปุ่มล็อกเอาท์
การใช้งาน shared preferences
ในการใช้งานระบบสมาชิกอย่างง่าย เราจะต้องมีข้อมูลๆ หนึ่งที่อ้างอิงสถานะการล็อกอินใช้งานหรือสถานะ
สมาชิกที่ล็อกอินแล้ว
ไฟล์ profile.dart
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'login.dart'; import '../models/counter_model.dart'; class Profile extends StatefulWidget { static const routeName = '/profile'; const Profile({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ProfileState(); } } class _ProfileState extends State<Profile> { // กำหนดตัวแปรใช้งาน SharedPreferences final Future<SharedPreferences> _prefs = SharedPreferences.getInstance(); late bool _loginSuccess; // กำหดตัวแปรสถานะการล็อกอิน @override void initState() { super.initState(); loadSettings(); // เรียกใช้งานตั้งค่าเมื่อเริ่มต้นเป็นฟังก์ชั่น ให้รองรับ async } // ตั้งค่าเริ่มต้น void loadSettings() async { // เรียกใช้งาน SharedPreferences ที่เป็น future final SharedPreferences prefs = await _prefs; // กำหนดค่า สถานะการล็อกอิน ถ้ามีข้อมูล ถ้าไม่มีให้กำหนดเป็น false _loginSuccess = prefs.getBool('loginSuccess') ?? false; } // เนื่องจากหน้า profile เราต้องการสถานะการล็อกอินไปกำหนดการแสดง // จึงสร้างเป็นฟังก์ชั่น คืนค่าสถานะเป็น future<bool> Future<bool> getLoginStatus() async { final SharedPreferences prefs = await _prefs; _loginSuccess = prefs.getBool('loginSuccess') ?? false; return _loginSuccess; } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Profile'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ FutureBuilder<bool>( future: getLoginStatus(), // ข้อมูล Future builder: (context, snapshot) { if (snapshot.hasData) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Profile Screen'), Visibility( // ส่วนที่แสดงกรณีล็อกอินแล้ว visible: _loginSuccess, // ใช้สถานะการล็อกอินกำหนดกรแสดง child: Column( children: [ FlutterLogo(size: 100,), Text('Welcome member'), ElevatedButton( onPressed: () async { // เมื่อล็อกเอาท์ // ใช้งานข้อมูล SharedPreferences final SharedPreferences prefs = await _prefs; await prefs.remove("loginSuccess"); // ลบค่าที่บันทึก setState(() { _loginSuccess = false; }); }, child: Text('Logout'), ), ], ), ), Visibility( // ส่วนที่แสดงกรณียังไม่ได้ล็อกอิน visible: !_loginSuccess, // ใช้สถานะตรงข้ามการล็อกอินกำหนดกรแสดง child: ElevatedButton( onPressed: () async { // กำหดให้รอค่า หลังจากเปิดไปหน้า lgoin final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => Login(), settings: RouteSettings( arguments: null ), ), ); // ถ้ามีการปิดหน้มที่เปิด และส่งค่ากลับมาเป็น true if (result == true) { setState(() { _loginSuccess = true; }); } }, child: Text('Go to Login'), ), ), ], ) ); } else if (snapshot.hasError) { // ถ้ามี error return Text('${snapshot.error}'); } return const CircularProgressIndicator(); }, ), Divider(), Text('You have pushed the button this many times:'), Count(), ], ) ), floatingActionButton: FloatingActionButton( key: const Key('increment_Button'), onPressed: () => context.read<Counter>().increment(), child: const Icon(Icons.add), ), ); } } class Count extends StatelessWidget { const Count({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Text( '${context.watch<Counter>().count}', key: const Key('counterState'), style: Theme.of(context).textTheme.headlineMedium); } }
คำอธิบายโค้ดและการทำงาน
// กำหนดตัวแปรใช้งาน SharedPreferences final Future<SharedPreferences> _prefs = SharedPreferences.getInstance(); late bool _loginSuccess; // กำหดตัวแปรสถานะการล็อกอิน
เนื่องจากเราจะใช้งานสถานะการล็อกอินจากข้อมูล SharedPreferences ซึ่งเมื่อเรียกใช้งานจะต้องรอข้อมูลก่อน
ดังนั้นสำหรับค่า _loginSuccess เราเลยยังไม่กำหนดค่าเริ่มต้นในทันที่ที่กำหนดตัวแปร แต่จะกำหนดตามทีหลัง
// ตั้งค่าเริ่มต้น void loadSettings() async { // เรียกใช้งาน SharedPreferences ที่เป็น future final SharedPreferences prefs = await _prefs; // กำหนดค่า สถานะการล็อกอิน ถ้ามีข้อมูล ถ้าไม่มีให้กำหนดเป็น false _loginSuccess = prefs.getBool('loginSuccess') ?? false; }
เมื่อเปิดมาหน้านี้ และยังไม่มีการล็อกอิน ค่า _loginSuccess จะเป็น false จะแสดงเนื้อหาสำหรับคนทั่วไปหรือ
คนที่ยังไม่ได้ล็อกอิน
เราใช้งาน FutureBuilder สำหรับจัดการข้อมูล สถานะการล็อกอินแบบ Future จะคล้ายกับการทำงานของฟ
ฟังก์ชั่น loadSettings() ด้านบน เพียงแต่ว่าฟังก์ชั่นด้านบนจะทำแค่ครั้งแรกครั้งเดียว แต่ฟังก์ชันด้านล่าง เราจะใช้
งานกับการแสดงข้อมูลรองรับหลังจากทำการล็อกอินแล้วด้วย
// เนื่องจากหน้า profile เราต้องการสถานะการล็อกอินไปกำหนดการแสดง // จึงสร้างเป็นฟังก์ชั่น คืนค่าสถานะเป็น future<bool> Future<bool> getLoginStatus() async { final SharedPreferences prefs = await _prefs; _loginSuccess = prefs.getBool('loginSuccess') ?? false; return _loginSuccess; }
ในการแสดงเนื้อหาส่วนของสมาชิก กับที่ไม่เป็นสมาชิก เราจะใช้ Visibility widget จัดการ อย่างไรก็ตาม ก็สามารถ
ใช้ if else แล้วใช้ค่า _loginSuccess สำหรับ return ส่วนเนื้อหาที่ต้องการแทนได้ แล้วแต่จะประยุกต์
ดูตัวอย่างหน้าตาก่อน และหลังล็อกอิน ดังรูป
ก่อนล็อกอิน
หลังล็อกอิน
ส่วนของการล็อกเอาท์ เราจำลองการทำงานอย่างง่าย โดยเมื่อกดล็อกเอาท์ ก็ให้ลบข้อมูลส่วนของ
SharedPreferences ที่เป็นข้อมูล key เท่ากับ loginSuccess ออก
onPressed: () async { // เมื่อล็อกเอาท์ // ใช้งานข้อมูล SharedPreferences final SharedPreferences prefs = await _prefs; await prefs.remove("loginSuccess"); // ลบค่าที่บันทึก setState(() { _loginSuccess = false; }); },
การลบช้อมูล SharedPreferences ทำได้ดังนี้
await prefs.clear() // ลบข้อมูลทั้งหมด await prefs.remove("loginSuccess"); // ลบช้อมูลเฉพาะ key ที่ต้องการ // ดูค่า key ที่บันทึกไว้ทั้งหมด print(prefs.getKeys()); // {email, password, loginSuccess}
ส่วนของการเชื่อมโยงไปหน้าล็อกอิน
onPressed: () async { // กำหดให้รอค่า หลังจากเปิดไปหน้า lgoin final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => Login(), settings: RouteSettings( arguments: null ), ), ); // ถ้ามีการปิดหน้มที่เปิด และส่งค่ากลับมาเป็น true if (result == true) { setState(() { _loginSuccess = true; }); } },
ในการเปิดหน้าล็อกอิน เรากำหนดให้มีการรอการดำเนินการเมื่อปิดหน้าล็อกอิน ผ่านตัวแปร result
ความหมายก็คือ เมื่อเปิดหน้าล็อกอินด้วย คำสั่ง push() ซึ่งเป็นการซ้อนหน้าใหม่ไว้ด้านบนแล้ว ให้รอ
รับข้อมูลจากหน้าล็อกอินผ่านตัวแปร result ซึ่งในตัวอย่าง ถ้ามีการส่งค่า true กลับมา นั่นก็คือมีการ
ล็อกอินผ่าน ก็ให้กำหนดค่า _loginSuccess เป็น true
ไปต่อกันที่ไฟล์ login.dart
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'register.dart'; class Login extends StatefulWidget { static const routeName = '/login'; const Login({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _LoginState(); } } class _LoginState extends State<Login> { late final SharedPreferences prefs; // สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง final _formKey = GlobalKey<FormState>(); // กำหนดตัวแปรรับค่า final _email = TextEditingController(); final _password = TextEditingController(); @override void initState() { super.initState(); loadSettings(); } void loadSettings() async { prefs = await SharedPreferences.getInstance(); } // กำหนดสถานะการแสดงแบบรหัสผ่าน bool _isHidden = true; bool _authenticatingStatus = false; @override void dispose() { _email.dispose(); // ยกเลิกการใช้งานที่เกี่ยวข้องทั้งหมดถ้ามี _password.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Login'), ), body: SingleChildScrollView( child: Form( key: _formKey, // กำหนด key child: Padding( padding: const EdgeInsets.all(15.0), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ SizedBox(height: 20.0,), FlutterLogo( size: 100, ), Text('Login Screen'), TextFormField( decoration: InputDecoration( hintText: 'Email', icon: Icon(Icons.email_outlined), ), controller: _email, // ผูกกับ TextFormField ที่จะใช้ ), SizedBox(height: 5.0,), TextFormField( decoration: InputDecoration( hintText: 'Password', icon: Icon(Icons.vpn_key), suffixIcon: IconButton( onPressed: (){ setState(() { _isHidden = !_isHidden; // เมื่อกดก็เปลี่ยนค่าตรงกันข้าม }); }, icon: Icon( _isHidden // เงื่อนไขการสลับ icon ? Icons.visibility_off : Icons.visibility ), ), ), controller: _password, // ผูกกับ TextFormField ที่จะใช้ obscureText: _isHidden, // ก่อนซ่อนหรือแสดงข้อความในรูปแบบรหัสผ่าน ), SizedBox(height: 10.0,), Visibility( visible: !_authenticatingStatus, child: ElevatedButton( onPressed: () async { // เปลี่ยนสถานะเป็นกำลังล็อกอิน setState(() { _authenticatingStatus = !_authenticatingStatus; }); // จำลองการหน่วงเวลา await Future.delayed(const Duration(seconds: 3)); // อ้างอิงฟอร์มที่กำลังใช้งาน ตรวจสอบความถูกต้องข้อมูลในฟอร์ม if (_formKey.currentState!.validate()) { //หากผ่าน // จำลองดึงข้อมูลจาก SharedPreferences String email = prefs.getString("email") ?? ''; String password = prefs.getString("password") ?? ''; // จำลองเปรียบเทียบค่า เพื่อทำการล็อกอิน if(email == _email.text && password == _password.text){ ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login Successful')), ); // กำหนดค่าให้กับ SharedPreferences จำลองล็อกอินผ่าน await prefs.setBool("loginSuccess", true); await Future.delayed(const Duration(seconds: 2)); Navigator.pop(context, true); // ปิดหน้านี้พร้อมคืนค่า true }else{ // จำลองล็อกอินไม่ผ่าน ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login fail.. try agin!')), ); await Future.delayed(const Duration(seconds: 2)); // เปลี่ยนสถานะเป็นกำลังล็อกอิน setState(() { _authenticatingStatus = !_authenticatingStatus; }); } } }, child: Container( alignment: Alignment.center, width: double.infinity, child: const Text('Login'), ), ), ), Visibility( visible: _authenticatingStatus, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CircularProgressIndicator(), SizedBox(width: 10.0,), Text(" Authenticating ... Please wait") ], ), ), SizedBox(height: 30.0,), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Or '), InkWell( child: Text('Register', style: TextStyle( decoration: TextDecoration.underline, color: Colors.blue )), onTap: () async { // เปิดหน้า สมัครสมาชิก โดย ซ้อนหน้า ล็อกอินเดิม ใช้ push แทน // ไม่เช่นนั้นจะไม่มีค่าส่งกลับจากหน้าล็อกอิน ที่เรารออรับค่าอยู่ // Navigator.pushReplacement( Navigator.push( context, MaterialPageRoute(builder: (context) => Register(), settings: RouteSettings( arguments: null ), ), ); }, ) ], ) ], ) ), ), ), ), ); } }
ในการจำลองหน้าล็อกอินเราตัดส่วนของการการ validate ข้อมูลออกไป เพื่อให้โค้ดกระชับ ในหน้านี้ เราก็ยัง
มีการใช้งานข้อมูล SharedPreferences ผ่านตัวแปร prefs และมีการกำหนดตัวแปรชื่อ _authenticatingStatus
เพื่อใช้เป็นสถานะ กำลังทำการล็อกอิน เราใช้สถานะนี้เป็นตัวกำหนดการแสดง ปุมล็อกอินกับตัวข้อมูลตัว loading
ว่ากำลังทำการล็อกอิน เข้าใจอย่างง่ายก็คือ เมื่อเรากดปุ่มล็อกอิน ตัว loading ก็จะแสดงขึ้นมาแทน ถ้าล็อกอินไม่ผ่าน
สถานะ _authenticatingStatus ก็จะกลับมาเป็น false ให้กดล็อกอินใหม่ แบบนี้เป็นต้น
มาเริ่มกันส่วนของกรณีที่ยังไม่เป็นสมาชิก หรือยังไม่สมัคร เรามีปุ่มสำหรับเปิดไปหน้าสมัครสมาชิกหรือ register
ด้วยรูปแบบคำสั่งดังนี้
onTap: () async { // เปิดหน้า สมัครสมาชิก โดย ซ้อนหน้า ล็อกอินเดิม ใช้ push แทน // ไม่เช่นนั้นจะไม่มีค่าส่งกลับจากหน้าล็อกอิน ที่เรารออรับค่าอยู่ // Navigator.pushReplacement( Navigator.push( context, MaterialPageRoute(builder: (context) => Register(), settings: RouteSettings( arguments: null ), ), ); },
จะเห็นว่าในการเปิดหน้า register เราใช้คำสั่ง pushReplacement() แทน push() ซึ่งก็คือ แทนที่จะเปิดเป้นหน้า
ใหม่ซ้อนบนหน้า login ก็ให้เปลี่ยนจากหน้า login เป็นหน้า register แทน ดังนั้นถ้ากดปุ่ม back ที่เครื่องหรือปุ่ม back
อัตโนมัติมุมบนซ้าย ก็จะเป็นการย้อนไปหน้า profile เลย ไม่ใช่ย้อนไปหน้า login
มาดูต่อส่วนของการจำลองการล็อกอิน
onPressed: () async { // เปลี่ยนสถานะเป็นกำลังล็อกอิน setState(() { _authenticatingStatus = !_authenticatingStatus; }); // จำลองการหน่วงเวลา await Future.delayed(const Duration(seconds: 3)); // อ้างอิงฟอร์มที่กำลังใช้งาน ตรวจสอบความถูกต้องข้อมูลในฟอร์ม if (_formKey.currentState!.validate()) { //หากผ่าน // จำลองดึงข้อมูลจาก SharedPreferences String email = prefs.getString("email") ?? ''; String password = prefs.getString("password") ?? ''; // จำลองเปรียบเทียบค่า เพื่อทำการล็อกอิน if(email == _email.text && password == _password.text){ ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login Successful')), ); // กำหนดค่าให้กับ SharedPreferences จำลองล็อกอินผ่าน await prefs.setBool("loginSuccess", true); await Future.delayed(const Duration(seconds: 2)); Navigator.pop(context, true); // ปิดหน้านี้พร้อมคืนค่า true }else{ // จำลองล็อกอินไม่ผ่าน ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login fail.. try agin!')), ); await Future.delayed(const Duration(seconds: 2)); // เปลี่ยนสถานะเป็นกำลังล็อกอิน setState(() { _authenticatingStatus = !_authenticatingStatus; }); } } },
ในตัวอย่าง เราจำลองการหน่วงเวลา เพื่อให้เสมือนว่ากำลังทำงานโดยใช้งาน
await Future.delayed(const Duration(seconds: 2));
ทันทีที่มีการกดปุ่ม ล็อกอิน เราก็เปลี่ยนสถานะกำลังล็อกอินเป็น true
// เปลี่ยนสถานะเป็นกำลังล็อกอิน setState(() { _authenticatingStatus = !_authenticatingStatus; });
ในตัวอย่างเรากำหนดเป็นค่าตรงข้าม นั่นก็คือเหมือน toggle สลับจากค่าหนึ่งเป็นค่าตรงข้าม
// จำลองดึงข้อมูลจาก SharedPreferences String email = prefs.getString("email") ?? ''; String password = prefs.getString("password") ?? '';
การเรียกดูข้อมูลจาก SharedPreferences
ข้อมูลจาก SharedPreferences เราเรียกผ่านตัวแปร prefs ซึ่งกำหนดไว้ในตอนต้นเรียบร้อยแล้ว
และเนื่องจากค่าใน SharedPreferences จะใช้เก็บข้อมูลที่ไม่ได้มีขนาดใหญ่อะไร เราจึงสามารถ
ใช้งานข้อมูลผ่านคำสั่ง ดังนี้
prefs.getBool(String key) // สำหรับข้อมูล boolean prefs.getDouble(String key) // สำหรับข้อมูล double prefs.getInt(String key) // สำหรับข้อมูล int prefs.getKeys() // ดูข้อมูล key ทั้งหมด คืนค่าเป็น Set<String> prefs.getString(String key) // สำหรับข้อมูล String prefs.getStringList(String key) // สำหรับข้อมูล List<String> // หากเรียกใช้งานไม่ตรงตามชนิดข้อมูลก็จะเกิด error
เมื่อทำการส่งข้อมูลฟอร์ม เราก็จำลองดึงค่าจากข้อมูล SharedPreferences ถ้ามี (สมัครสมาชิกแล้ว)
มาไว้ในตัวแปร จากนั้นจำลองการเปรียบเทียบกับข้อมูลในฟอร์มอย่างง่าย
if(email == _email.text && password == _password.text){
ถ้าล็อกอินผ่าน
// กำหนดค่าให้กับ SharedPreferences จำลองล็อกอินผ่าน await prefs.setBool("loginSuccess", true); await Future.delayed(const Duration(seconds: 2)); Navigator.pop(context, true); // ปิดหน้านี้พร้อมคืนค่า true
บันทึกข้อมูลสถานะการล็อกอินไว้ใน SharedPreferences จากนั้นก็ปิดหน้าล็อกอิน พร้อมส่งค่า true กลับ
ไปเพื่อกำหนดให้หน้า profile โหลดส่วนของ Widget build() อีกครั้ง อย่าลืมว่า คำสั่ง pop() เป็นการปิดหน้า
login ที่อยู่ด้านบนของหน้า profile ซึ่งเปิดไว้อยู่แล้ว การกลับไปหน้า profile ในรูปแบบคำส่ังนี้จึงไม่ใช่การ
โหลดหน้า profile ใหม่ เราจึงต้องส่งค่ากลับไป เพื่อไปกำหนดให้ state เปลี่ยนแปลง และทำคำสัง build()
อีกครั้ง
อย่างไรก็ตามการกำหนดคำสั่งที่จะกลับไปหน้าเดิม ก็ขึ้นกับโครงสร้างการทำงานด้วย ไม่เจาะจงว่าจะต้อง
เป็นคำสั่ง pop() เพราะ route ที่ใช้งานแต่ละโปรเจ็คอาจจะไม่เหมือนกัน
ในกรณีล็อกอินไม่ผ่าน เราก็ทำการเปลี่ยนค่าสถานะกำลังล็อกอินกลายเป็น false อีก เพื่อซ่อนตัว loading
และแสดงปุ่มล็อกอินสำหรับลองใหม่อีกครั้ง
ตัวอย่างหน้ากำลังล็อกอิน
ไปต่อที่ส่วนสุดท้าย หน้าสมัครสมาชิก
ไฟล์ register.dart
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; // import 'login.dart'; class Register extends StatefulWidget { static const routeName = '/register'; const Register({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _RegisterState(); } } class _RegisterState extends State<Register> { late final SharedPreferences prefs; // สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง final _formKey = GlobalKey<FormState>(); // กำหนดตัวแปรรับค่า final _email = TextEditingController(); final _password = TextEditingController(); // กำหนดสถานะการแสดงแบบรหัสผ่าน bool _isHidden = true; bool _registeringStatus = false; @override void initState() { super.initState(); loadSettings(); } void loadSettings() async { prefs = await SharedPreferences.getInstance(); } @override void dispose() { _email.dispose(); // ยกเลิกการใช้งานที่เกี่ยวข้องทั้งหมดถ้ามี _password.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Register'), ), body: SingleChildScrollView( child: Form( key: _formKey, // กำหนด key child: Padding( padding: const EdgeInsets.all(15.0), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ SizedBox(height: 20.0,), FlutterLogo( size: 100, ), Text('Register Screen'), TextFormField( decoration: InputDecoration( hintText: 'Email', icon: Icon(Icons.email_outlined), ), controller: _email, // ผูกกับ TextFormField ที่จะใช้ ), SizedBox(height: 5.0,), TextFormField( decoration: InputDecoration( hintText: 'Password', icon: Icon(Icons.vpn_key), suffixIcon: IconButton( onPressed: (){ setState(() { _isHidden = !_isHidden; // เมื่อกดก็เปลี่ยนค่าตรงกันข้าม }); }, icon: Icon( _isHidden // เงื่อนไขการสลับ icon ? Icons.visibility_off : Icons.visibility ), ), ), controller: _password, // ผูกกับ TextFormField ที่จะใช้ obscureText: _isHidden, // ก่อนซ่อนหรือแสดงข้อความในรูปแบบรหัสผ่าน ), SizedBox(height: 10.0,), Visibility( visible: !_registeringStatus, child: ElevatedButton( onPressed: () async { // เปลี่ยนสถานะกำลังสมัครสมาชิก setState(() { _registeringStatus = !_registeringStatus; }); await Future.delayed(const Duration(seconds: 3)); if (_formKey.currentState!.validate()) { //หากผ่าน // กำหนดค่าให้กับ SharedPreferences จากข้อมูลฟอร์ม await prefs.setString("email", _email.text); await prefs.setString("password", _password.text); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Create new user Successful')), ); await Future.delayed(const Duration(seconds: 2)); // กลับไปยังหน้าล็อกอิน โดยปิดหน้านี้จากที่กำลังซ้อนอยู่ Navigator.pop(context); // ปิดหน้านี้ // เราไม่ใช้การเปลี่ยนหน้าด้วย pushReplacement เพื่อให้หน้า login ส่งค่ากลับได้ /* Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => Login(), settings: RouteSettings( arguments: null ), ), ); */ } }, child: Container( alignment: Alignment.center, width: double.infinity, child: const Text('Register'), ), ), ), Visibility( visible: _registeringStatus, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CircularProgressIndicator(), SizedBox(width: 10.0,), Text(" Registering ... Please wait") ], ), ), SizedBox(height: 30.0,), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Already member? '), InkWell( child: Text('Login', style: TextStyle( decoration: TextDecoration.underline, color: Colors.blue )), onTap: (){ // กลับไปยังหน้าล็อกอิน โดยปิดหน้านี้จากที่กำลังซ้อนอยู่ Navigator.pop(context); // ปิดหน้านี้ // เราไม่ใช้การเปลี่ยนหน้าด้วย pushReplacement เพื่อให้หน้า login ส่งค่ากลับได้ /* Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => Login(), settings: RouteSettings( arguments: null ), ), ); */ }, ) ], ) ], ) ), ), ), ), ); } }
ส่วนของหน้าสมัครสมาชิกก็ใช้คล้ายๆ กับหน้าล็อกอิน ผู้ใช้กรอกข้อมูลแค่ อีเมลกับรหัสผ่าน เพื่อสมัครสมาชิก
และเมื่อกดปุ่ม regrister ก็นำข้อมูลที่กรอก เก็บลงใน SharedPreferences ดูส่วนของการทำงานเมื่อทีการ
กรอกข้อมูลและบันทึก
onPressed: () async { // เปลี่ยนสถานะกำลังสมัครสมาชิก setState(() { _registeringStatus = !_registeringStatus; }); await Future.delayed(const Duration(seconds: 3)); if (_formKey.currentState!.validate()) { //หากผ่าน // กำหนดค่าให้กับ SharedPreferences จากข้อมูลฟอร์ม await prefs.setString("email", _email.text); await prefs.setString("password", _password.text); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Create new user Successful')), ); await Future.delayed(const Duration(seconds: 2)); // กลับไปยังหน้าล็อกอิน โดยปิดหน้านี้จากที่กำลังซ้อนอยู่ Navigator.pop(context); // ปิดหน้านี้ } },
การบันทึกข้อมูลลง SharedPreferences จะใช้รูปแบบคำสั่งต่างๆ ดังนี้
await prefs.setBool(String key, bool value) // คืนค่าเป็น Future<bool> true / false await prefs.setDouble(String key, double value) // คืนค่าเป็น Future<bool> true / false await prefs.setInt(String key, int value) // คืนค่าเป็น Future<bool> true / false await prefs.setString(String key, String value) // คืนค่าเป็น Future<bool> true / false await prefs.setStringList(String key, List<String> value) // คืนค่าเป็น Future<bool> true / false
เนื่องจากการบันทึกข้อมูล หากต้องการผลลัพธ์ว่าบันทึกสำเร็จหรือไม่ เราต้องใช้งานร่วมกับ await เพราะข้อมูล
ที่คืนกลับมาเป็นข้อมูล Future<bool> true / false
ข้อมูลที่จะบันทึกต้องมีชนิดข้อมูลที่สอดคล้องกับคำสั่งที่ใช้งานด้วย ทุกคำสั่ง set ที่เรียกใช้งานในชื่อ key
ซ้ำจะหมายถึงการอัพเดทค่าข้อมูลเดิม จะไม่ใช่การบันทึกค่าใหม่ใน key เดิม เข้าใจอย่างง่ายก็คือ ใน app จะมี
key ที่ไม่ซ้ำกัน
ตัวอย่างการกรอกข้อมูล เพื่อสมัครสมาชิก
ในหน้าสมัครสมาชิก เรามีลิ้งค์สำหรับกลับไปหน้าล็อกอิน กรณีเป็นสมาชิกอยู่แล้ว หรือถ้าเป็นสมาชิกใหม่
และต้องการสมัครสมาชิก ก็ทำการกรอกข้อมูล จากนั้นกดปุ่ม Register เมื่อสมัครสมาชิกเรียบร้อยแล้วก็จะกลับ
ไปหน้าล็อกอินให้อัตโนมัติ เพื่อทำการล็อกอินเข้าใช้งานตามข้อมูลที่กรอกล่าสุด
เนื้อหานี้เราจะเน้นไปที่การใช้งานข้อมูล SharedPreferences และภาพรวมระบบสมาชิก เป็นแนวทางสำหรับ
เนื้อหาต่อๆ ไป ซึ่งเราจะมีการใช้งาน provider รวมถึงใช้งานร่วมกับข้อมูลจาก server จำลอง จะเป็นยังไง
รอติดตามในบทความต่อไป