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

构建 Flutter 应用的数据存储层:SharedPreferences 与 SQLite 的结合

2024-03-155.5k 阅读

1. 引言:Flutter 应用的数据存储需求

在开发 Flutter 应用时,数据存储是一个至关重要的环节。无论是保存用户设置、缓存少量数据,还是存储大量结构化数据,选择合适的数据存储方案都能显著提升应用的性能和用户体验。Flutter 提供了多种数据存储选项,其中 SharedPreferences 和 SQLite 是两种常用的技术,将它们结合使用可以满足不同场景下的数据存储需求。

2. SharedPreferences 基础

2.1 什么是 SharedPreferences

SharedPreferences 是一种轻量级的键值对存储机制,用于在应用程序中保存简单的配置数据和少量的用户设置。它非常适合存储诸如用户的登录状态、应用主题设置、最近一次打开应用的时间等数据。在 Android 原生开发中广泛使用,Flutter 通过 shared_preferences 插件提供了对它的支持。

2.2 安装和导入 shared_preferences 插件

pubspec.yaml 文件中添加依赖:

dependencies:
  shared_preferences: ^2.0.15

然后运行 flutter pub get 命令来获取插件。在 Dart 代码中导入该库:

import 'package:shared_preferences/shared_preferences.dart';

2.3 使用 SharedPreferences 存储数据

以下是存储不同类型数据的示例:

  • 存储布尔值
Future<void> saveLoginStatus(bool isLoggedIn) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isLoggedIn', isLoggedIn);
}
  • 存储整数
Future<void> saveScore(int score) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setInt('score', score);
}
  • 存储字符串
Future<void> saveUserName(String name) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('userName', name);
}

2.4 从 SharedPreferences 读取数据

读取数据时,需要提供一个默认值,以防该键对应的数据不存在。

  • 读取布尔值
Future<bool> getLoginStatus() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool('isLoggedIn')?? false;
}
  • 读取整数
Future<int> getScore() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getInt('score')?? 0;
}
  • 读取字符串
Future<String?> getUserName() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('userName');
}

3. SQLite 基础

3.1 什么是 SQLite

SQLite 是一个轻量级的嵌入式数据库引擎,它不需要独立的服务器进程,直接读写普通磁盘文件。SQLite 支持标准的 SQL 查询语言,适用于存储大量结构化数据,如应用中的用户数据、消息记录、本地缓存的列表数据等。在 Flutter 中,我们可以使用 sqflite 插件来操作 SQLite 数据库。

3.2 安装和导入 sqflite 插件

pubspec.yaml 文件中添加依赖:

dependencies:
  sqflite: ^2.2.0

运行 flutter pub get 后,在 Dart 代码中导入:

import 'package:sqflite/sqflite.dart';

3.3 创建和打开 SQLite 数据库

Future<Database> openDatabase() async {
  final databasesPath = await getDatabasesPath();
  final path = join(databasesPath, 'my_database.db');

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

上述代码中,getDatabasesPath 获取应用数据库的存储路径,join 方法拼接数据库文件名。openDatabase 方法打开或创建数据库,如果数据库不存在则会调用 onCreate 回调创建表。

3.4 插入数据到 SQLite

Future<void> insertUser(User user) async {
  final db = await openDatabase();
  await db.insert(
    'users',
    user.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

这里假设 User 类有 toMap 方法将对象转换为适合插入数据库的映射。conflictAlgorithm 定义了插入冲突时的处理方式,这里选择 replace,即如果有冲突则替换原有数据。

3.5 从 SQLite 查询数据

Future<List<User>> queryAllUsers() async {
  final db = await openDatabase();
  final List<Map<String, dynamic>> maps = await db.query('users');
  return List.generate(maps.length, (i) {
    return User(
      id: maps[i]['id'],
      name: maps[i]['name'],
      age: maps[i]['age'],
    );
  });
}

query 方法执行 SQL 查询,返回一个包含查询结果的 List<Map<String, dynamic>>,然后通过 List.generate 将其转换为 User 对象列表。

4. 结合 SharedPreferences 和 SQLite

4.1 何时结合使用

当应用需要存储一些简单配置数据(如用户设置),同时又有大量结构化数据(如用户生成的文档、聊天记录等)时,结合使用 SharedPreferences 和 SQLite 是一个很好的选择。例如,我们可以使用 SharedPreferences 存储用户当前选择的主题模式,而使用 SQLite 存储用户的详细笔记内容。

4.2 示例场景:用户登录状态与用户数据存储

  1. 使用 SharedPreferences 存储登录状态: 在用户登录成功后,我们将登录状态存储在 SharedPreferences 中。
Future<void> saveLoginStatus(bool isLoggedIn) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isLoggedIn', isLoggedIn);
}

在应用启动时,我们可以检查登录状态:

Future<bool> getLoginStatus() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool('isLoggedIn')?? false;
}
  1. 使用 SQLite 存储用户详细信息: 假设我们有一个 User 类:
class User {
  final int? id;
  final String name;
  final int age;

  User({this.id, required this.name, required this.age});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }
}

