flutter學(xué)習(xí)第 13 節(jié):本地存儲(chǔ)

在移動(dòng)應(yīng)用開(kāi)發(fā)中,本地存儲(chǔ)是一項(xiàng)關(guān)鍵功能,用于保存用戶偏好設(shè)置、離線數(shù)據(jù)、登錄狀態(tài)等信息。Flutter 提供了多種本地存儲(chǔ)方案,適用于不同的場(chǎng)景需求。本節(jié)課將介紹 Flutter 中常用的本地存儲(chǔ)方式,包括輕量級(jí)的鍵值對(duì)存儲(chǔ)和更復(fù)雜的數(shù)據(jù)庫(kù)存儲(chǔ),并通過(guò)實(shí)例展示其實(shí)際應(yīng)用。

一、本地存儲(chǔ)概述

本地存儲(chǔ)在移動(dòng)應(yīng)用中具有重要作用,主要應(yīng)用場(chǎng)景包括:

  • 保存用戶登錄狀態(tài),避免重復(fù)登錄
  • 存儲(chǔ)應(yīng)用配置和用戶偏好設(shè)置
  • 緩存網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù),實(shí)現(xiàn)離線功能
  • 存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù),如聊天記錄、任務(wù)列表等

Flutter 中常用的本地存儲(chǔ)方案有:

  1. shared_preferences:輕量級(jí)鍵值對(duì)存儲(chǔ),適用于簡(jiǎn)單數(shù)據(jù)
  2. sqflite:SQLite 數(shù)據(jù)庫(kù)封裝,適用于結(jié)構(gòu)化數(shù)據(jù)
  3. hive:高性能 NoSQL 數(shù)據(jù)庫(kù),純 Dart 實(shí)現(xiàn)
  4. flutter_secure_storage:安全存儲(chǔ),適用于敏感信息如令牌

本節(jié)課重點(diǎn)介紹前兩種最常用的存儲(chǔ)方案。



二、輕量級(jí)存儲(chǔ):shared_preferences

shared_preferences 是 Flutter 社區(qū)提供的一個(gè)插件,用于存儲(chǔ)簡(jiǎn)單的鍵值對(duì)數(shù)據(jù)。它在 iOS 上使用 NSUserDefaults,在 Android 上使用 SharedPreferences,提供了跨平臺(tái)的一致 API。

1. 安裝與配置

pubspec.yaml 中添加依賴:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.5.3  # 使用最新版本

運(yùn)行 flutter pub get 安裝依賴。

2. 基本使用方法

shared_preferences 支持存儲(chǔ)的數(shù)據(jù)類型包括:String、int、doubleboolList<String>。

基本操作步驟:

  1. 獲取 SharedPreferences 實(shí)例
  2. 使用相應(yīng)的方法進(jìn)行數(shù)據(jù)讀寫
  3. 無(wú)需手動(dòng)關(guān)閉實(shí)例
import 'package:shared_preferences/shared_preferences.dart';

// 保存數(shù)據(jù)
Future<void> saveData() async {
  // 獲取實(shí)例
  final prefs = await SharedPreferences.getInstance();

  // 存儲(chǔ)不同類型的數(shù)據(jù)
  await prefs.setString('username', 'john_doe');
  await prefs.setInt('age', 30);
  await prefs.setDouble('height', 1.75);
  await prefs.setBool('isPremium', false);
  await prefs.setStringList('hobbies', ['reading', 'sports', 'coding']);
}

// 讀取數(shù)據(jù)
Future<void> readData() async {
  final prefs = await SharedPreferences.getInstance();

  // 讀取數(shù)據(jù),提供默認(rèn)值
  String username = prefs.getString('username') ?? 'Guest';
  int age = prefs.getInt('age') ?? 0;
  double height = prefs.getDouble('height') ?? 0.0;
  bool isPremium = prefs.getBool('isPremium') ?? false;
  List<String> hobbies = prefs.getStringList('hobbies') ?? [];

  print('Username: $username');
  print('Age: $age');
  print('Height: $height');
  print('Is Premium: $isPremium');
  print('Hobbies: $hobbies');
}

