บทที่ 13: Local Storage (SharedPreferences)
สารบัญเนื้อหา
บทที่ 13: Local Storage (SharedPreferences)
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจความแตกต่างระหว่าง Local Storage และ Database
- ใช้ SharedPreferences เพื่อเก็บค่าการตั้งค่า (Settings) และสถานะการล็อกอิน (Session)
- บันทึกและดึงข้อมูลประเภทต่างๆ (String, int, bool, JSON)
- สร้าง Service Class เพื่อจัดการข้อมูลในเครื่องอย่างเป็นระบบ
📚 ทฤษฎีและแนวคิด
1. Local Storage vs Database
- SharedPreferences: เปรียบเสมือนสมุดจดเล่มเล็กๆ เก็บข้อมูลง่ายๆ เช่น "เปิดเสียงไหม = ใช่", "ชื่อผู้ใช้ = สมชาย" เหมาะสำหรับ Key-Value pairs
- Database (SQLite/Hive): เปรียบเสมือนตู้เก็บเอกสารขนาดใหญ่ เก็บข้อมูลซับซ้อน เช่น "รายการบันทึก 1,000 รายการ"
2. เมื่อไหร่ควรใช้ SharedPreferences?
✅ เก็บ Token การเข้าสู่ระบบ ✅ เก็บค่า Settings (Dark Mode, ภาษา) ✅ เก็บ Flag เล็กๆ น้อยๆ (เช่น "เคยเปิด Intro แล้วหรือยัง")
❌ ไม่ควรใช้เก็บ: ข้อมูลขนาดใหญ่ (รูปภาพ), รายการที่มีความสัมพันธ์กันซับซ้อน
💻 การเขียนโค้ด (Implementation)
1. ติดตั้ง Package
# pubspec.yaml
dependencies:
shared_preferences: ^2.2.0
2. สร้าง Session Manager
ไฟล์: mobile/lib/services/session.dart
เราจะสร้าง Class นี้เพื่อจัดการข้อมูล "การเข้าสู่ระบบ" ของผู้ใช้โดยเฉพาะ
import 'dart:convert'; // ใช้แปลง Object <-> String
import 'package:shared_preferences/shared_preferences.dart';
class Session {
อธิบายทีละบรรทัด (Imports):
-
import 'dart:convert'- ใช้สำหรับแปลง Object เป็น String (Encode) ก่อนบันทึกลงเครื่อง
-
import 'package:shared_preferences/shared_preferences.dart'- Library หลักสำหรับอ่าน/เขียนข้อมูลลง Disk ของมือถือ (iOS Uses NSUserDefaults, Android uses SharedPreferences)
อธิบาย: // ตัวแปร static เพื่อเก็บค่าไว้ใน RAM ให้เรียกใช้ได้เร็วๆ ไม่ต้องอ่านจาก Disk ตลอด static String? token; static Map<String, dynamic>? user;
// --- 1. Initialization (เริ่มต้นทำงาน) --- // เรียกฟังก์ชันนี้ตอนเปิดแอป (Splash Screen) static Future<void> init() async { // ขอ instance จากระบบ (เปรียบเหมือนขอลิ้นชักส่วนตัว) final prefs = await SharedPreferences.getInstance();
// โหลดข้อมูลใส่ RAM
token = prefs.getString('auth_token');
// ข้อมูล User ถูกเก็บเป็น JSON String ต้องแปลงกลับเป็น Map
final userStr = prefs.getString('auth_user');
if (userStr != null) {
try {
user = jsonDecode(userStr);
} catch (e) {
print("Error decoding user: $e");
}
}
}
// --- 2. Save (บันทึกข้อมูล) --- // เรียกเมื่อ Login สำเร็จ static Future<void> save(String newToken, Map<String, dynamic> newUser) async { // 1. อัปเดตใน RAM ก่อน (เพื่อให้แอปใช้งานได้ทันที) token = newToken; user = newUser;
// 2. บันทึกลง Disk (เพื่อให้ปิดแอปแล้วข้อมูลไม่หาย)
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', newToken);
// Object เก็บลงตรงๆ ไม่ได้ ต้องแปลงเป็น JSON String ก่อน
await prefs.setString('auth_user', jsonEncode(newUser));
}
// --- 3. Clear (ล้างข้อมูล) --- // เรียกเมื่อ Logout static Future<void> clear() async { token = null; user = null;
final prefs = await SharedPreferences.getInstance();
// ลบเฉพาะ Key ที่เราต้องการ
await prefs.remove('auth_token');
await prefs.remove('auth_user');
// หรือถ้าจะลบเกลี้ยงทั้งแอปใช้: await prefs.clear();
}
// Helper เช็คว่า Login อยู่ไหม static bool get isLoggedIn => token != null; }
### 3. สร้าง Theme Controller (จัดการการตั้งค่า)
ไฟล์: `mobile/lib/services/theme_controller.dart`
ตัวอย่างการใช้ SharedPreferences เก็บการตั้งค่าที่ไม่ใช่ Security
```dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeController {
// Singleton Pattern: ให้ทั้งแอปมี Controller ตัวเดียว
static final ThemeController instance = ThemeController._();
ThemeController._();
// ValueNotifier: ตัวแปรที่บอก UI ให้รีเฟรชได้เมื่อค่าเปลี่ยน
final ValueNotifier<ThemeMode> themeMode = ValueNotifier(ThemeMode.system);
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
// อ่านค่าที่บันทึกไว้ (คืนค่ามาเป็น String เช่น "dark", "light")
final saved = prefs.getString('theme_mode');
if (saved != null) {
// แปลง String กลับเป็น ThemeMode Enum
themeMode.value = ThemeMode.values.firstWhere(
(e) => e.name == saved,
orElse: () => ThemeMode.system, // ค่าเริ่มต้นถ้าหาไม่เจอ
);
}
}
Future<void> setTheme(ThemeMode mode) async {
// 1. เปลี่ยนค่าในตัวแปร -> UI เปลี่ยนทันที
themeMode.value = mode;
// 2. บันทึกลงเครื่อง -> ปิดเปิดใหม่ค่าไม่หาย
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme_mode', mode.name); // เก็บชื่อ Enum (เช่น "dark")
}
}
4. การใช้งานจริง (Splash Screen)
ไฟล์: mobile/lib/screens/splash_screen.dart
Future<void> _checkAuth() async {
// 1. เริ่มต้นโหลดข้อมูลจาก Disk
await Session.init();
await ThemeController.instance.init();
// 2. เช็คว่ามี Token ไหม?
if (Session.isLoggedIn) {
// ไปหน้าหลัก
} else {
// ไปหน้า Login
}
}
5. ชนิดข้อมูลที่เก็บได้ (Supported Types)
SharedPreferences รองรับข้อมูลพื้นฐานเท่านั้น:
prefs.setString('username', 'Somchai');
prefs.setInt('score', 100);
prefs.setBool('is_active', true);
prefs.setDouble('height', 175.5);
prefs.setStringList('tags', ['work', 'life']); // เก็บ Array ของ String ได้
ถ้าต้องการเก็บ Object ซับซ้อน (เช่น DateTime, Custom Class) ต้องแปลงเป็น String หรือ Int ก่อน (เช่นเก็บเป็น timestamp, json string)
📝 แบบฝึกหัด
โจทย์ที่ 1: "พึ่งเคยเปิดครั้งแรก" (First Launch)
สิ่งที่ต้องทำ:
- สร้าง Key ชื่อ
is_first_launch - ใน Splash Screen ให้เช็คว่าค่านี้เป็น
trueหรือfalse(default คือ null ให้ถือว่าเป็น true) - ถ้าเป็นครั้งแรก ให้พาไปหน้า "Onboarding" (แนะนำแอพ)
- เมื่อดู Onboarding จบ ให้บันทึกค่าเป็น
false
โจทย์ที่ 2: จำภาษาล่าสุด (Language Persistance)
สิ่งที่ต้องทำ:
- เพิ่ม logic ใน
LanguageController(บทที่ 16) - ใช้
prefs.setString('language_code', 'th')เมื่อเปลี่ยนภาษา - ใช้
prefs.getString('language_code')ตอนเปิดแอปเพื่อคืนค่า
โจทย์ที่ 3: ระบบ Favorite ชั่วคราว
สิ่งที่ต้องทำ:
- ใช้
setStringList('favorite_ids') - เมื่อกดหัวใจที่ Note ให้เพิ่ม ID ลงใน List แล้วบันทึก
- เมื่อเปิดแอป ให้โหลด List นี้มาเช็คว่า Note ไหนเป็น Favorite บ้าง
📌 สรุป
SharedPreferences คือเพื่อนแท้ของนักพัฒนา Mobile App
- ง่าย: Key-Value คู่กัน
- เร็ว: เหมาะกับข้อมูลเล็กๆ
- ถาวร: ปิดแอป เปิดเครื่องใหม่ ข้อมูลยังอยู่
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 14: Speech-to-Text Integration ถึงเวลาของฟีเจอร์พระเอก! เราจะมาทำระบบแปลงเสียงเป็นข้อความกัน