สรัางระบบล็อก App ด้วย PIN number ใน Flutter อย่างง่าย

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
pin flutter sharedpreferences

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ pin flutter sharedpreferences

ดูแล้ว 292 ครั้ง


เนื้อหาต่อไปนี้ เราจะมาใช้เทคนิคอย่างง่าย ในการทำระบบ
ล็อกแอป หรือระบบความปลอดภัยส่วนตัวแบบ Local Authen
โดยใช้งานในรูปแบบ PIN number ที่ให้เราสามารถกำหนดให้แอป
สามารถเข้าใช้งานได้เมื่อใส่รหัส PIN ที่ถูกต้อง ซึ่งเราจะต้องทำการเปิด
การใช้งานการตั้งค่า PIN ในครั้งแรก หลังจากนั้่น หากมีการเปิดเข้าใช้งาน
แอปอีกครั้ง ก็จะต้องกรอกรหัส PIN ให้ถูกต้อง ถึงจะเข้าใช้งานได้ อาจจะเหมาะ
กับกรณีแอปของเรามีการเก็บข้อมูลที่บันทึกไว้ และต้องการให้เพียงเราเท่านั้น
ที่สามารถเข้าดูและใช้งานได้ แบบนี้เป็นต้น
 
เนื้อหานี้ใช้ไฟล์ตัวอย่างจากบทความ ไฟล์เนื้อหาเพิ่มเติมที่ 2
การใช้งาน BottomNavigationBar ใน Flutter เบื้องต้น http://niik.in/961
 

แนวทางและการเรียนรู้จากบทความนี้

    - การสร้างการกำหนดการเปิดใช้งาน PIN
    - การสร้างหน้ากรอก PIN และการตรวจสอบเงื่อนไขการเข้าใช้งาน
    - การใช้งาน shared preferences เพื่อเก็บค่า PIN และเรียกใช้งาน
 

ค่าตัวแปรที่เกี่ยวข้องและการอธิบาย

    - pincodeStatus ค่า bool เก็บสถานะเปิดใช้งาน pin หรือไม่
    - pincodeValue ค่า String เก็บข้อมูลเลข PIN ที่บันทึกไว้ตรวจสอบ
    - authorized ค่า bool เก็บสถานะอนุญาตเข้าใช้งาน ถ้าว่ากรอกรหัส PIN ถูกต้อง
 

ลำดับขั้นตอนการทำงาน

    - เมื่อเข้าใช้งานแอป ตัวแอปจะอ่านค่า pincodeStatus เพื่อดูสถานะว่ามีการล็อกแอปไหม
    - ถ้ามีการล็อกแอปด้วย pin ก็จะเรียกหน้ากรอก pin ขึ้นมาแสดง 
    - เมื่อผู้ใช้กรอก pin ถูกต้องก็เข้าใช้งานแอปได้
    - ถ้าอ่านค่า pincodeStatus ยังไม่มีการล็อกแอป ก็เข้าใช้งานปกติ แต่มีหน้า settings
    ที่สามารถเข้าไปเปิดการตั้งค่าการล็อกแอปได้ เมื่อเปิดการตั้งค่า ก็จะขึ้นหนัา pin มาให้กำหนด
    ค่า เพื่อตั้งค่า pin ไว้ใช้งาน และเก็บค่าไว้ใน pincodeValue ไว้ตรวจสอบ
    จากนั้นก็กำหนดค่า authorized เป็น true เพื่อให้สามารถเข้าใช้งานหลังจากตั้งค่าแล้ว
    และเมื่อปิดและเปิดแอปมาอีกครั้งก็จะขึ้นหน้า pin ให้กรอก แบบนี้เป็นตัน
 
เนื้อหานี้จำเป็นต้องรู้จักการใช้งาน shared preferences สำหรับเก็บข้อมูล ดูได้ที่
ประยุกต์เก็บข้อมูลด้วย shared preferences ใน Flutter http://niik.in/1059
 

สร้างหน้าเปิดปิดการตั้งค่า PIN number

    หลังจากทำความเข้าใจแนวทางไปแล้ว มาลงรายละเอียดที่โค้ดกัน ไฟล์หลักของเราจะมี 2-3 ไฟล์ คือ
ไฟล์ สำหรับใช้ตั้งค่าการเปิดปิด PIN ใช้เป็นไฟล์ settings.dart  และไฟล์ที่จัดการเกี่ยวกับ PIN 
ทั้งหมด ไม่ว่าจะเป็นการตั้งค่าเมื่อเปิดใช้ การยกเลิกการตั้ง และการตรวจสอบ PIN ก่อนใช้งาน ทั้งสาม
ส่วนนี้เราจะไว้ในไฟล์เดียวคือไฟล์ pincode.dart
    *คำอธิบายแสดงในโค้ด
 