创建和打开数据库:

Future<Database> openDatabase() async {
  final databasesPath = await getDatabasesPath();
  final path = join(databasesPath, 'user_database.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,
          age INTEGER
        )
      ''');
    },
  );
}

插入用户数据:

Future<void> insertUser(User user) async {
  final db = await openDatabase();
  await db.insert(
    'users',
    user.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

查询用户数据:

Future<List<User>> queryAllUsers() async {
  final db = await openDatabase();
  final List<Map<String, dynamic>> maps = await db.query('users');
  return List.generate(maps.length, (i) {
    return User(
      id: maps[i]['id'],
      name: maps[i]['name'],
      age: maps[i]['age'],
    );
  });
}

4.3 数据同步与更新策略

在某些情况下,我们可能需要在 SQLite 和 SharedPreferences 之间进行数据同步。例如,当用户在应用中修改了自己的昵称,我们既要更新 SQLite 中的用户表,也要更新 SharedPreferences 中用于显示昵称的缓存。

  1. 更新 SQLite 数据
Future<void> updateUserName(int id, String newName) async {
  final db = await openDatabase();
  await db.update(
    'users',
    {'name': newName},
    where: 'id =?',
    whereArgs: [id],
  );
}
  1. 更新 SharedPreferences 数据
Future<void> updateCachedUserName(String newName) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('cachedUserName', newName);
}

5. 性能优化与注意事项

5.1 SharedPreferences 性能优化

  1. 批量操作:尽量避免多次调用 SharedPreferences 的单个操作方法,而是将多个操作合并为一次。例如,如果需要存储多个键值对,可以使用 SharedPreferencessetValues 方法(在 shared_preferences 插件中有相关支持)。
  2. 减少频繁读写:由于 SharedPreferences 最终是将数据写入磁盘文件,频繁读写会影响性能。尽量在需要时一次性读取所需数据,而不是在多个地方多次读取相同的数据。

5.2 SQLite 性能优化

  1. 事务处理:对于涉及多个数据库操作的场景,如插入多条记录或更新多个表,使用事务可以提高性能并确保数据的一致性。
Future<void> insertMultipleUsers(List<User> users) async {
  final db = await openDatabase();
  await db.transaction((txn) async {
    for (final user in users) {
      await txn.insert(
        'users',
        user.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace,
      );
    }
  });
}
  1. 索引优化:为经常查询的列创建索引可以显著提高查询性能。例如,如果经常根据用户名查询用户,可在 name 列上创建索引:
await db.execute('CREATE INDEX idx_user_name ON users (name)');

5.3 注意事项

  1. 数据安全:无论是 SharedPreferences 还是 SQLite,存储的数据都是以明文形式存在设备上。对于敏感数据,如用户密码等,需要进行加密处理后再存储。
  2. 数据库版本管理:在 SQLite 中,当数据库结构发生变化时,需要正确管理数据库版本。在 openDatabase 方法中通过 version 参数和 onUpgrade 回调来处理数据库升级操作。
Future<Database> openDatabase() async {
  final databasesPath = await getDatabasesPath();
  final path = join(databasesPath, 'user_database.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,
          age INTEGER
        )
      ''');
    },
    onUpgrade: (Database db, int oldVersion, int newVersion) async {
      if (oldVersion == 1 && newVersion == 2) {
        await db.execute('ALTER TABLE users ADD COLUMN email TEXT');
      }
    },
  );
}

6. 总结结合使用的优势

结合 SharedPreferences 和 SQLite 为 Flutter 应用的数据存储提供了一个灵活且高效的解决方案。SharedPreferences 适用于快速存储和读取少量简单数据,而 SQLite 则能处理大量结构化数据的持久化需求。通过合理使用这两种技术,我们可以优化应用的性能、提升用户体验,并确保数据的安全性和一致性。在实际开发中,根据应用的具体需求和数据特点,灵活运用它们的优势,能够打造出健壮且高性能的数据存储层。

7. 实际应用案例分析

7.1 一个笔记应用的场景

假设我们正在开发一个简单的笔记应用。用户可以创建、编辑和删除笔记,同时应用有一些用户设置,如笔记的默认字体大小、是否开启夜间模式等。

  1. 使用 SharedPreferences 存储用户设置: 存储默认字体大小:
Future<void> saveDefaultFontSize(double fontSize) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setDouble('defaultFontSize', fontSize);
}