// 更新數(shù)據(jù)
Future<void> updateData() async {
  final prefs = await SharedPreferences.getInstance();
  // 更新已存在的鍵的值
  await prefs.setInt('age', 31);
  await prefs.setBool('isPremium', true);
}

// 刪除數(shù)據(jù)
Future<void> deleteData() async {
  final prefs = await SharedPreferences.getInstance();
  // 刪除指定鍵
  await prefs.remove('height');

  // 清除所有數(shù)據(jù)
  // await prefs.clear();
}

3. shared_preferences 封裝

為了簡(jiǎn)化使用并避免重復(fù)代碼,可以封裝一個(gè)工具類:

import 'package:shared_preferences/shared_preferences.dart';

class PrefsService {
  // 單例模式
  static final PrefsService _instance = PrefsService._internal();
  factory PrefsService() => _instance;
  PrefsService._internal();

  late SharedPreferences _prefs;

  // 初始化
  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  // 存儲(chǔ)方法
  Future<void> setString(String key, String value) async {
    await _prefs.setString(key, value);
  }

  Future<void> setInt(String key, int value) async {
    await _prefs.setInt(key, value);
  }

  Future<void> setDouble(String key, double value) async {
    await _prefs.setDouble(key, value);
  }

  Future<void> setBool(String key, bool value) async {
    await _prefs.setBool(key, value);
  }

  Future<void> setStringList(String key, List<String> value) async {
    await _prefs.setStringList(key, value);
  }

  // 讀取方法
  String getString(String key, {String defaultValue = ''}) {
    return _prefs.getString(key) ?? defaultValue;
  }

  int getInt(String key, {int defaultValue = 0}) {
    return _prefs.getInt(key) ?? defaultValue;
  }

  double getDouble(String key, {double defaultValue = 0.0}) {
    return _prefs.getDouble(key) ?? defaultValue;
  }

  bool getBool(String key, {bool defaultValue = false}) {
    return _prefs.getBool(key) ?? defaultValue;
  }

  List<String> getStringList(String key, {List<String> defaultValue = const []}) {
    return _prefs.getStringList(key) ?? defaultValue;
  }

  // 刪除方法
  Future<void> remove(String key) async {
    await _prefs.remove(key);
  }

  Future<void> clear() async {
    await _prefs.clear();
  }

  // 檢查鍵是否存在
  bool containsKey(String key) {
    return _prefs.containsKey(key);
  }
}

使用封裝類:

// 初始化(通常在 app 啟動(dòng)時(shí))
await PrefsService().init();

// 存儲(chǔ)數(shù)據(jù)
await PrefsService().setString('username', 'jane_smith');
await PrefsService().setInt('score', 100);

// 讀取數(shù)據(jù)
String username = PrefsService().getString('username');
int score = PrefsService().getInt('score');

4. shared_preferences 適用場(chǎng)景與局限性

適用場(chǎng)景

  • 存儲(chǔ)用戶偏好設(shè)置(如主題模式、語(yǔ)言選擇)
  • 保存簡(jiǎn)單的用戶狀態(tài)(如登錄狀態(tài)標(biāo)記)
  • 存儲(chǔ)少量的配置信息

局限性

  • 不適合存儲(chǔ)大量數(shù)據(jù)
  • 不支持復(fù)雜數(shù)據(jù)結(jié)構(gòu)
  • 數(shù)據(jù)存儲(chǔ)在明文文件中,不適合存儲(chǔ)敏感信息
  • 沒(méi)有查詢功能,只能通過(guò)鍵獲取值


三、數(shù)據(jù)庫(kù)存儲(chǔ):sqflite 基礎(chǔ)

對(duì)于需要存儲(chǔ)大量結(jié)構(gòu)化數(shù)據(jù)的場(chǎng)景,sqflite 是更好的選擇。sqflite 是 Flutter 中 SQLite 數(shù)據(jù)庫(kù)的封裝,提供了完整的 SQL 操作能力。

1. 安裝與配置

pubspec.yaml 中添加依賴:

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.4.2  # SQLite 數(shù)據(jù)庫(kù)
  path: ^1.9.1     # 用于處理文件路徑

運(yùn)行 flutter pub get 安裝依賴。

2. 數(shù)據(jù)庫(kù)基本操作