ไฟล์ settings.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'pincode.dart';

class Settings extends StatefulWidget {
  static const routeName = '/settings';

  const Settings({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _SettingsState();
  }
}

class _SettingsState extends State<Settings> {
  late final SharedPreferences prefs;
  bool _pincodestatus = false; // สถานะเปิดใช้ pin
  bool _authorized = false; // สถานะเข้าใช้งาน
  String _pincodevalue = ''; // ค่า pin ในที่นี้ใช้ 4 ตัวเลข
  bool _isLoadingPrefs = true; // สถานะการโหลดค่าจาก SharedPreferences

  @override
  void initState() {
    super.initState();
    // โหลดค่าจาก SharedPreferences
    _loadValueFromSharedPreferences();
  }

  // Method to load value from SharedPreferences
  Future<void> _loadValueFromSharedPreferences() async {
    prefs = await SharedPreferences.getInstance();

    setState(() {
      _pincodestatus = prefs.getBool('pincodestatus') ?? _pincodestatus;
      _authorized = prefs.getBool('authorized') ?? _authorized;
      _pincodevalue = prefs.getString('pincodevalue') ?? _pincodevalue;
      _isLoadingPrefs = false; //  โหลดค่าเรียบร้อยแล้ว
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoadingPrefs) {
      // คืนค่ากรณี กำลังโหลดค่า SharedPreferences ยังไม่เสร็จ
      return const Center(child: SizedBox.shrink());
    }
    print("debug: _pincodestatus ${_pincodestatus}");
    print("debug: _authorized ${_authorized}");
    print("debug: _pincodevalue ${_pincodevalue}");
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings'),
      ),
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          // Text('Settings Screen'),
          ListTile(
            title: Text('Lock app with PIN number'),
            trailing: Checkbox(
              value: _pincodestatus,
              onChanged: (bool? value) async {
                // เมื่อมีการเปลี่ยนแปลงการตั้งค่า
                // เปิดหน้ากำหนด pin และรอดำเนินการจากหน้านั้น
                final result = await Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => Pincode()),
                );
                // จัดการเงื่อนไขรับค่าที่ส่งกลับมา
                print("debug: ${result}");
                if (result == 'cancel') { // ถ้าเป็นการยกเลิก
                  setState(() {
                    _authorized = false;
                    _pincodestatus = false;
                    _pincodevalue = '';
                  });
                } else {
                  // ถ้ามีการตั้งค่า result ที่ส่งกลับมาเป็นค่า code
                  if (result != null) { 
                    setState(() {
                      _authorized = true;
                      _pincodestatus = true;
                      _pincodevalue = result;
                    });
                  }
                }
              },
            ),
          ),
        ],
      )),
    );
  }
}
 

ไฟล์ pincode.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'launcher.dart';

class Pincode extends StatefulWidget {
  static const routeName = '/pincode';

  const Pincode({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PincodeState();
  }
}

class _PincodeState extends State<Pincode> {
  late final SharedPreferences prefs;
  bool _pincodestatus = false; // สถานะเปิดใช้ pin
  bool _authorized = false; // สถานะเข้าใช้งาน
  String _pincodevalue = ''; // ค่า pin ในที่นี้ใช้ 4 ตัวเลข
  bool _isLoadingPrefs = true; // สถานะการโหลดค่าจาก SharedPreferences  
  // วนลูปสร้าง controller และ focusNodes ตามจำนวนที่ต้องการ
  // ในที่นี้ใช้แค่ 4 ตัว สามารถปรับเป็นจำนวนที่ต้องการได้
  final _codeController = List.generate(4, (index) => TextEditingController());
  final _focusNodes = List.generate(4, (index) => FocusNode());


  @override
  void initState() {
    super.initState();
    // Autofocus ไฟที่ช่องตัวเลขแรก
    _focusNodes[0].requestFocus(); 
        // โหลดค่าจาก SharedPreferences
    _loadValueFromSharedPreferences();
  }

  // โหลดข้อมูลจาก SharedPreferences 
  Future<void> _loadValueFromSharedPreferences() async {
    prefs = await SharedPreferences.getInstance();

    setState(() {
      _pincodestatus = prefs.getBool('pincodestatus') ?? _pincodestatus;
      _authorized = prefs.getBool('authorized') ?? _authorized;
      _pincodevalue = prefs.getString('pincodevalue') ?? _pincodevalue;
      _isLoadingPrefs = false; // Set to false once the data is loaded
    });
  }

