Skip to content

Flutter - 数据库概念

Flutter 提供了强大的包,用于与各种类型的数据库进行交互,包括本地数据库和基于云的数据库。主要选项包括:

  • sqflite:用于访问和操作设备本地 SQLite 数据库的标准包。
  • cloud_firestore:一个用于与 Google 灵活、可扩展的 NoSQL 云数据库 Firebase Firestore 交互的包。
  • 其他选项:像 drift(以前称为 Moor)这样的包提供了对 SQLite/SQL 更高层次的抽象,而 hive 则提供了快速的键值存储。Realm 也为 Flutter 提供了官方 SDK。

本章重点介绍用于本地存储的 sqflite 和用于云存储的 cloud_firestore,并演示常见模式。

使用 SQLite 和 sqflite 进行本地存储

Section titled “使用 SQLite 和 sqflite 进行本地存储”

SQLite 是一个久经考验的嵌入式 SQL 数据库引擎,非常适合在设备本地存储结构化数据。sqflite 包提供了一个低级 API,用于在 Flutter 中有效地与 SQLite 数据库交互。

sqflite 提供的核心功能包括:

  • 打开或创建数据库:使用 openDatabase,您可以连接到现有的数据库文件或创建一个新的文件。
  • 执行 SQL 语句:execute 等方法允许运行原始 SQL 命令(CREATE TABLE, INSERT, UPDATE, DELETE)。使用参数化查询(? 占位符)来防止 SQL 注入漏洞。
  • 查询数据:query 方法提供了一种便捷的方式,可以使用结构化参数(列、where、orderBy 等)来获取数据。可以使用 rawQuery 执行原始查询。
  • 事务:transaction 确保多个数据库操作原子地执行(全部成功或全部失败)。

让我们更新产品应用示例,演示如何使用 sqflite 和现代实践来存储和获取产品信息。

  1. 创建一个新的 Flutter 应用:flutter create product_sqlite_app。
  2. 进入项目目录:cd product_sqlite_app。
  3. 添加依赖:打开 pubspec.yaml 并在 dependencies 下添加 sqflite 和 path_provider:
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0 # 使用最新的稳定版本
path_provider: ^2.1.1 # 使用最新的稳定版本
path: ^1.8.3 # 通常会传递包含,但最好指定
  1. 在终端中运行 flutter pub get 安装包。
  2. 定义数据模型:创建一个 lib/product.dart 文件,包含 Product 类。确保它使用 null safety 并包含转换为 Map 以及从 Map 转换的方法,这对于数据库交互至关重要。建议使用自增主键。
class Product {
final int? id; // 在插入新产品之前可能为 null
final String name;
final String description;
final int price;
final String image;
// 对不可变对象使用 const 构造函数
const Product({
this.id,
required this.name,
required this.description,
required this.price,
required this.image,
});
// 工厂构造函数,用于从 Map 创建 Product 对象(例如,从数据库)
factory Product.fromMap(Map<String, dynamic> map) {
return Product(
id: map['id'] as int?,
name: map['name'] as String,
description: map['description'] as String,
price: map['price'] as int,
image: map['image'] as String,
);
}
// 将 Product 实例转换为 Map 的方法(例如,用于数据库插入/更新)
Map<String, dynamic> toMap() {
// 当 'id' 为 null 时排除它(用于自增插入)
return {
if (id != null) 'id': id,
'name': name,
'description': description,
'price': price,
'image': image,
};
}
// 可选:用于简化更新的 copyWith 方法
Product copyWith({
int? id,
String? name,
String? description,
int? price,
String? image,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
price: price ?? this.price,
image: image ?? this.image,
);
}
@override
String toString() {
return 'Product(id: $id, name: $name, description: $description, price: $price, image: $image)';
}
}
  1. 创建一个数据库 Helper 服务:创建一个 lib/database_helper.dart 文件。此类将使用单例模式或依赖注入封装所有数据库操作。
