การจัดการ Local Storage ด้วย SharedPreferences และ SQLite ใน Flutter
ในบทความที่แล้ว เราได้เรียนรู้วิธีการดึงข้อมูลจาก API เพื่อนำมาแสดงผลในแอปพลิเคชัน Flutter ของเรา แต่ข้อมูลที่ดึงมานั้นเป็นข้อมูลที่อยู่บน Server เมื่อแอปพลิเคชันปิดไป ข้อมูลเหล่านั้นก็จะหายไป ในบทความนี้ เราจะมาเรียนรู้วิธีการจัดการข้อมูลในเครื่อง (Local Storage) เพื่อให้ข้อมูลยังคงอยู่แม้ว่าแอปพลิเคชันจะถูกปิดไปแล้วก็ตาม โดยเราจะครอบคลุมถึง SharedPreferences และ SQLite ซึ่งเป็นสองวิธีที่นิยมใช้กันในการจัดการข้อมูลในเครื่อง
SharedPreferences: สำหรับข้อมูลขนาดเล็ก
SharedPreferences เป็นวิธีที่ง่ายที่สุดในการจัดเก็บข้อมูลขนาดเล็ก เช่น การตั้งค่าของผู้ใช้ (User Preferences), ข้อมูลการ Login หรือข้อมูลอื่นๆ ที่มีขนาดไม่ใหญ่มากนัก SharedPreferences จะจัดเก็บข้อมูลในรูปแบบ key-value pair โดยข้อมูลจะถูกจัดเก็บในรูปแบบ XML (บน Android) หรือ plist (บน iOS)
การใช้งาน SharedPreferences
ในการใช้งาน SharedPreferences เราจะต้องทำการ import package `shared_preferences` เข้ามาในโปรเจกต์ของเราก่อน โดยการเพิ่ม dependency เข้าไปในไฟล์ `pubspec.yaml`:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.2 # ตรวจสอบเวอร์ชันล่าสุดเสมอ
หลังจากนั้นให้รันคำสั่ง `flutter pub get` เพื่อทำการติดตั้ง package
การอ่านและเขียนข้อมูลด้วย SharedPreferences
เมื่อติดตั้ง package เรียบร้อยแล้ว เราสามารถใช้งาน SharedPreferences ได้ดังนี้:
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
// Initialize Flutter binding เพื่อให้แน่ใจว่า SharedPreferences ถูก initialize ก่อน
WidgetsFlutterBinding.ensureInitialized();
// ดึง instance ของ SharedPreferences
final prefs = await SharedPreferences.getInstance();
// เขียนข้อมูล (String)
await prefs.setString('username', 'JohnDoe');
// เขียนข้อมูล (Integer)
await prefs.setInt('age', 30);
// เขียนข้อมูล (Boolean)
await prefs.setBool('isLoggedIn', true);
// อ่านข้อมูล (String)
final username = prefs.getString('username');
print('Username: $username'); // Output: Username: JohnDoe
// อ่านข้อมูล (Integer)
final age = prefs.getInt('age');
print('Age: $age'); // Output: Age: 30
// อ่านข้อมูล (Boolean)
final isLoggedIn = prefs.getBool('isLoggedIn');
print('Is Logged In: $isLoggedIn'); // Output: Is Logged In: true
// ลบข้อมูล
await prefs.remove('username');
// ตรวจสอบว่ามี key นี้หรือไม่
final containsUsername = prefs.containsKey('username');
print('Contains Username: $containsUsername'); // Output: Contains Username: false
// ล้างข้อมูลทั้งหมด
// await prefs.clear();
}
คำอธิบายโค้ด:
- `SharedPreferences.getInstance()`: เป็น asynchronous function ที่จะคืนค่า instance ของ SharedPreferences
- `prefs.setString()`, `prefs.setInt()`, `prefs.setBool()`: เป็น method สำหรับเขียนข้อมูลประเภท String, Integer และ Boolean ตามลำดับ
- `prefs.getString()`, `prefs.getInt()`, `prefs.getBool()`: เป็น method สำหรับอ่านข้อมูลประเภท String, Integer และ Boolean ตามลำดับ
- `prefs.remove()`: เป็น method สำหรับลบข้อมูล
- `prefs.containsKey()`: เป็น method สำหรับตรวจสอบว่ามี key นั้นอยู่ใน SharedPreferences หรือไม่
- `prefs.clear()`: เป็น method สำหรับล้างข้อมูลทั้งหมดใน SharedPreferences
ข้อควรระวัง: SharedPreferences เหมาะสำหรับข้อมูลขนาดเล็กเท่านั้น หากต้องการจัดเก็บข้อมูลที่มีโครงสร้างที่ซับซ้อน หรือข้อมูลที่มีขนาดใหญ่ ควรใช้ SQLite แทน
SQLite: สำหรับข้อมูลที่มีโครงสร้างและขนาดใหญ่
SQLite เป็น relational database management system (RDBMS) ที่มีขนาดเล็กและทำงานแบบ embedded ซึ่งหมายความว่าฐานข้อมูลจะถูกจัดเก็บอยู่ในไฟล์เดียวภายในแอปพลิเคชันของเรา ทำให้ไม่ต้องพึ่งพา Server ภายนอก SQLite เหมาะสำหรับการจัดเก็บข้อมูลที่มีโครงสร้างที่ซับซ้อน เช่น ข้อมูลผู้ใช้, ข้อมูลสินค้า หรือข้อมูลอื่นๆ ที่ต้องการ query ข้อมูลด้วย SQL
การใช้งาน SQLite
ในการใช้งาน SQLite เราจะต้องทำการ import package `sqflite` และ `path_provider` เข้ามาในโปรเจกต์ของเราก่อน โดยการเพิ่ม dependency เข้าไปในไฟล์ `pubspec.yaml`:
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0 # ตรวจสอบเวอร์ชันล่าสุดเสมอ
path_provider: ^2.1.2 # ตรวจสอบเวอร์ชันล่าสุดเสมอ
หลังจากนั้นให้รันคำสั่ง `flutter pub get` เพื่อทำการติดตั้ง package
การสร้างฐานข้อมูลและตาราง
ก่อนที่เราจะสามารถใช้งาน SQLite ได้ เราจะต้องสร้างฐานข้อมูลและตารางก่อน:
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
class DatabaseHelper {
static final _databaseName = "MyDatabase.db";
static final _databaseVersion = 1;
static final table = 'my_table';
static final columnId = '_id';
static final columnName = 'name';
static final columnAge = 'age';
// ทำให้เป็น singleton class
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
// only have a single app-wide reference to the database
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
// lazily instantiate the db the first time it is accessed
_database = await _initDatabase();
return _database!;
}
// this opens the database (and creates it if it doesn't exist)
_initDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, _databaseName);
return await openDatabase(path,
version: _databaseVersion, onCreate: _onCreate);
}
// SQL code to create the database table
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $table (
$columnId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnName TEXT NOT NULL,
$columnAge INTEGER NOT NULL
)
''');
}
// Helper methods
// Inserts a row in the database where each key in the Map is a column name
// and the value is the column value. The return value is the id of the
// inserted row.
Future<int> insert(Map<String, dynamic> row) async {
Database db = await instance.database;
return await db.insert(table, row);
}
// All of the rows are returned as a list of maps, where each map is
// a key-value list of columns.
Future<List<Map<String, dynamic>>> queryAllRows() async {
Database db = await instance.database;
return await db.query(table);
}
// All of the methods (insert, query, update, delete) can also be done using
// raw SQL commands. Raw methods are provided to give you full control.
Future<int?> queryRowCount() async {
Database db = await instance.database;
return Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM $table'));
}
// We are assuming here that the id column in the map is set. The other
// column values will be used to update the row.
Future<int> update(Map<String, dynamic> row) async {
Database db = await instance.database;
int id = row[columnId];
return await db.update(table, row, where: '$columnId = ?', whereArgs: [id]);
}
// Deletes the row specified by the id. The number of affected rows is
// returned. This should be 1 as long as the row exists.
Future<int> delete(int id) async {
Database db = await instance.database;
return await db.delete(table, where: '$columnId = ?', whereArgs: [id]);
}
}
คำอธิบายโค้ด:
- `DatabaseHelper`: เป็น class ที่จัดการการเชื่อมต่อกับฐานข้อมูล และมี method สำหรับ CRUD operations
- `_databaseName`: ชื่อของไฟล์ฐานข้อมูล
- `_databaseVersion`: เวอร์ชันของฐานข้อมูล
- `table`: ชื่อของตาราง
- `columnId`, `columnName`, `columnAge`: ชื่อของ column ในตาราง
- `_initDatabase()`: สร้างและเปิดฐานข้อมูล
- `_onCreate()`: สร้างตารางในฐานข้อมูล
- `insert()`: เพิ่มข้อมูลลงในตาราง
- `queryAllRows()`: ดึงข้อมูลทั้งหมดจากตาราง
- `queryRowCount()`: นับจำนวนแถวในตาราง
- `update()`: แก้ไขข้อมูลในตาราง
- `delete()`: ลบข้อมูลในตาราง
การใช้งาน DatabaseHelper
เราสามารถใช้งาน `DatabaseHelper` ได้ดังนี้:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final dbHelper = DatabaseHelper.instance;
// row to insert
Map<String, dynamic> row = {
DatabaseHelper.columnName : 'John Doe',
DatabaseHelper.columnAge : 30
};
final id = await dbHelper.insert(row);
print('inserted row id: $id');
// query all rows
final allRows = await dbHelper.queryAllRows();
print('query all rows:');
allRows.forEach(print);
// query row count
final count = await dbHelper.queryRowCount();
print('row count is: $count');
// update row
Map<String, dynamic> rowToUpdate = {
DatabaseHelper.columnId : 1,
DatabaseHelper.columnName : 'Jane Doe',
DatabaseHelper.columnAge : 35
};
final rowsAffected = await dbHelper.update(rowToUpdate);
print('updated $rowsAffected row(s)');
// query all rows after update
final allRowsAfterUpdate = await dbHelper.queryAllRows();
print('query all rows after update:');
allRowsAfterUpdate.forEach(print);
// delete row
final deletedRows = await dbHelper.delete(1);
print('deleted $deletedRows row(s)');
// query all rows after delete
final allRowsAfterDelete = await dbHelper.queryAllRows();
print('query all rows after delete:');
allRowsAfterDelete.forEach(print);
}
คำอธิบายโค้ด:
โค้ดนี้แสดงให้เห็นถึงการใช้งาน `DatabaseHelper` ในการเพิ่ม, ดึง, แก้ไข และลบข้อมูลในฐานข้อมูล SQLite
สรุป
ในบทความนี้ เราได้เรียนรู้วิธีการจัดการข้อมูลในเครื่องด้วย SharedPreferences และ SQLite โดย SharedPreferences เหมาะสำหรับข้อมูลขนาดเล็กและการตั้งค่าของผู้ใช้ ในขณะที่ SQLite เหมาะสำหรับข้อมูลที่มีโครงสร้างที่ซับซ้อนและมีขนาดใหญ่ การเลือกใช้วิธีการจัดเก็บข้อมูลที่เหมาะสมจะช่วยให้แอปพลิเคชันของเราทำงานได้อย่างมีประสิทธิภาพและจัดการข้อมูลได้อย่างถูกต้อง
ในบทความถัดไป เราจะเรียนรู้วิธีการใช้ Firebase Authentication ใน Flutter เพื่อจัดการการยืนยันตัวตนของผู้ใช้