MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter SharedPreferences与SQLite的综合应用:复杂场景下的数据管理

2023-09-161.7k 阅读

Flutter 数据管理概述

在 Flutter 应用开发中,数据管理是一个至关重要的环节。随着应用功能的不断丰富和场景的日益复杂,选择合适的数据存储和管理方式显得尤为关键。Flutter 提供了多种数据管理方案,其中 SharedPreferences 和 SQLite 是两种常见且功能强大的工具,它们在不同的数据管理场景中发挥着重要作用。

SharedPreferences 是一种轻量级的键值对存储方式,适用于存储简单的数据,如用户配置、应用设置等。它的优点在于使用方便、易于上手,并且在 Flutter 应用中可以快速地进行数据的读取和写入操作。例如,我们可以使用它来存储用户的登录状态、主题设置等少量的数据。

而 SQLite 是一款轻型的关系型数据库,具有强大的数据存储和查询功能。它适用于需要存储大量结构化数据,并且需要进行复杂查询和事务处理的场景。比如在一个电商应用中,需要存储商品信息、用户订单等大量数据,并进行数据的增删改查操作,SQLite 就是一个很好的选择。

在实际的复杂应用场景中,往往单一的数据管理方式无法满足所有需求,需要将 SharedPreferences 和 SQLite 结合使用,发挥各自的优势,以实现高效、灵活的数据管理。

SharedPreferences 基础使用

引入依赖

在使用 SharedPreferences 之前,首先需要在 pubspec.yaml 文件中引入依赖:

dependencies:
  shared_preferences: ^2.0.15

然后运行 flutter pub get 命令来获取依赖。

写入数据

下面是向 SharedPreferences 写入数据的示例代码:

import 'package:shared_preferences/shared_preferences.dart';

Future<void> saveData() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  // 存储布尔值
  await prefs.setBool('isLoggedIn', true);
  // 存储字符串
  await prefs.setString('username', 'JohnDoe');
  // 存储整数
  await prefs.setInt('age', 30);
  // 存储双精度浮点数
  await prefs.setDouble('height', 175.5);
  // 存储字符串列表
  await prefs.setStringList('hobbies', ['reading', 'running']);
}

在上述代码中,首先通过 SharedPreferences.getInstance() 获取 SharedPreferences 实例,然后使用不同的 set 方法来存储不同类型的数据。

读取数据

读取数据的代码如下:

import 'package:shared_preferences/shared_preferences.dart';

Future<void> readData() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  // 读取布尔值
  bool? isLoggedIn = prefs.getBool('isLoggedIn');
  // 读取字符串
  String? username = prefs.getString('username');
  // 读取整数
  int? age = prefs.getInt('age');
  // 读取双精度浮点数
  double? height = prefs.getDouble('height');
  // 读取字符串列表
  List<String>? hobbies = prefs.getStringList('hobbies');

  print('isLoggedIn: $isLoggedIn');
  print('username: $username');
  print('age: $age');
  print('height: $height');
  print('hobbies: $hobbies');
}

这里通过 get 方法根据键来获取相应的数据,如果键不存在,则返回 null

SQLite 基础使用

引入依赖

同样,在使用 SQLite 之前,需要在 pubspec.yaml 文件中引入依赖:

dependencies:
  sqflite: ^2.2.8
  path: ^1.8.3

运行 flutter pub get 获取依赖。

打开数据库

以下是打开 SQLite 数据库的代码:

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

Future<Database> openDatabaseConnection() async {
  String databasesPath = await getDatabasesPath();
  String path = join(databasesPath, 'example.db');

  return openDatabase(
    path,
    version: 1,
    onCreate: (Database db, int version) async {
      // 创建表
      await db.execute('''
        CREATE TABLE users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          age INTEGER
        )
      ''');
    },
  );
}

在上述代码中,首先获取应用的数据库路径,然后使用 openDatabase 方法打开数据库。如果数据库不存在,onCreate 回调函数会被触发,在这里可以创建数据库表。

插入数据

插入数据到 users 表的代码如下:

Future<void> insertUser(Database db, String name, int age) async {
  await db.insert(
    'users',
    {
      'name': name,
      'age': age,
    },
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

这里使用 insert 方法将数据插入到 users 表中,conflictAlgorithm 参数指定了在插入数据时遇到冲突的处理方式,这里选择了 replace,即替换已有的数据。

查询数据

查询 users 表中所有数据的代码如下:

Future<List<Map<String, dynamic>>> queryAllUsers(Database db) async {
  return await db.query('users');
}

query 方法会返回一个 List<Map<String, dynamic>>,其中每个 Map 代表一行数据,键是列名,值是对应的数据。

更新数据

更新 users 表中数据的代码如下:

Future<void> updateUser(Database db, int id, String name, int age) async {
  await db.update(
    'users',
    {
      'name': name,
      'age': age,
    },
    where: 'id =?',
    whereArgs: [id],
  );
}

这里通过 update 方法根据 id 更新指定行的数据,where 子句指定了更新的条件,whereArgs 用于传递条件参数。

删除数据

删除 users 表中数据的代码如下:

Future<void> deleteUser(Database db, int id) async {
  await db.delete(
    'users',
    where: 'id =?',
    whereArgs: [id],
  );
}

delete 方法根据 id 删除指定行的数据。

复杂场景下的结合应用

场景分析

假设我们正在开发一个笔记应用,用户可以创建、编辑和删除笔记。对于这样一个应用,我们可以使用 SQLite 来存储笔记的详细内容,如标题、正文、创建时间等,因为这些数据量相对较大且需要进行结构化管理。而对于一些用户设置,如是否开启夜间模式、最近一次打开的笔记分类等简单数据,我们可以使用 SharedPreferences 来存储。

结合使用示例

首先,定义 SQLite 数据库相关操作:

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

class NoteDatabase {
  static final NoteDatabase instance = NoteDatabase._init();
  static Database? _database;

  NoteDatabase._init();

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDB('notes.db');
    return _database!;
  }

  Future<Database> _initDB(String filePath) async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, filePath);

    return openDatabase(
      path,
      version: 1,
      onCreate: (Database db, int version) async {
        await db.execute('''
          CREATE TABLE notes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            createdAt TEXT NOT NULL
          )
        ''');
      },
    );
  }

  Future<int> createNote(Map<String, dynamic> note) async {
    Database db = await instance.database;
    return await db.insert('notes', note);
  }

  Future<List<Map<String, dynamic>>> readAllNotes() async {
    Database db = await instance.database;
    return await db.query('notes');
  }

  Future<int> updateNote(Map<String, dynamic> note) async {
    Database db = await instance.database;
    return await db.update(
      'notes',
      note,
      where: 'id =?',
      whereArgs: [note['id']],
    );
  }

  Future<int> deleteNote(int id) async {
    Database db = await instance.database;
    return await db.delete(
      'notes',
      where: 'id =?',
      whereArgs: [id],
    );
  }
}

上述代码定义了一个 NoteDatabase 类,封装了 SQLite 数据库的打开、创建表、插入、查询、更新和删除笔记的操作。

然后,定义 SharedPreferences 相关操作:

import 'package:shared_preferences/shared_preferences.dart';

class AppSettings {
  static const String _keyNightMode = 'isNightMode';
  static const String _keyLastOpenedCategory = 'lastOpenedCategory';

  static Future<void> setNightMode(bool isNightMode) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_keyNightMode, isNightMode);
  }

  static Future<bool?> getNightMode() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_keyNightMode);
  }

  static Future<void> setLastOpenedCategory(String category) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyLastOpenedCategory, category);
  }

  static Future<String?> getLastOpenedCategory() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyLastOpenedCategory);
  }
}

这里定义了 AppSettings 类,用于管理应用的设置,如夜间模式和最近一次打开的笔记分类。

在实际的页面中,可以这样结合使用:

import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';

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

  @override
  State<NoteApp> createState() => _NoteAppState();
}

class _NoteAppState extends State<NoteApp> {
  bool _isNightMode = false;
  String _lastOpenedCategory = 'All Notes';

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

  Future<void> _loadSettings() async {
    bool? isNightMode = await AppSettings.getNightMode();
    String? lastOpenedCategory = await AppSettings.getLastOpenedCategory();
    if (isNightMode != null) {
      setState(() {
        _isNightMode = isNightMode;
      });
    }
    if (lastOpenedCategory != null) {
      setState(() {
        _lastOpenedCategory = lastOpenedCategory;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: _isNightMode? ThemeData.dark() : ThemeData.light(),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Note App - $_lastOpenedCategory'),
          actions: [
            Switch(
              value: _isNightMode,
              onChanged: (value) async {
                setState(() {
                  _isNightMode = value;
                });
                await AppSettings.setNightMode(value);
              },
            ),
          ],
        ),
        body: // 这里添加笔记列表和操作相关的 UI
      ),
    );
  }
}