读取默认字体大小:

Future<double> getDefaultFontSize() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getDouble('defaultFontSize')?? 16.0;
}

存储夜间模式状态:

Future<void> saveNightModeStatus(bool isNightMode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('isNightMode', isNightMode);
}

读取夜间模式状态:

Future<bool> getNightModeStatus() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool('isNightMode')?? false;
}
  1. 使用 SQLite 存储笔记数据: 定义 Note 类:
class Note {
  final int? id;
  final String title;
  final String content;
  final DateTime createdAt;

  Note({this.id, required this.title, required this.content, required this.createdAt});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'createdAt': createdAt.millisecondsSinceEpoch,
    };
  }
}

创建和打开数据库:

Future<Database> openDatabase() async {
  final databasesPath = await getDatabasesPath();
  final path = join(databasesPath, 'notes_database.db');

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

插入笔记:

Future<void> insertNote(Note note) async {
  final db = await openDatabase();
  await db.insert(
    'notes',
    note.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

查询所有笔记:

Future<List<Note>> queryAllNotes() async {
  final db = await openDatabase();
  final List<Map<String, dynamic>> maps = await db.query('notes');
  return List.generate(maps.length, (i) {
    return Note(
      id: maps[i]['id'],
      title: maps[i]['title'],
      content: maps[i]['content'],
      createdAt: DateTime.fromMillisecondsSinceEpoch(maps[i]['createdAt']),
    );
  });
}

通过这种方式,我们将用户设置和笔记数据分开存储,充分发挥了 SharedPreferences 和 SQLite 的各自优势,提升了应用的整体性能和用户体验。

7.2 电商应用中的购物车与用户设置

在电商应用中,用户设置(如地区偏好、语言设置)可以使用 SharedPreferences 存储。而购物车中的商品列表,由于涉及到商品的详细信息、数量、价格等结构化数据,适合使用 SQLite 存储。

  1. SharedPreferences 存储用户设置: 存储地区偏好:
Future<void> saveRegionPreference(String region) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('regionPreference', region);
}

读取地区偏好:

Future<String?> getRegionPreference() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('regionPreference');
}

存储语言设置:

Future<void> saveLanguageSetting(String language) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('languageSetting', language);
}

读取语言设置:

Future<String?> getLanguageSetting() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('languageSetting');
}
  1. SQLite 存储购物车数据: 定义 CartItem 类:
class CartItem {
  final int? id;
  final String productId;
  final String productName;
  final double price;
  final int quantity;

  CartItem({this.id, required this.productId, required this.productName, required this.price, required this.quantity});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'productId': productId,
      'productName': productName,
      'price': price,
      'quantity': quantity,
    };
  }
}

创建和打开数据库:

Future<Database> openDatabase() async {
  final databasesPath = await getDatabasesPath();
  final path = join(databasesPath, 'cart_database.db');

  return openDatabase(
    path,
    version: 1,
    onCreate: (Database db, int version) async {
      await db.execute('''
        CREATE TABLE cart_items (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          productId TEXT,
          productName TEXT,
          price REAL,
          quantity INTEGER
        )
      ''');
    },
  );
}

插入购物车商品:

Future<void> insertCartItem(CartItem item) async {
  final db = await openDatabase();
  await db.insert(
    'cart_items',
    item.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

查询购物车所有商品:

Future<List<CartItem>> queryAllCartItems() async {
  final db = await openDatabase();
  final List<Map<String, dynamic>> maps = await db.query('cart_items');
  return List.generate(maps.length, (i) {
    return CartItem(
      id: maps[i]['id'],
      productId: maps[i]['productId'],
      productName: maps[i]['productName'],
      price: maps[i]['price'],
      quantity: maps[i]['quantity'],
    );
  });
}

这样的设计使得电商应用能够高效地管理用户设置和购物车数据,为用户提供流畅的购物体验。

8. 未来发展与替代方案探讨

8.1 基于云存储的方案

随着云计算的发展,一些云存储服务,如 Firebase Firestore、AWS DynamoDB 等,为移动应用提供了强大的后端数据存储支持。这些云存储方案具有自动扩展、实时同步等优点。例如,Firebase Firestore 可以与 Flutter 应用无缝集成,实现实时数据更新和离线数据存储。

  1. Firebase Firestore 集成: 在 Flutter 项目中添加 Firebase 依赖:
dependencies:
  firebase_core: ^2.10.0
  cloud_firestore: ^4.7.0

初始化 Firebase:

import 'package:firebase_core/firebase_core.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

使用 Firestore 存储数据:

import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> addUserToFirestore(User user) async {
  final firestore = FirebaseFirestore.instance;
  await firestore.collection('users').add({
    'name': user.name,
    'age': user.age,
  });
}

读取数据:

Future<List<User>> getUsersFromFirestore() async {
  final firestore = FirebaseFirestore.instance;
  final snapshot = await firestore.collection('users').get();
  return snapshot.docs.map((doc) {
    return User(
      name: doc.get('name'),
      age: doc.get('age'),
    );
  }).toList();
}

然而,云存储方案通常需要依赖网络连接,对于一些对离线使用要求较高的应用,可能需要结合本地存储(如 SharedPreferences 和 SQLite)来提供更好的用户体验。

8.2 新兴的本地存储技术

除了 SharedPreferences 和 SQLite,还有一些新兴的本地存储技术值得关注。例如,Flutter 中的 Hive 是一个轻量级、高性能的键值对数据库,它支持对象序列化,并且性能比 SharedPreferences 更好。

  1. Hive 基础使用: 添加 Hive 依赖:
dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

初始化 Hive:

import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';

Future<void> main() async {
  await Hive.initFlutter();
  runApp(MyApp());
}

使用 Hive 存储数据:

Future<void> saveUserToHive(User user) async {
  final box = await Hive.openBox<User>('usersBox');
  await box.add(user);
}

读取数据:

Future<List<User>> getUsersFromHive() async {
  final box = await Hive.openBox<User>('usersBox');
  return box.values.toList();
}

Hive 在一些场景下可以替代 SharedPreferences 或 SQLite,特别是对于需要高性能键值对存储且支持对象存储的应用。但在选择存储方案时,需要综合考虑应用的具体需求、数据量、性能要求以及开发成本等因素。

9. 总结

在 Flutter 应用开发中,SharedPreferences 和 SQLite 的结合为数据存储提供了一种可靠且灵活的解决方案。通过深入理解它们的特性、使用方法以及性能优化技巧,开发者能够根据应用的实际需求打造出高效的数据存储层。同时,关注新兴的存储技术和云存储方案,能够使应用在不同场景下提供更好的用户体验。在实际项目中,应根据具体情况权衡各种存储方案的优缺点,选择最适合的方案来满足应用的数据存储需求。无论是小型应用的简单配置存储,还是大型应用的复杂数据管理,合理运用这些技术都能为应用的成功奠定坚实的基础。

10. 常见问题解答

10.1 SharedPreferences 数据丢失问题

有时可能会遇到 SharedPreferences 数据丢失的情况,这可能是由于应用卸载、设备存储空间不足、应用更新过程中的异常等原因导致。为了尽量避免数据丢失,可以定期备份 SharedPreferences 数据到其他存储介质(如 SQLite 或云存储)。另外,在应用更新时,要确保正确处理 SharedPreferences 的数据迁移。

10.2 SQLite 数据库锁问题

SQLite 是单线程写入的,在高并发写入场景下可能会出现数据库锁问题。可以通过以下方法解决:

  1. 使用事务:将多个写入操作放在一个事务中,减少锁的竞争时间。
  2. 合理安排读写操作顺序:尽量避免在同一时间进行大量的读写操作,可将读操作安排在写操作相对较少的时间段。
  3. 优化数据库结构:合理设计表结构和索引,减少查询和写入的时间,从而降低锁的持有时间。

10.3 跨平台数据存储兼容性问题

虽然 Flutter 旨在实现跨平台开发,但不同平台(如 Android 和 iOS)对于数据存储的底层实现和限制可能略有不同。在使用 SharedPreferences 和 SQLite 时,要注意以下几点:

  1. 路径差异:在获取数据库路径或 SharedPreferences 存储路径时,不同平台可能有不同的方式。Flutter 的插件通常会处理这些差异,但在某些特殊情况下可能需要手动处理。
  2. 权限问题:不同平台对于应用访问文件系统和存储数据的权限要求不同。确保在 AndroidManifest.xml 和 Info.plist 文件中正确配置权限,以保证应用在各个平台上能够正常进行数据存储操作。

10.4 数据加密与安全问题

如前文所述,SharedPreferences 和 SQLite 存储的数据默认是明文的。为了保护敏感数据,可以使用以下加密方法:

  1. 对称加密:如 AES(高级加密标准)算法,在存储数据前对数据进行加密,读取数据时进行解密。在 Dart 中可以使用 pointycastle 库来实现 AES 加密。
  2. 非对称加密:对于一些需要验证数据来源和完整性的场景,可以使用非对称加密算法,如 RSA。同样,pointycastle 库也提供了 RSA 加密的支持。在使用加密技术时,要妥善保管加密密钥,避免密钥泄露导致数据安全问题。

通过解决这些常见问题,能够使我们在使用 SharedPreferences 和 SQLite 结合进行 Flutter 应用数据存储开发时更加顺畅,打造出稳定、安全且高性能的应用。