import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'product.dart';
class DatabaseHelper {
// 单例模式
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
static Database? _database;
static const String _dbName = 'ProductDB.db';
static const String _tableName = 'Product';
static const int _dbVersion = 1;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB();
return _database!;
}
Future<Database> _initDB() async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, _dbName);
return await openDatabase(
path,
version: _dbVersion,
onCreate: _onCreate,
// 可选:在此处理 schema 迁移
// onUpgrade: _onUpgrade,
);
}
// 创建表并插入初始数据
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $_tableName (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT NOT NULL,
price INTEGER NOT NULL,
image TEXT NOT NULL
)
''');
// 可选:使用 Batch 高效地插入初始数据
final batch = db.batch();
_getInitialProducts().forEach((product) {
batch.insert(_tableName, product.toMap());
});
await batch.commit(noResult: true);
}
List<Product> _getInitialProducts() {
return [
Product(name: 'iPhone', description: 'The stylish phone', price: 1000, image: 'iphone.png'),
Product(name: 'Pixel', description: 'The feature phone', price: 800, image: 'pixel.png'),
Product(name: 'Laptop', description: 'Productive tool', price: 2000, image: 'laptop.png'),
// ... 添加其他初始产品
];
}
// 插入产品
Future<int> insertProduct(Product product) async {
final db = await database;
// 使用 `insert`,它会处理自增 ID
return await db.insert(_tableName, product.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
}
// 获取所有产品
Future<List<Product>> getAllProducts() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(_tableName, orderBy: 'id ASC');
return List.generate(maps.length, (i) {
return Product.fromMap(maps[i]);
});
}
// 按 ID 获取单个产品
Future<Product?> getProductById(int id) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
_tableName,
where: 'id = ?',
whereArgs: [id],
limit: 1, // 期望只有一个结果
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
} else {
return null;
}
}
// 更新产品
Future<int> updateProduct(Product product) async {
final db = await database;
return await db.update(
_tableName,
product.toMap(),
where: 'id = ?',
whereArgs: [product.id],
);
}
// 删除产品
Future<int> deleteProduct(int id) async {
final db = await database;
return await db.delete(
_tableName,
where: 'id = ?',
whereArgs: [id],
);
}
// 关闭数据库(可选,在特定场景下有用)
Future<void> close() async {
final db = await database;
db.close();
_database = null; // 重置静态变量
}
}
  1. 与 UI 集成:修改您的 main.dart(或相关的 widget 文件)以使用 DatabaseHelper。
  2. 使用 FutureBuilder 或状态管理解决方案(如 Provider 或 Riverpod)来处理数据库调用的异步特性并相应地更新 UI。
// 在 StatefulWidget 的 State 或 StateNotifier/ChangeNotifier 中的示例用法
// 获取 helper 实例
final dbHelper = DatabaseHelper();
// 获取产品
Future<List<Product>> _loadProducts() async {
return await dbHelper.getAllProducts();
}
// 添加新产品
Future<void> _addProduct(Product newProduct) async {
await dbHelper.insertProduct(newProduct);
// 刷新 UI(例如,调用 setState 或 notifyListeners)
}
// 更新现有产品
Future<void> _updateProduct(Product updatedProduct) async {
await dbHelper.updateProduct(updatedProduct);
// 刷新 UI
}
// 删除产品
Future<void> _deleteProduct(int id) async {
await dbHelper.deleteProduct(id);
// 刷新 UI
}

这种设置提供了清晰的关注点分离,并使用现代 Dart/Flutter 实践进行本地数据库管理。

Firebase 提供了后端即服务(BaaS)功能,显著加快了应用开发。Firebase Firestore 是一个流行的选择,它是一个基于云的实时 NoSQL 数据库,可自动扩展并提供离线支持。

cloud_firestore 包允许 Flutter 应用直接与 Firestore 交互。让我们更新示例以连接到 Firestore。

  1. 创建一个新的 Flutter 应用:flutter create product_firestore_app。
  2. 进入项目目录:cd product_firestore_app。
  3. 添加 Firebase Core 和 Firestore 依赖:打开 pubspec.yaml:
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.2 # 使用最新的稳定版本
cloud_firestore: ^4.13.6 # 使用最新的稳定版本
  1. 运行 flutter pub get。
  2. 配置 Firebase:这是关键一步,涉及平台特定的设置。
  3. a. 安装 Firebase CLI:npm install -g firebase-tools(或其他方法)。
  4. b. 登录:firebase login。
  5. c. 安装 FlutterFire CLI:dart pub global activate flutterfire_cli。
  6. d. 配置您的项目:在您的 Flutter 项目根目录中运行 flutterfire configure。此命令将引导您选择或创建一个 Firebase 项目,并自动配置您的 Android、iOS 以及可能的 Web 应用,生成 firebase_options.dart 等必要文件并更新原生配置文件。
  7. e.(手动回退 - 不推荐):如果 CLI 失败,请按照 Firebase 控制台说明手动添加 Android/iOS 应用,下载 google-services.json (Android) / GoogleService-Info.plist (iOS),并配置 Gradle/Xcode。
  8. 在您的应用中初始化 Firebase:更新 lib/main.dart,在运行应用之前初始化 Firebase。
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; // 由 flutterfire configure 生成
void main() async {
// 确保 Flutter 绑定已初始化
WidgetsFlutterBinding.ensureInitialized();
// 初始化 Firebase
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// ... MyApp widget 的其余部分
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
);
}
}
  1. 更新 Product 类(如果需要):确保 Product.fromMap 可以处理 Firestore 的数据类型(例如,用于日期的 Timestamp)。添加适用于 Firestore 的 toJson 或 toMap 方法。
import 'package:cloud_firestore/cloud_firestore.dart';
class Product {
final String? id; // Firestore 文档 ID
final String name;
final String description;
final int price;
final String image;
// 在 Firestore 中对日期使用 Timestamp
final Timestamp? createdAt;
Product({
this.id,
required this.name,
required this.description,
required this.price,
required this.image,
this.createdAt,
});
// 将 Firestore DocumentSnapshot 转换为 Product 对象
factory Product.fromFirestore(DocumentSnapshot<Map<String, dynamic>> snapshot, SnapshotOptions? options) {
final data = snapshot.data();
return Product(
id: snapshot.id,
name: data?['name'] ?? '',
description: data?['description'] ?? '',
price: data?['price'] ?? 0,
image: data?['image'] ?? '',
createdAt: data?['createdAt'] as Timestamp?,
);
}
// 将 Product 对象转换为 Map 以用于 Firestore
Map<String, dynamic> toFirestore() {
return {
'name': name,
'description': description,
'price': price,
'image': image,
// 创建时对服务器端时间戳使用 FieldValue
'createdAt': createdAt ?? FieldValue.serverTimestamp(),
};
}
}
  1. 与 Firestore 交互:使用 FirebaseFirestore.instance 获取数据。
  2. a. 获取集合引用:FirebaseFirestore.instance.collection('products')。
  3. b. 对您的 Product 模型使用 .withConverter 以确保类型安全。
  4. c. 使用 get()(用于单次获取)或 snapshots()(用于实时更新)来获取数据。
// 获取带类型转换器的集合引用
final productsRef = FirebaseFirestore.instance
.collection('products')
.withConverter<Product>(
fromFirestore: Product.fromFirestore,
toFirestore: (Product product, _) => product.toFirestore(),
);
// 获取产品一次
Future<List<Product>> fetchProductsOnce() async {
try {
final querySnapshot = await productsRef.orderBy('createdAt', descending: true).get();
return querySnapshot.docs.map((doc) => doc.data()).toList();
} catch (e) {
print('获取产品错误:$e');
return [];
}
}
// 获取产品的实时流
Stream<QuerySnapshot<Product>> fetchProductsStream() {
return productsRef.orderBy('createdAt', descending: true).snapshots();
}
// 添加新产品
Future<void> addProduct(Product newProduct) async {
try {
await productsRef.add(newProduct);
print('产品已添加!');
} catch (e) {
print('添加产品错误:$e');
}
}
// 更新产品(需要文档 ID)
Future<void> updateProduct(String docId, Product updatedProduct) async {
try {
await productsRef.doc(docId).update(updatedProduct.toFirestore());
print('产品已更新!');
} catch (e) {
print('更新产品错误:$e');
}
}
// 删除产品(需要文档 ID)
Future<void> deleteProduct(String docId) async {
try {
await productsRef.doc(docId).delete();
print('产品已删除!');
} catch (e) {
print('删除产品错误:$e');
}
}
  1. 使用 StreamBuilder 进行实时更新:如果使用 snapshots(),请将您的 UI 组件(例如 ListView)包裹在 StreamBuilder 中,以便在 Firestore 中的数据更改时自动重建 UI。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Products from Firestore")),
body: StreamBuilder<QuerySnapshot<Product>>(
stream: fetchProductsStream(), // 您的流函数
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text('No products found.'));
}
// 数据可用
final products = snapshot.data!.docs.map((doc) => doc.data()).toList();
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
// 使用产品数据构建您的列表项 widget
return ListTile(
title: Text(product.name),
subtitle: Text(product.description),
// ... 添加删除/编辑等交互
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 示例:添加一个新产品
addProduct(Product(name: 'New Gadget', description: 'Latest tech', price: 500, image: 'gadget.png'));
},
child: Icon(Icons.add),
),
);
}

Firestore 提供了一个强大且可扩展的后端解决方案。请记住在 Firebase 控制台中配置 Firestore 安全规则来保护您的数据。