在上述代码中,在 initState 方法中加载应用设置,根据 SharedPreferences 中存储的夜间模式状态来设置应用主题,并且可以在 UI 中通过开关来更新夜间模式设置并保存到 SharedPreferences 中。同时,根据 SharedPreferences 中存储的最近一次打开的笔记分类来设置页面标题。而对于笔记的具体数据管理,则由 NoteDatabase 类负责。

事务处理与数据一致性

在使用 SQLite 进行数据操作时,事务处理是保证数据一致性的重要手段。例如,在一个涉及多个表的复杂操作中,可能需要插入数据到多个相关表中。如果其中一个插入操作失败,而没有事务处理,那么可能会导致数据不一致的问题。

以下是一个使用事务处理的示例,假设我们有两个表 ordersorder_items,在创建订单时需要同时插入订单信息和订单商品信息:

Future<void> createOrderAndItems(
  Database db,
  String orderNumber,
  List<Map<String, dynamic>> items,
) async {
  await db.transaction((txn) async {
    int orderId = await txn.insert(
      'orders',
      {'order_number': orderNumber},
    );

    for (Map<String, dynamic> item in items) {
      item['order_id'] = orderId;
      await txn.insert(
        'order_items',
        item,
      );
    }
  });
}

在上述代码中,通过 db.transaction 方法开启一个事务,在事务内部执行多个插入操作。如果任何一个插入操作失败,整个事务会回滚,从而保证数据的一致性。

性能优化与缓存策略

在复杂场景下,性能优化是不可忽视的。对于 SQLite 数据库,合理的索引设计可以大大提高查询性能。例如,如果经常根据笔记的标题来查询笔记,可以在 notes 表的 title 列上创建索引:

Future<void> createTitleIndex(Database db) async {
  await db.execute('CREATE INDEX idx_title ON notes (title)');
}

对于 SharedPreferences,由于它是基于文件系统的存储,频繁的读写操作可能会影响性能。因此,可以考虑在内存中缓存一些经常使用的数据,减少对 SharedPreferences 的直接读写次数。例如,在应用启动时将一些关键的设置数据读取到内存中,并在需要时优先从内存中获取:

class AppSettingsCache {
  static bool? _isNightMode;
  static String? _lastOpenedCategory;

  static Future<bool?> getNightMode() async {
    if (_isNightMode != null) return _isNightMode;
    bool? value = await AppSettings.getNightMode();
    _isNightMode = value;
    return value;
  }

  static Future<String?> getLastOpenedCategory() async {
    if (_lastOpenedCategory != null) return _lastOpenedCategory;
    String? value = await AppSettings.getLastOpenedCategory();
    _lastOpenedCategory = value;
    return value;
  }
}

通过这种方式,可以在一定程度上提高应用的数据读取性能。

数据迁移与版本管理

随着应用的发展,数据库结构和 SharedPreferences 中的数据可能需要进行迁移和版本管理。对于 SQLite 数据库,可以通过在 openDatabase 方法的 version 参数和 onUpgrade 回调函数来实现数据库版本的管理。例如,当数据库版本从 1 升级到 2 时,需要添加一个新的表 tags

Future<Database> openDatabaseConnection() async {
  String databasesPath = await getDatabasesPath();
  String path = join(databasesPath, 'example.db');

  return openDatabase(
    path,
    version: 2,
    onCreate: (Database db, int version) async {
      await db.execute('''
        CREATE TABLE users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          age INTEGER
        )
      ''');
    },
    onUpgrade: (Database db, int oldVersion, int newVersion) async {
      if (oldVersion == 1 && newVersion == 2) {
        await db.execute('''
          CREATE TABLE tags (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            note_id INTEGER,
            FOREIGN KEY (note_id) REFERENCES notes(id)
          )
        ''');
      }
    },
  );
}

对于 SharedPreferences 中的数据迁移,可以在应用启动时检查是否需要进行数据迁移操作。例如,当应用的某个功能更新后,SharedPreferences 中存储的设置数据格式发生了变化,需要进行转换:

Future<void> migrateSharedPreferencesData() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  bool? oldSetting = prefs.getBool('oldSettingKey');
  if (oldSetting != null) {
    // 进行数据迁移操作,例如将旧设置转换为新设置
    bool newSetting = oldSetting? true : false;
    await prefs.setBool('newSettingKey', newSetting);
    await prefs.remove('oldSettingKey');
  }
}

通过合理的数据库版本管理和 SharedPreferences 数据迁移,可以保证应用在不同版本间的数据兼容性和稳定性。