SQLite 數(shù)據(jù)庫(kù)操作主要包括:創(chuàng)建數(shù)據(jù)庫(kù)和表、插入數(shù)據(jù)、查詢數(shù)據(jù)、更新數(shù)據(jù)和刪除數(shù)據(jù)。

創(chuàng)建數(shù)據(jù)庫(kù)和表

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  // 數(shù)據(jù)庫(kù)名稱
  static const _databaseName = "MyDatabase.db";
  // 數(shù)據(jù)庫(kù)版本
  static const _databaseVersion = 1;

  // 表名
  static const table = 'notes';

  // 列名
  static const columnId = '_id';
  static const columnTitle = 'title';
  static const columnContent = 'content';
  static const columnCreatedAt = 'created_at';

  // 單例模式
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // 數(shù)據(jù)庫(kù)實(shí)例
  static Database? _database;
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  // 初始化數(shù)據(jù)庫(kù)
  _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(path,
        version: _databaseVersion,
        onCreate: _onCreate);
  }

  // 創(chuàng)建表
  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $table (
            $columnId INTEGER PRIMARY KEY AUTOINCREMENT,
            $columnTitle TEXT NOT NULL,
            $columnContent TEXT,
            $columnCreatedAt TEXT NOT NULL
          )
          ''');
  }
}

插入數(shù)據(jù)

// 插入數(shù)據(jù)
Future<int> insertNote(Map<String, dynamic> note) async {
  Database db = await DatabaseHelper.instance.database;
  // 插入數(shù)據(jù)并返回新記錄的 ID
  return await db.insert(DatabaseHelper.table, note);
}

// 使用示例
void addNote() async {
  Map<String, dynamic> newNote = {
    DatabaseHelper.columnTitle: 'First Note',
    DatabaseHelper.columnContent: 'This is my first note',
    DatabaseHelper.columnCreatedAt: DateTime.now().toIso8601String()
  };

  int id = await insertNote(newNote);
  print('Inserted note with id: $id');
}

查詢數(shù)據(jù)

// 查詢所有數(shù)據(jù)
Future<List<Map<String, dynamic>>> queryAllNotes() async {
  Database db = await DatabaseHelper.instance.database;
  return await db.query(DatabaseHelper.table);
}

// 根據(jù) ID 查詢
Future<List<Map<String, dynamic>>> queryNote(int id) async {
  Database db = await DatabaseHelper.instance.database;
  return await db.query(DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id]);
}

// 使用示例
void getNotes() async {
  List<Map<String, dynamic>> notes = await queryAllNotes();
  print('All notes: $notes');

  if (notes.isNotEmpty) {
    List<Map<String, dynamic>> singleNote = await queryNote(notes[0][DatabaseHelper.columnId]);
    print('First note: $singleNote');
  }
}

更新數(shù)據(jù)

// 更新數(shù)據(jù)
Future<int> updateNote(Map<String, dynamic> note) async {
  Database db = await DatabaseHelper.instance.database;
  int id = note[DatabaseHelper.columnId];
  // 返回受影響的行數(shù)
  return await db.update(DatabaseHelper.table, note,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id]);
}

// 使用示例
void modifyNote(int noteId) async {
  Map<String, dynamic> updatedNote = {
    DatabaseHelper.columnId: noteId,
    DatabaseHelper.columnTitle: 'Updated Note',
    DatabaseHelper.columnContent: 'This note has been updated',
    DatabaseHelper.columnCreatedAt: DateTime.now().toIso8601String()
  };

  int rowsAffected = await updateNote(updatedNote);
  print('Updated $rowsAffected rows');
}

刪除數(shù)據(jù)

// 刪除數(shù)據(jù)
Future<int> deleteNote(int id) async {
  Database db = await DatabaseHelper.instance.database;
  // 返回受影響的行數(shù)
  return await db.delete(DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id]);
}

// 使用示例
void removeNote(int noteId) async {
  int rowsDeleted = await deleteNote(noteId);
  print('Deleted $rowsDeleted rows');
}

3. 數(shù)據(jù)庫(kù)版本遷移

當(dāng)應(yīng)用升級(jí)需要修改數(shù)據(jù)庫(kù)結(jié)構(gòu)時(shí),需要進(jìn)行數(shù)據(jù)庫(kù)遷移:

// 在 DatabaseHelper 類中修改 _initDatabase 方法
_initDatabase() async {
  String path = join(await getDatabasesPath(), _databaseName);
  return await openDatabase(
    path,
    version: _databaseVersion,
    onCreate: _onCreate,
    onUpgrade: _onUpgrade, // 添加升級(jí)回調(diào)
    onDowngrade: onDatabaseDowngradeDelete, // 降級(jí)時(shí)刪除數(shù)據(jù)庫(kù)
  );
}

// 數(shù)據(jù)庫(kù)升級(jí)
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
  if (oldVersion < 2) {
    // 版本 2 新增了一個(gè) 'category' 列
    await db.execute('''
      ALTER TABLE ${DatabaseHelper.table} 
      ADD COLUMN category TEXT
    ''');
  }
  if (oldVersion < 3) {
    // 版本 3 新增了一個(gè) 'priority' 列
    await db.execute('''
      ALTER TABLE ${DatabaseHelper.table} 
      ADD COLUMN priority INTEGER DEFAULT 0
    ''');
  }
}

4. 模型類與數(shù)據(jù)庫(kù)操作封裝

為了更好地管理數(shù)據(jù),通常會(huì)創(chuàng)建模型類并封裝數(shù)據(jù)庫(kù)操作:

// 筆記模型類
class Note {
  final int? id;
  final String title;
  final String? content;
  final String createdAt;
  final String? category;
  final int priority;

  Note({
    this.id,
    required this.title,
    this.content,
    required this.createdAt,
    this.category,
    this.priority = 0,
  });

  // 從 Map 轉(zhuǎn)換為 Note 對(duì)象
  factory Note.fromMap(Map<String, dynamic> map) {
    return Note(
      id: map[DatabaseHelper.columnId],
      title: map[DatabaseHelper.columnTitle],
      content: map[DatabaseHelper.columnContent],
      createdAt: map[DatabaseHelper.columnCreatedAt],
      category: map['category'],
      priority: map['priority'] ?? 0,
    );
  }

  // 轉(zhuǎn)換為 Map
  Map<String, dynamic> toMap() {
    return {
      DatabaseHelper.columnId: id,
      DatabaseHelper.columnTitle: title,
      DatabaseHelper.columnContent: content,
      DatabaseHelper.columnCreatedAt: createdAt,
      'category': category,
      'priority': priority,
    };
  }
}

// 封裝數(shù)據(jù)庫(kù)操作
class NoteService {
  // 插入筆記
  Future<int> insertNote(Note note) async {
    Database db = await DatabaseHelper.instance.database;
    return await db.insert(DatabaseHelper.table, note.toMap());
  }

  // 獲取所有筆記
  Future<List<Note>> getAllNotes() async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(DatabaseHelper.table);
    return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
  }

  // 根據(jù) ID 獲取筆記
  Future<Note?> getNoteById(int id) async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id],
    );
    if (maps.isNotEmpty) {
      return Note.fromMap(maps.first);
    }
    return null;
  }

  // 更新筆記
  Future<int> updateNote(Note note) async {
    Database db = await DatabaseHelper.instance.database;
    return await db.update(
      DatabaseHelper.table,
      note.toMap(),
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [note.id],
    );
  }

  // 刪除筆記
  Future<int> deleteNote(int id) async {
    Database db = await DatabaseHelper.instance.database;
    return await db.delete(
      DatabaseHelper.table,
      where: '${DatabaseHelper.columnId} = ?',
      whereArgs: [id],
    );
  }

  // 按類別查詢筆記
  Future<List<Note>> getNotesByCategory(String category) async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      DatabaseHelper.table,
      where: 'category = ?',
      whereArgs: [category],
    );
    return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
  }

  // 按優(yōu)先級(jí)排序查詢
  Future<List<Note>> getNotesSortedByPriority() async {
    Database db = await DatabaseHelper.instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      DatabaseHelper.table,
      orderBy: 'priority DESC',
    );
    return List.generate(maps.length, (i) => Note.fromMap(maps[i]));
  }
}


四、實(shí)例:保存用戶登錄狀態(tài)

下面實(shí)現(xiàn)一個(gè)保存用戶登錄狀態(tài)的功能,使用 shared_preferences 存儲(chǔ)登錄信息:

import 'package:shared_preferences/shared_preferences.dart';

class AuthService {
  static const _tokenKey = 'auth_token';
  static const _userIdKey = 'user_id';
  static const _usernameKey = 'username';

  // 保存登錄狀態(tài)
  Future<void> saveLoginState({
    required String token,
    required String userId,
    required String username,
  }) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_tokenKey, token);
    await prefs.setString(_userIdKey, userId);
    await prefs.setString(_usernameKey, username);
  }

  // 獲取當(dāng)前登錄用戶信息
  Future<Map<String, String>?> getCurrentUser() async {
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString(_tokenKey);
    final userId = prefs.getString(_userIdKey);
    final username = prefs.getString(_usernameKey);

    if (token != null && userId != null && username != null) {
      return {
        'token': token,
        'userId': userId,
        'username': username,
      };
    }
    return null;
  }

  // 檢查是否已登錄
  Future<bool> isLoggedIn() async {
    final user = await getCurrentUser();
    return user != null;
  }

  // 登出,清除登錄狀態(tài)
  Future<void> logout() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_tokenKey);
    await prefs.remove(_userIdKey);
    await prefs.remove(_usernameKey);
  }

  // 獲取認(rèn)證令牌
  Future<String?> getAuthToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_tokenKey);
  }
}

在應(yīng)用啟動(dòng)時(shí)檢查登錄狀態(tài):

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 檢查登錄狀態(tài)
  AuthService authService = AuthService();
  bool isLoggedIn = await authService.isLoggedIn();
  
  runApp(MyApp(isLoggedIn: isLoggedIn));
}

class MyApp extends StatelessWidget {
  final bool isLoggedIn;

  const MyApp({super.key, required this.isLoggedIn});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Local Storage Demo',
      home: isLoggedIn ? const HomeScreen() : const LoginScreen(),
    );
  }
}

登錄頁(yè)面實(shí)現(xiàn):

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  final _authService = AuthService();
  bool _isLoading = false;

  Future<void> _login(BuildContext context) async {
    setState(() {
      _isLoading = true;
    });

    // 模擬 API 登錄請(qǐng)求
    await Future.delayed(const Duration(seconds: 2));

    // 實(shí)際應(yīng)用中應(yīng)該驗(yàn)證用戶名和密碼
    if (_usernameController.text.isNotEmpty &&
        _passwordController.text.isNotEmpty) {
      // 保存登錄狀態(tài)
      await _authService.saveLoginState(
        token: 'fake_auth_token_123456',
        userId: 'user_123',
        username: _usernameController.text,
      );

      // 導(dǎo)航到主頁(yè)
      if (mounted) {
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (context) => const HomeScreen()),
        );
      }
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Please enter username and password')),
        );
      }
    }

    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(labelText: 'Username'),
            ),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            const SizedBox(height: 20),
            _isLoading
                ? const CircularProgressIndicator()
                : ElevatedButton(
                    onPressed: () => _login(context),
                    child: const Text('Login'),
                  ),
          ],
        ),
      ),
    );
  }
}

主頁(yè)實(shí)現(xiàn):

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _authService = AuthService();
  String? _username;

  @override
  void initState() {
    super.initState();
    _loadUserInfo();
  }

  Future<void> _loadUserInfo() async {
    final user = await _authService.getCurrentUser();
    setState(() {
      _username = user?['username'];
    });
  }

  Future<void> _logout(BuildContext context) async {
    await _authService.logout();
    if (mounted) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => const LoginScreen()),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () => _logout(context),
          ),
        ],
      ),
      body: Center(
        child: _username != null
            ? Text('Welcome, $_username! You are logged in.')
            : const Text('Loading user info...'),
      ),
    );
  }
}


五、實(shí)例:離線數(shù)據(jù)緩存

使用 sqflite 實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)的本地緩存,實(shí)現(xiàn)離線查看功能:

// 新聞模型類
class Article {
  final String id;
  final String title;
  final String? description;
  final String url;
  final String? urlToImage;
  final String publishedAt;
  final String? content;

  Article({
    required this.id,
    required this.title,
    this.description,
    required this.url,
    this.urlToImage,
    required this.publishedAt,
    this.content,
  });

  // 從 Map 轉(zhuǎn)換
  factory Article.fromMap(Map<String, dynamic> map) {
    return Article(
      id: map['id'],
      title: map['title'],
      description: map['description'],
      url: map['url'],
      urlToImage: map['urlToImage'],
      publishedAt: map['publishedAt'],
      content: map['content'],
    );
  }

  // 轉(zhuǎn)換為 Map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'description': description,
      'url': url,
      'urlToImage': urlToImage,
      'publishedAt': publishedAt,
      'content': content,
      'cachedAt': DateTime.now().toIso8601String(), // 緩存時(shí)間
    };
  }
}

// 新聞數(shù)據(jù)庫(kù)幫助類
class NewsDatabaseHelper {
  static const _databaseName = "NewsDatabase.db";
  static const _databaseVersion = 1;
  static const table = 'articles';

  NewsDatabaseHelper._privateConstructor();
  static final NewsDatabaseHelper instance =
      NewsDatabaseHelper._privateConstructor();

  static Database? _database;
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(
      path,
      version: _databaseVersion,
      onCreate: _onCreate,
    );
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $table (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            description TEXT,
            url TEXT NOT NULL,
            urlToImage TEXT,
            publishedAt TEXT NOT NULL,
            content TEXT,
            cachedAt TEXT NOT NULL
          )
          ''');
  }

  // 批量插入或更新文章
  Future<void> batchInsertOrUpdateArticles(List<Article> articles) async {
    Database db = await instance.database;
    Batch batch = db.batch();

    for (var article in articles) {
      batch.insert(
        table,
        article.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace, // 存在則替換
      );
    }

    await batch.commit();
  }

  // 獲取所有緩存文章
  Future<List<Article>> getCachedArticles() async {
    Database db = await instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      table,
      orderBy: 'publishedAt DESC',
    );
    return List.generate(maps.length, (i) => Article.fromMap(maps[i]));
  }

  // 獲取單篇文章
  Future<Article?> getArticleById(String id) async {
    Database db = await instance.database;
    List<Map<String, dynamic>> maps = await db.query(
      table,
      where: 'id = ?',
      whereArgs: [id],
    );
    if (maps.isNotEmpty) {
      return Article.fromMap(maps.first);
    }
    return null;
  }

  // 清除過(guò)期緩存(例如超過(guò)7天的)
  Future<void> clearExpiredCache() async {
    Database db = await instance.database;
    final sevenDaysAgo = DateTime.now()
        .subtract(const Duration(days: 7))
        .toIso8601String();
    await db.delete(table, where: 'cachedAt < ?', whereArgs: [sevenDaysAgo]);
  }

  // 清除所有緩存
  Future<void> clearAllCache() async {
    Database db = await instance.database;
    await db.delete(table);
  }
}