  // ฟังก์์ชั่นสำหรับยกเลิกการกำหนด pin 
  void _verifyCodeToCancel() async {
    // รวมตัวเลขแต่ละช่องเป็นรหัส pin 4 ตัว
    String code = _codeController.map((controller) => controller.text).join();
    // ถ้าจะยกเลิก ต้องกรอกรหัส pin เดิมให้ถูกต้อง
    if (code == _pincodevalue) {
      await prefs.setString("pincodevalue", '');
      await prefs.setBool("pincodestatus", false);
      await prefs.setBool("authorized", false);
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Success"),
          content: Text("Pin verified successfully!"),
          actions: [
            TextButton(
              onPressed: () {
                // มีการซ้อนทับ 2 ชั้น ต้องเอาหน้าเพจชิ้นบนออก สองครั้ง
                // คือ showDialog และหน้า pin
                Navigator.of(context).pop();
                // เมื่อเอาหน้า pin ออกหลังตรวจสอบสำเร็จ ส่งค่า cancel กลับไปหน้าหลัก
                Navigator.pop(context, 'cancel');
              },
              child: Text("OK"),
            )
          ],
        ),
      );
    } else {
      // ถ้ากรอกผิด ขึ้นให้ลองใหม่ จนกว่าจะกรอกถูก หรือยกเลิกไป
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Error"),
          content: Text("Incorrect pin. Try again."),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text("OK"),
            )
          ],
        ),
      );
    }
  }

  // ฟังก์ชั่นตรวจสอบค่า pin ที่กรอกเพือเข้าใช้งาน
  void _verifyCode() async {
    String code = _codeController.map((controller) => controller.text).join();
    if (code == _pincodevalue) {
      // ถ้ากรอกข้อมูล pin ถูกต้อง
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Success"),
          content: Text("Pin verified successfully!"),
          actions: [
            TextButton(
              onPressed: () async {
                // เก็บสถานะการเข้าใช้งาน เป็น true แล้ว แทนหน้า pin ปัจจุบ้น ด้วย
                /// หน้าหลักของแอป ในที่นี้คือหน้า Launcher
                // เนื่องจากมีการซ้อนทับ 2 ชั้น ต้องเอาหน้าเพจชิ้นบนออก สองครั้ง
                // คือ showDialog และหน้า pin
                await prefs.setBool("authorized", true);
                Navigator.of(context).pop();                
                Navigator.pushReplacement(
                  context,
                  MaterialPageRoute(builder: (context) => const Launcher()),
                );
              },
              child: Text("OK"),
            )
          ],
        ),
      );
    } else {
      // ถ้ายังกรอกไม่ถูกต้อง ก็ไม่สามารถเข้าใช้งานได้ ต้องกรอกจนกว่าจะถูกต้อง
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Error"),
          content: Text("Incorrect pin. Try again."),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text("OK"),
            )
          ],
        ),
      );
    }
  }

  // ฟังก์ชั่นตรวจสอบสำหรับกยเลิกการเปิดใช้งาน pin
  void _checkAutoVerify() {
    String code = _codeController.map((controller) => controller.text).join();
    if (code.length == 4 &&
        !_codeController.any((controller) => controller.text.isEmpty)) {
      _verifyCodeToCancel(); 
    }
  }

  // ฟังก์ชั่นตั้งค่า pin เมื่อเปิดใช้งานการใช้งาน pin
  void _setAutoVerify() async {
    String code = _codeController.map((controller) => controller.text).join();
    if (code.length == 4 &&
        !_codeController.any((controller) => controller.text.isEmpty)) {
      await prefs.setString("pincodevalue", code);
      await prefs.setBool("pincodestatus", true);
      // เมื่อตั้งค่าแล้ว ส่งค่า code กลับออกไป พร้อมปิดหน้า dialog
      Navigator.pop(context, code);
    }
  }

  @override
  void dispose() {
    // ล้างค่าตัวแปรที่ไมได้ใช้งาน
    _codeController.forEach((controller) => controller.dispose());
    _focusNodes.forEach((focusNode) => focusNode.dispose());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoadingPrefs) {
      // คืนค่ากรณี กำลังโหลดค่า SharedPreferences ยังไม่เสร็จ
      return const Center(child: SizedBox.shrink());
    }
    print("debug: _pincodestatus ${_pincodestatus}");
    print("debug: _authorized ${_authorized}");
    print("debug: _pincodevalue ${_pincodevalue}");

    // รับค่า arguments ที่ส่งมาใช้งาน จากการเข้าใช้งานแอปครั้งแรก และมีการเปิดใช้งาน pin
    final args = ModalRoute.of(context)!.settings.arguments as String?;
    print("debug: args ${args}");
    return Scaffold(
      appBar: AppBar(
        title: Text('PinCode'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Enter your PIN number',
              style: TextStyle(fontSize: 20),
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: List.generate(4, (index) {
                return Container(
                  width: 50,
                  child: TextField(
                    controller: _codeController[index],
                    focusNode: _focusNodes[index],
                    keyboardType: TextInputType.number,
                    textAlign: TextAlign.center,
                    maxLength: 1,
                    autofocus: index == 0,
                    //        obscureText: true, // ซ่อนข้อมูลแสดงแบบรหัสผ่าน
                    decoration: InputDecoration(
                      counterText: '',
                      filled: true, // Enables background color
                      fillColor:
                          Colors.grey.shade200, // Set the background color
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8.0),
                        borderSide: BorderSide(
                          color: Colors.black, // Border color
                          width: 2.0, // Border thickness
                        ),
                      ),
                      focusedBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8.0),
                        borderSide: BorderSide(
                          color: Colors.blue, // Border color when focused
                          width: 2.0, // Border thickness
                        ),
                      ),
                      enabledBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8.0),
                        borderSide: BorderSide(
                          color: Colors.grey
                              .withAlpha(50), // Border color when not focused
                          width: 2.0,
                        ),
                      ),
                    ),
                    onChanged: (value) {
                      // ส่วนการจัดการการโฟกัส ช่องกรอกข้อมูลอัตโนมัติ
                      if (value.length == 1 && index < 3) {
                        FocusScope.of(context)
                            .requestFocus(_focusNodes[index + 1]);
                      } else if (value.isEmpty && index > 0) {
                        FocusScope.of(context)
                            .requestFocus(_focusNodes[index - 1]);
                      }
                      // ตรวจสอบข้อมูล pin ที่กรอกเข้ามา
                      String code = _codeController
                          .map((controller) => controller.text)
                          .join();
                      // ถ้าเป็น pin ที่มี 4 ตัว และไม่ใช่ค่าว่าง    
                      if (code.length == 4 &&
                          !_codeController
                              .any((controller) => controller.text.isEmpty)) {
                        if (args != null) { // ถ้าส่งมาจากหน้าแรกหรือ launcher
                          _verifyCode(); // ตรวจสอบการเข้าใช้งานด้วย pin
                        } else {
                          // ถ้าเปิดใช้งานอยู่ ต้องการปิดใช้งาน
                          if (_pincodestatus) { 
                            _checkAutoVerify();
                          } else {
                            // ต้องการตั้งค่า pin เพื่อใช้งาน
                            _setAutoVerify();
                          }
                        }
                      }
                    },
                  ),
                );
              }),
            ),
            SizedBox(height: 16),
            TextButton(
              onPressed: () {
                // Handle resend code logic
              },
              child: Text(
                args != null // ถ้าสงมาจากหน้าแรก
                    ? 'ใส่รหัส PIN เพื่อเข้าใช้งาน'
                    : _pincodestatus // ถ้าเป็นการตั้งค่า
                        ? 'ใส่รหัส PIN เพื่อยกเลิก'
                        : 'ใส่รหัส PIN เพื่อยเปิดใช้งานล็อกแอป',
                style: TextStyle(color: Colors.black),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
 

ตัวอย่างผลลัพธ์การทำงาน

 

กรณีเปิดการใช้งาน


 

กรณียกเลิกการใช้งาน



 
ครั้งแรก เมื่อเราเข้าใช้งาน และมายังหน้า settings ก็จะมีให้เลือกว่าจะเปิดใช้งาน pin หรือไม่ ถ้าเรา
เลือกเปิดใช้งาน ก็จะแสดงหน้า pincode ให้เรากำหนด pin เพื่อใช้งาน เมื่อกดตั้งค่า pin เรียบร้อยแล้ว
สถานะการเปิดใช้งาน pin ก็จะเป็น true 
และเมื่อสมมติเราต้องการยกเลิกการตั้งค่า pin ก็ให้กดที่การตั้งค่าอีกครั้ง หน้า pincode ก็จะแสดงให้
เรากรอก pin เพื่อยืนยันการยกเลิก เป็นอันเสร็จเรียบร้อยตามกระบวนการ

 
 

การเรียกใช้งานตรวจสอบ PIN เมื่อเข้าใช้งาน

    โค้ดแอปตัวอย่างของเราจะมีไฟล์หน้าเริ่มต้นคือ launcher.dart ดังนั้นเราจะแก้ไขในไฟล์ส่วนนี้
แนวทางการทำงาน เมื่อเราได้ทำการเปิดใช้งาน pin เพื่อล็อกแอปแล้ว ถ้าเข้ามาครั้งแรกที่หน้า 
launcher ก็จะตรวจสอบค่าที่บันทึกใน SharedPreferences ว่ามีการเปิดใช้งาน pin และยังไม่ได้
ปลอดล็อกเพื่อเข้าใช้งานหรือไม่ ถ้ายังเปิดใช้งาน pin แต่ยังไม่ปลดล็อก ก็จะทำการดึงหน้า pincode
มาแสดงแทนหน้า launcher โดยในการดึงมาแสดง เราก็มีการส่งค่าข้อมูลไปยังไฟล์ pincode.dart
เพื่อใช้เป็นเงื่อนไขการเช้าใช้งาน
    หากผู้ใช้กรอก pin ไม่ถูกต้องก็จะไม่สามารถเข้าใช้งานแอปได้จะค้างที่หน้า pin แต่ถ้าผู้ใช้กรอก
ข้อมูล pin ถูกต้องตามที่ได้กำหนดไว้ ก็จะทำการบันทึกสถานะต่างๆ ไว้ใน SharedPreferences
ให้สามารถเรียกใช้ภายหลังอีกได้
    อย่างไรก็ดีในการตั้งค่าเมื่อใช้งาน pin จะมีจุดหนึ่งที่เราต้องสนใจคือ เมื่อเราเข้าใช้งานสำเร็จผ่าน 
pin แล้่ว ค่าการใช้งานจะถูกบันทึกเป็น true ดังนั้น ที่เราต้องการคือ ต้องมีการกรอก pin ทุกครั้ง
ที่เปิดแอปขึ้นมาใหม่หรือมีการหยุดแล้วกลับมาใหม่ ซึ่งในกระบวนการนี้ เราจะต้องกำหนดการทำงาน
ให้กับ AppLifecycleState เพิ่มเข้ามา และมีการกำหนดให้ลบหรือล้างค่าการปลดล็อก เมื่ออยู่ใน
สถานะดังกล่าว  คำอธิบายแสดงในโค้ด
 

ไฟล์ launcher.dart

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'home.dart';
import 'contact.dart';
import 'profile.dart';
import 'about.dart';
import 'settings.dart';
import 'pincode.dart';

import '../components/sidemenu.dart';

class Launcher extends StatefulWidget {
  static const routeName = '/';

  const Launcher({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _LauncherState();
  }
}

class _LauncherState extends State<Launcher> with WidgetsBindingObserver {
  late final SharedPreferences prefs;
  bool _pincodestatus = false; // สถานะเปิดใช้ pin
  bool _authorized = false; // สถานะเข้าใช้งาน
  String _pincodevalue = ''; // ค่า pin ในที่นี้ใช้ 4 ตัวเลข
  bool _isLoadingPrefs = true; // สถานะการโหลดค่าจาก SharedPreferences

  int _selectedIndex = 0;
  final List<Widget> _pageWidget = <Widget>[
    const Home(),
    const About(),
    const Profile(),
    const Contact(),
    const Settings(),
  ];
  final List<BottomNavigationBarItem> _menuBar = <BottomNavigationBarItem>[
    const BottomNavigationBarItem(
      icon: Icon(FontAwesomeIcons.house),
      label: 'Home',
    ),
    const BottomNavigationBarItem(
      icon: Icon(FontAwesomeIcons.circleInfo),
      label: 'About',
    ),
    const BottomNavigationBarItem(
      icon: Icon(FontAwesomeIcons.userLarge),
      label: 'Profile',
    ),
    const BottomNavigationBarItem(
      icon: Icon(FontAwesomeIcons.addressCard),
      label: 'Contact',
    ),
    const BottomNavigationBarItem(
      icon: Icon(FontAwesomeIcons.gear),
      label: 'Settings',
    ),
  ];

  @override
  void initState() {
    super.initState();
    // โหลดค่าจาก SharedPreferences
    _loadValueFromSharedPreferences();
    // กำหนดการตรวจจับ สถานะ state ของ app
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    // ล้างค่าตรวจจับ สถานะ state ของ app
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  // ส่วนของการทำงานเมื่อ state ของ app มีการเปลี่ยนแปลง
  // บาง ค่าของ LifecycleState อาจจะไม่ทำงานได้
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    print("debug: AppLifecycleState: $state");
    if (state == AppLifecycleState.paused) {
      // ล้างค่าการปลดล็อก ถ้ามีการหยุดชั่วคราวของแอป
      _authorized = false;
      await prefs.setBool('authorized', false);
      print("debug: App is in background.");
    } else if (state == AppLifecycleState.resumed) {
      print("debug: App is in foreground.");
    } else if (state == AppLifecycleState.detached) {
      // ล้างค่าการปลดล็อก ถ้ามีการปิดแอป ส่วนนี้อาจจะไทม่ทำงานได้
      // และบางครั้ง เวลาเริ่มแอปใหม่ อาจจะไม่มีการใส่ pin ได้
      _authorized = false;
      await prefs.setBool('authorized', false);      
      print("debug: App is about to be terminated.");
    }
  }  

  // โหลดข้อมูลจาก SharedPreferences 
  Future<void> _loadValueFromSharedPreferences() async {
    prefs = await SharedPreferences.getInstance();

    setState(() {
      _pincodestatus = prefs.getBool('pincodestatus') ?? _pincodestatus;
      _authorized = prefs.getBool('authorized') ?? _authorized;
      _pincodevalue = prefs.getString('pincodevalue') ?? _pincodevalue;
      _isLoadingPrefs = false;  //  โหลดค่าเรียบร้อยแล้ว
      // เงื่่อนไขถ้ามีการเปิดใช้งานการล็อกแอป และยังไม่ปลดล็อก
      if (_pincodestatus && !_authorized) {
        _authen(); // เรียกใช้งานการตรวจสอบ pin
      }   
    });
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  // การตรวจสอบ pin โดยแทนที่หน้าหลักด้วยหน้า pincode
  void _authen() async {
      String? args = 'pin'; // ส่งค่านี้ไป เพื่อใช้แยกว่าเป็นการ ตรวจสอบเข้าใช้งาน
      await Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => Pincode(),
            settings: RouteSettings(arguments: args // ส่งค่าไปใน  arguments
          ),
        ),
      );
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoadingPrefs) {
      // คืนค่ากรณี กำลังโหลดค่า SharedPreferences ยังไม่เสร็จ      
      return const Center(child: SizedBox.shrink());
    } 
    print("debug: _pincodestatus ${_pincodestatus}");
    print("debug: _authorized ${_authorized}");
    print("debug: _pincodevalue ${_pincodevalue}");
    return Scaffold(
      body: _pageWidget.elementAt(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: _menuBar,
        currentIndex: _selectedIndex,
        selectedItemColor: Theme.of(context).primaryColor,
        unselectedItemColor: Colors.grey,
        onTap: _onItemTapped,
      ),
      drawer: SideMenu(),
    );
  }
}
 

ผลลัพธ์การทำงาน



 
 
เมื่อเปิดใช้งานการล็อกแอป หากเข้ามาใช้งานครั้งแรก ก็จะเปิดหน้า pin ให้เรากรอกรหัส pin ตามที่
ได้ตั้งไว้ให้ถูกต้อง ถ้ากรอกผิด ก็จะขึ้นแจ้งเตือนให้กรอก จนกว่าจะกรอกถูก ไม่เช่นนั้นก็จะเข้าใช้งานไม่ได้
เมื่อกรอกถูกต้อง ระบบก็จะโหลดหน้าหลัก launcher มาแสดง
 
ข้อสังเกตในโค้ดคือ จะมีการใช้งาน WidgetsBindingObserver Mixin ในส่วนของ State class
 
class _LauncherState extends State<Launcher> with WidgetsBindingObserver {
 
เพื่อใช้ในการตรวจสอบและจัดการเกี่ยวกับ  AppLifecycleState  ตามที่ได้อธิบายการทำงานในโค้ด
 
เนื้อหานี้เป็นนวทางการจัดการเกี่ยวกับ local authen ที่เรากำหนดและจัดรุปแบบเอง สามารถ
นำไปปรับประยุกต์เพิ่มเติมได้ และเนื้อหานี้ ก็จะต่อยอดไปถึงตอนหน้า เกี่ยวกับ local authen เกี่ยวกับ
การปลดล็อกด้วยระบบของเครื่องเช่น pattern หรือ ใบหน้า หรือ สแกนลายนิ้วมือ รอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 21-09-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_044_20092024_source.rar


   เพิ่มเติมเนื้อหา ครั้งที่ 2 วันที่ 23-09-2024


ปรับแต่งรูปแบบหน้า PIN แบบใหม่

จากเดิมตัวอย่างเราใช้ pin code โดยเป็นการกรอกข้อมูลตัวเลขลงใน textfield เราสามารถปรับหน้า pincode ใหม่ได้ดังนี้



สามารถนำไปแทนไฟล์เดิม ได้เลย และไม่ต้องแก้ไขส่วนอื่น

ไฟล์ pincode.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'launcher.dart';

class Pincode extends StatefulWidget {
  static const routeName = '/pincode';

  const Pincode({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PincodeState();
  }
}

class _PincodeState extends State<Pincode> {
  late final SharedPreferences prefs;
  bool _pincodestatus = false; // สถานะเปิดใช้ pin
  bool _authorized = false; // สถานะเข้าใช้งาน
  String _pincodevalue = ''; // ค่า pin ในที่นี้ใช้ 4 ตัวเลข
  bool _isLoadingPrefs = true; // สถานะการโหลดค่าจาก SharedPreferences

  // ส่วนสำหรับกำหนด pin
  String pin = '';
  final int pinLength = 6; // ความยาว
  bool _forAuthen = false; // มาจากหน้าแรกหรือไม่

  @override
  void initState() {
    super.initState();
    // โหลดค่าจาก SharedPreferences
    _loadValueFromSharedPreferences();
  }

  // โหลดข้อมูลจาก SharedPreferences
  Future<void> _loadValueFromSharedPreferences() async {
    prefs = await SharedPreferences.getInstance();

    setState(() {
      _pincodestatus = prefs.getBool('pincodestatus') ?? _pincodestatus;
      _authorized = prefs.getBool('authorized') ?? _authorized;
      _pincodevalue = prefs.getString('pincodevalue') ?? _pincodevalue;
      _isLoadingPrefs = false; // Set to false once the data is loaded
    });
  }

  // ฟังก์์ชั่นสำหรับยกเลิกการกำหนด pin
  void _verifyCodeToCancel() async {
    // ถ้าจะยกเลิก ต้องกรอกรหัส pin เดิมให้ถูกต้อง
    if (pin == _pincodevalue) {
      await prefs.setString("pincodevalue", '');
      await prefs.setBool("pincodestatus", false);
      await prefs.setBool("authorized", false);
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Success"),
          content: Text("Pin verified successfully!"),
          actions: [
            TextButton(
              onPressed: () {
                // มีการซ้อนทับ 2 ชั้น ต้องเอาหน้าเพจชิ้นบนออก สองครั้ง
                // คือ showDialog และหน้า pin
                Navigator.of(context).pop();
                // เมื่อเอาหน้า pin ออกหลังตรวจสอบสำเร็จ ส่งค่า cancel กลับไปหน้าหลัก
                Navigator.pop(context, 'cancel');
              },
              child: Text("OK"),
            )
          ],
        ),
      );
    } else {
      // ถ้ากรอกผิด ขึ้นให้ลองใหม่ จนกว่าจะกรอกถูก หรือยกเลิกไป
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Error"),
          content: Text("Incorrect pin. Try again."),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text("OK"),
            )
          ],
        ),
      );
    }
  }

  // ฟังก์ชั่นตรวจสอบค่า pin ที่กรอกเพือเข้าใช้งาน
  void _verifyCode() async {
    if (pin == _pincodevalue) {
      // ถ้ากรอกข้อมูล pin ถูกต้อง
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Success"),
          content: Text("Pin verified successfully!"),
          actions: [
            TextButton(
              onPressed: () async {
                // เก็บสถานะการเข้าใช้งาน เป็น true แล้ว แทนหน้า pin ปัจจุบ้น ด้วย
                /// หน้าหลักของแอป ในที่นี้คือหน้า Launcher
                // เนื่องจากมีการซ้อนทับ 2 ชั้น ต้องเอาหน้าเพจชิ้นบนออก สองครั้ง
                // คือ showDialog และหน้า pin
                await prefs.setBool("authorized", true);
                Navigator.of(context).pop();
                Navigator.pushReplacement(
                  context,
                  MaterialPageRoute(builder: (context) => const Launcher()),
                );
              },
              child: Text("OK"),
            )
          ],
        ),
      );
    } else {
      // ถ้ายังกรอกไม่ถูกต้อง ก็ไม่สามารถเข้าใช้งานได้ ต้องกรอกจนกว่าจะถูกต้อง
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text("Error"),
          content: Text("Incorrect pin. Try again."),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text("OK"),
            )
          ],
        ),
      );
    }
  }

  // ฟังก์ชั่นตรวจสอบสำหรับกยเลิกการเปิดใช้งาน pin
  void _checkAutoVerify() {
    _verifyCodeToCancel();
  }

  // ฟังก์ชั่นตั้งค่า pin เมื่อเปิดใช้งานการใช้งาน pin
  void _setAutoVerify() async {
    await prefs.setString("pincodevalue", pin);
    await prefs.setBool("pincodestatus", true);
    // เมื่อตั้งค่าแล้ว ส่งค่า code กลับออกไป พร้อมปิดหน้า dialog
    Navigator.pop(context, pin);
  }

  @override
  void dispose() {
    // ล้างค่าตัวแปรที่ไมได้ใช้งาน
    super.dispose();
  }

  // ฟังก์ชั่นทำงานเมื่อกดเลข pin
  void _onKeyPressed(String value, [bool action = false]) {
    setState(() {
      if (pin.length < pinLength) {
        pin += value;
      }
      if (pin.length == pinLength) {
        // ถ้าไม่ได้มาจากหน้าแรกจะเป็นการตั้งต่าและยกเลิก
        if (action == false) {
          // ถ้าเปิดใช้งานอยู่ ต้องการปิดใช้งาน
          if (_pincodestatus) {
            _checkAutoVerify();
          } else {
            // ต้องการตั้งค่า pin เพื่อใช้งาน
            _setAutoVerify();
          }
        } else {
          // ถ้ามาจากหน้าแรกจะเป็นการตรวจสอบการเข้าใช้งาน
          _verifyCode(); // ตรวจสอบการเข้าใช้งานด้วย pin
        }
      }
    });
  }

  // ฟังก์ชั่นลบตัวเลข pin
  void _onDelete() {
    setState(() {
      if (pin.isNotEmpty) {
        pin = pin.substring(0, pin.length - 1);
      }
    });
  }

  // ฟังก์ชั่นสร้างส่วนแสดง pin
  Widget _buildPinDisplay() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(pinLength, (index) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: CircleAvatar(
            radius: 10,
            backgroundColor:
                index < pin.length ? Colors.black : Colors.grey[300],
          ),
        );
      }),
    );
  }

  // ฟังก์ชั่นสร้างปุ่มต่างๆ 
  Widget _buildKey(String value) {
    return GestureDetector(
      onTap: () => _onKeyPressed(value, _forAuthen),
      child: CircleAvatar(
        radius: 30,
        backgroundColor: Colors.grey[200],
        child: Text(
          value,
          style: TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
            color: Colors.black38,
          ),
        ),
      ),
    );
  }

  // ฟังก์ชั่นสำหรับสร้างปุ่มกด
  Widget _buildKeypad() {
    return GridView.builder(
      shrinkWrap: true,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        mainAxisSpacing: 15,
        crossAxisSpacing: 15,
      ),
      itemCount: 12, // 0-9, delete key, empty key
      itemBuilder: (context, index) {
        if (index == 9) { // ปุ่มที่ 9
          if (_forAuthen) { // มาจากหน้าแรกไม่มีปุ่มยกเลิก
            return SizedBox(); // Empty space
          } else {
            // ถ้าไม่ใช่มาจากหน้าแรก สามารถยกเลิกได้
            return GestureDetector(
              onTap: () {
                Navigator.of(context).pop();
              },
              child: CircleAvatar(
                radius: 30,
                backgroundColor: Colors.red[100],
                child: Text(
                  'ยกเลิก',
                  style: TextStyle(
                    color: Colors.black38,
                  ),
                ),
              ),
            );
          }
        } else if (index == 10) {
          // ปุมที่ 1 เลข 0
          return _buildKey('0');
        } else if (index == 11) {
          // ปุ่มมุมขวาสุด ปุ่มลบ
          return GestureDetector(
            onTap: _onDelete,
            child: CircleAvatar(
              radius: 30,
              backgroundColor: Colors.grey[200],
              child: Icon(
                Icons.backspace,
                color: Colors.black38,
              ),
            ),
          );
        } else {
          // ปุ่มตัวเลข
          return _buildKey((index + 1).toString());
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoadingPrefs) {
      // คืนค่ากรณี กำลังโหลดค่า SharedPreferences ยังไม่เสร็จ
      return const Center(child: SizedBox.shrink());
    }
    print("debug: _pincodestatus ${_pincodestatus}");
    print("debug: _authorized ${_authorized}");
    print("debug: _pincodevalue ${_pincodevalue}");

    // รับค่า arguments ที่ส่งมาใช้งาน จากการเข้าใช้งานแอปครั้งแรก และมีการเปิดใช้งาน pin
    final args = ModalRoute.of(context)!.settings.arguments as String?;
    if (args != null) { // ถ้ามีการส่งค่ามา แสดงว่าเปิดจากหน้าแรก
      // เปิดจากหน้าแรก 
      _forAuthen = true;
    }
    print("debug: args ${args}");
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 50.0, horizontal: 30.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              args != null // ถ้าสงมาจากหน้าแรก
                  ? 'ใส่รหัส PIN เพื่อเข้าใช้งาน'
                  : _pincodestatus // ถ้าเป็นการตั้งค่า
                      ? 'ใส่รหัส PIN เพื่อยกเลิก'
                      : 'ตั้งรหัส PIN เพื่อยเปิดใช้งานล็อกแอป',
              style: TextStyle(fontSize: 20, color: Colors.grey),
            ),
            SizedBox(height: 20),
            _buildPinDisplay(), // สร้าง pin
            SizedBox(height: 40),
            _buildKeypad(), // สร้างปุ่ม
          ],
        ),
      ),
    );
  }
}


 



กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