安全考虑

在数据管理过程中,安全是一个重要的方面。对于 SQLite 数据库,应该对敏感数据进行加密存储。可以使用第三方库如 encrypt 来对数据进行加密和解密。例如,在插入用户密码到数据库之前进行加密:

import 'package:encrypt/encrypt.dart';

final key = Key.fromLength(32);
final iv = IV.fromLength(16);
final encrypter = Encrypter(AES(key));

Future<void> insertUserWithEncryptedPassword(Database db, String name, String password) async {
  String encryptedPassword = encrypter.encrypt(password, iv: iv).base64;
  await db.insert(
    'users',
    {
      'name': name,
      'encrypted_password': encryptedPassword,
    },
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

在查询用户信息时,再对加密的密码进行解密。

对于 SharedPreferences,虽然它存储的数据相对简单,但也应该避免存储过于敏感的数据。如果必须存储,同样可以进行加密处理。同时,要注意应用的权限管理,确保只有授权的组件能够访问和修改这些数据。

与其他 Flutter 功能的集成

与 Provider 的集成

在 Flutter 应用中,provider 是一个常用的状态管理库。可以将 SharedPreferences 和 SQLite 的数据管理功能与 provider 集成,以便在整个应用中方便地共享数据。例如,创建一个 NoteProvider 类,用于管理笔记数据:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class NoteProvider with ChangeNotifier {
  List<Map<String, dynamic>> _notes = [];

  List<Map<String, dynamic>> get notes => _notes;

  Future<void> loadNotes() async {
    List<Map<String, dynamic>> notes = await NoteDatabase.instance.readAllNotes();
    setState(() {
      _notes = notes;
    });
  }

  Future<void> addNote(Map<String, dynamic> note) async {
    int id = await NoteDatabase.instance.createNote(note);
    note['id'] = id;
    setState(() {
      _notes.add(note);
    });
  }

  Future<void> updateNote(Map<String, dynamic> note) async {
    await NoteDatabase.instance.updateNote(note);
    setState(() {
      int index = _notes.indexWhere((element) => element['id'] == note['id']);
      if (index != -1) {
        _notes[index] = note;
      }
    });
  }

  Future<void> deleteNote(int id) async {
    await NoteDatabase.instance.deleteNote(id);
    setState(() {
      _notes.removeWhere((element) => element['id'] == id);
    });
  }

  void setState(VoidCallback fn) {
    fn();
    notifyListeners();
  }
}

在应用的 main 函数中,可以将 NoteProvider 提供给整个应用:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => NoteProvider(),
      child: const NoteApp(),
    ),
  );
}

这样,在应用的各个组件中,都可以通过 Provider.of<NoteProvider>(context) 来获取和操作笔记数据。

与 Flutter 导航的集成

在 Flutter 应用中,导航是常见的功能。可以将 SharedPreferences 中存储的导航相关信息,如上次打开的页面索引,与 Flutter 的导航功能结合使用。例如,在应用启动时,根据 SharedPreferences 中存储的页面索引,直接导航到相应的页面:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FutureBuilder<int?>(
        future: AppSettings.getLastOpenedPageIndex(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            int index = snapshot.data!;
            if (index == 0) {
              return HomePage();
            } else if (index == 1) {
              return SettingsPage();
            }
          }
          return HomePage();
        },
      ),
    );
  }
}

同时,在页面切换时,可以更新 SharedPreferences 中存储的页面索引:

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await AppSettings.setLastOpenedPageIndex(1);
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SettingsPage()),
            );
          },
          child: const Text('Go to Settings'),
        ),
      ),
    );
  }
}

通过这种方式,可以实现应用导航状态的持久化,提供更好的用户体验。

总结

在 Flutter 应用开发的复杂场景下,将 SharedPreferences 和 SQLite 综合应用能够充分发挥两者的优势,实现高效、灵活且安全的数据管理。通过合理的代码设计、事务处理、性能优化、数据迁移以及与其他 Flutter 功能的集成,可以构建出功能强大、用户体验良好的应用。开发者在实际应用中,应根据具体的业务需求和数据特点,灵活选择和运用这两种数据管理方式,以满足应用不断发展的需求。同时,要始终关注数据安全和性能问题,确保应用的稳定性和可靠性。在未来的 Flutter 开发中,随着应用场景的不断拓展和用户需求的日益复杂,对数据管理的要求也会越来越高,熟练掌握并灵活运用 SharedPreferences 和 SQLite 的综合应用技巧将成为开发者必备的能力之一。