新聞倉(cāng)庫(kù)類,整合網(wǎng)絡(luò)請(qǐng)求和本地緩存:

import 'package:dio/dio.dart';

class NewsRepository {
  final Dio _dio = Dio();
  final NewsDatabaseHelper _dbHelper = NewsDatabaseHelper.instance;
  final String _apiKey = 'your_news_api_key';

  // 獲取頭條新聞,優(yōu)先從網(wǎng)絡(luò)獲取,失敗則使用緩存
  Future<List<Article>> getTopHeadlines({bool forceRefresh = false}) async {
    try {
      // 如果不是強(qiáng)制刷新,先檢查緩存
      if (!forceRefresh) {
        final cachedArticles = await _dbHelper.getCachedArticles();
        if (cachedArticles.isNotEmpty) {
          return cachedArticles;
        }
      }

      // 網(wǎng)絡(luò)請(qǐng)求
      Response response = await _dio.get(
        'https://newsapi.org/v2/top-headlines',
        queryParameters: {
          'country': 'us',
          'apiKey': _apiKey,
        },
      );

      // 解析數(shù)據(jù)
      List<Article> articles = (response.data['articles'] as List)
          .map((item) => Article(
        id: item['url'], // 使用 url 作為唯一標(biāo)識(shí)
        title: item['title'],
        description: item['description'],
        url: item['url'],
        urlToImage: item['urlToImage'],
        publishedAt: item['publishedAt'],
        content: item['content'],
      ))
          .toList();

      // 緩存到本地?cái)?shù)據(jù)庫(kù)
      await _dbHelper.batchInsertOrUpdateArticles(articles);

      // 清除過(guò)期緩存
      await _dbHelper.clearExpiredCache();

      return articles;
    } catch (e) {
      // 網(wǎng)絡(luò)請(qǐng)求失敗,嘗試返回緩存
      final cachedArticles = await _dbHelper.getCachedArticles();
      if (cachedArticles.isNotEmpty) {
        return cachedArticles;
      }
      // 沒(méi)有緩存,拋出錯(cuò)誤
      throw Exception('Failed to fetch news and no cache available');
    }
  }
}


