Flutter - 数据库概念
Flutter - 现代数据库概念
Section titled “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 和现代实践来存储和获取产品信息。
- 创建一个新的 Flutter 应用:
flutter create product_sqlite_app。 - 进入项目目录:
cd product_sqlite_app。 - 添加依赖:打开
pubspec.yaml并在dependencies下添加sqflite和path_provider:
dependencies: flutter: sdk: flutter sqflite: ^2.3.0 # 使用最新的稳定版本 path_provider: ^2.1.1 # 使用最新的稳定版本 path: ^1.8.3 # 通常会传递包含,但最好指定- 在终端中运行
flutter pub get安装包。 - 定义数据模型:创建一个
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)'; }}- 创建一个数据库 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; // 重置静态变量 }}- 与 UI 集成:修改您的
main.dart(或相关的 widget 文件)以使用DatabaseHelper。 - 使用
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 Firestore 进行云存储
Section titled “使用 Firebase Firestore 进行云存储”Firebase 提供了后端即服务(BaaS)功能,显著加快了应用开发。Firebase Firestore 是一个流行的选择,它是一个基于云的实时 NoSQL 数据库,可自动扩展并提供离线支持。
cloud_firestore 包允许 Flutter 应用直接与 Firestore 交互。让我们更新示例以连接到 Firestore。
- 创建一个新的 Flutter 应用:
flutter create product_firestore_app。 - 进入项目目录:
cd product_firestore_app。 - 添加 Firebase Core 和 Firestore 依赖:打开
pubspec.yaml:
dependencies: flutter: sdk: flutter firebase_core: ^2.24.2 # 使用最新的稳定版本 cloud_firestore: ^4.13.6 # 使用最新的稳定版本- 运行
flutter pub get。 - 配置 Firebase:这是关键一步,涉及平台特定的设置。
- a. 安装 Firebase CLI:
npm install -g firebase-tools(或其他方法)。 - b. 登录:
firebase login。 - c. 安装 FlutterFire CLI:
dart pub global activate flutterfire_cli。 - d. 配置您的项目:在您的 Flutter 项目根目录中运行
flutterfire configure。此命令将引导您选择或创建一个 Firebase 项目,并自动配置您的 Android、iOS 以及可能的 Web 应用,生成firebase_options.dart等必要文件并更新原生配置文件。 - e.(手动回退 - 不推荐):如果 CLI 失败,请按照 Firebase 控制台说明手动添加 Android/iOS 应用,下载
google-services.json(Android) /GoogleService-Info.plist(iOS),并配置 Gradle/Xcode。 - 在您的应用中初始化 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( // ... ); }}- 更新
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(), }; }}- 与 Firestore 交互:使用
FirebaseFirestore.instance获取数据。 - a. 获取集合引用:
FirebaseFirestore.instance.collection('products')。 - b. 对您的
Product模型使用.withConverter以确保类型安全。 - 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'); }}- 使用
StreamBuilder进行实时更新:如果使用snapshots(),请将您的 UI 组件(例如ListView)包裹在StreamBuilder中,以便在 Firestore 中的数据更改时自动重建 UI。
@overrideWidget 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 安全规则来保护您的数据。