六、其他存儲(chǔ)方案簡(jiǎn)介

1. flutter_secure_storage

用于存儲(chǔ)敏感信息,如令牌、密碼等,數(shù)據(jù)會(huì)被加密存儲(chǔ):

dependencies:
  flutter_secure_storage: ^9.2.4

使用示例:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

// 存儲(chǔ)敏感數(shù)據(jù)
await storage.write(key: 'auth_token', value: 'sensitive_token_123');

// 讀取數(shù)據(jù)
String? token = await storage.read(key: 'auth_token');

// 刪除數(shù)據(jù)
await storage.delete(key: 'auth_token');

2. Hive

Hive 是一個(gè)高性能、輕量級(jí)的 NoSQL 數(shù)據(jù)庫(kù),純 Dart 實(shí)現(xiàn),速度快且易于使用:

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

使用示例:

// 初始化
await Hive.initFlutter();
Hive.registerAdapter(NoteAdapter()); // 注冊(cè)適配器
await Hive.openBox<Note>('notes');

// 獲取盒子
var box = Hive.box<Note>('notes');

// 添加數(shù)據(jù)
var note = Note(id: 1, title: 'Hive Demo', content: 'Hello Hive');
await box.put(note.id, note);

// 獲取數(shù)據(jù)
Note? savedNote = box.get(1);

// 查詢所有數(shù)據(jù)
List<Note> allNotes = box.values.toList();

// 更新數(shù)據(jù)
note.title = 'Updated Title';
await box.put(note.id, note);

// 刪除數(shù)據(jù)
await box.delete(1);


七、本地存儲(chǔ)最佳實(shí)踐

  1. 選擇合適的存儲(chǔ)方案
    • 簡(jiǎn)單鍵值對(duì)數(shù)據(jù)使用 shared_preferences
    • 敏感信息使用 flutter_secure_storage
    • 結(jié)構(gòu)化數(shù)據(jù)使用 sqflitehive
  2. 數(shù)據(jù)分層管理
    • 封裝存儲(chǔ)操作,與業(yè)務(wù)邏輯分離
    • 使用倉(cāng)庫(kù)模式(Repository Pattern)整合網(wǎng)絡(luò)和本地存儲(chǔ)
  3. 性能優(yōu)化
    • 批量操作數(shù)據(jù)庫(kù),減少 IO 次數(shù)
    • 及時(shí)清理過(guò)期緩存,避免存儲(chǔ)空間過(guò)大
    • 數(shù)據(jù)庫(kù)操作放在后臺(tái)線程,避免阻塞 UI
  4. 錯(cuò)誤處理
    • 對(duì)所有存儲(chǔ)操作添加異常捕獲
    • 網(wǎng)絡(luò)請(qǐng)求失敗時(shí),提供使用緩存數(shù)據(jù)的降級(jí)策略
  5. 數(shù)據(jù)遷移
    • 規(guī)劃好數(shù)據(jù)庫(kù)版本,做好升級(jí)遷移方案
    • 重大更新時(shí)考慮數(shù)據(jù)備份和恢復(fù)機(jī)制
  6. 安全考慮
    • 敏感數(shù)據(jù)必須加密存儲(chǔ)
    • 避免存儲(chǔ)不必要的用戶隱私數(shù)據(jù)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容