引言
再次欢迎回到 每天一个Flutter开发小项目 系列博客!在之前的六篇博客中,我们逐步解锁了 Flutter 移动开发的各项核心技能,从 UI 构建、交互设计、状态管理到页面导航,您已然具备了构建功能丰富 Flutter 应用的基础。
然而,我们之前构建的应用大多还停留在“一次性”使用的阶段,应用数据无法在应用关闭后保存,这显然无法满足真实应用的需求。在实际开发中,数据持久化是至关重要的一环,它使得应用能够记住用户的操作、保存用户的创作,真正发挥应用的价值。今天,我们将聚焦 Flutter 应用的 “记忆力” —— 数据持久化,并构建一个每个人都需要的 简易笔记应用,让您掌握 Flutter 应用数据本地存储的专业技巧。
通过本篇博客,您将深入学习:
- Flutter 数据持久化的核心概念: 理解数据持久化的重要性,掌握 Flutter 中实现数据持久化的各种方案。
sqflite
数据库的专业应用: 深入学习sqflite
插件,掌握在 Flutter 应用中使用 SQLite 数据库进行数据持久化的全流程,包括数据库创建、表设计、CRUD 操作等。- 本地数据库CRUD操作: 熟练掌握使用
sqflite
进行 SQLite 数据库的 增 (Create)、删 (Delete)、改 (Update)、查 (Read) 操作,构建完整的数据管理功能。 - 异步数据库操作的最佳实践: 理解 Flutter 中数据库操作的异步特性,掌握异步编程技巧,确保应用流畅性。
- 简易笔记应用的功能实现: 构建一个功能完善的简易笔记应用,包括笔记创建、查看、编辑、删除、列表展示等核心功能。
- 应用数据管理的专业技能: 从数据模型设计到数据库操作,全面提升 Flutter 应用数据管理的专业技能。
项目简介: 简易笔记应用
我们的简易笔记应用将围绕以下核心功能展开:
- 创建新笔记: 用户可以创建新的笔记,并输入笔记标题和内容。
- 查看笔记列表: 应用主页以列表形式展示所有已创建的笔记,方便用户浏览和查找。
- 查看笔记详情: 点击笔记列表项,可以进入笔记详情页面,查看完整的笔记内容。
- 编辑现有笔记: 在笔记详情页面,用户可以编辑笔记的标题和内容,并保存修改。
- 删除笔记: 在笔记列表或详情页面,用户可以删除不再需要的笔记。
- 数据持久化: 所有笔记数据都将持久化存储在本地数据库中,应用重启后数据不会丢失。
通过构建简易笔记应用,我们将重点实践:
sqflite
数据库集成: 在 Flutter 应用中集成sqflite
插件,搭建本地数据库环境。- 数据库表设计: 设计合理的数据库表结构,存储笔记数据 (例如,笔记ID、标题、内容、创建时间等字段)。
- CRUD 操作实现: 使用
sqflite
插件实现笔记数据的增删改查操作,包括数据库查询、数据插入、数据更新、数据删除等。 - 异步数据库操作: 使用
async/await
关键字处理数据库操作的异步任务,确保 UI 线程不被阻塞。 - 简易笔记应用完整功能实现: 从 UI 界面到数据逻辑,完整构建一个实用的简易笔记应用。
Flutter 数据持久化方案概览
在深入 sqflite
之前,我们先来概览 Flutter 中常用的数据持久化方案,以便您对 Flutter 数据存储有更全面的了解。Flutter 提供了多种数据持久化方案,适用于不同的数据存储需求:
-
shared_preferences
: 轻量级的键值对存储方案,适用于存储少量的应用配置数据、用户偏好设置等简单数据。shared_preferences
以键值对的形式将数据存储在设备的本地存储中,例如,存储用户的登录状态、主题设置、应用语言等。 优点: 简单易用,快速上手。 缺点: 只适合存储少量简单数据,不支持复杂数据结构和查询操作。 -
sqflite
数据库: SQLite 数据库的 Flutter 插件,适用于存储结构化数据,支持关系型数据库的各种操作,例如,表创建、数据查询、事务处理等。sqflite
将数据存储在设备本地的 SQLite 数据库文件中。优点: 功能强大,支持复杂数据结构和查询操作,性能较好,适用于存储中等规模的结构化数据。 缺点: 相对于shared_preferences
学习曲线稍陡峭,需要了解 SQL 数据库的基本知识。 -
path_provider
: 文件路径提供器插件,用于获取设备文件系统中常用目录的路径,例如,应用文档目录、临时目录等。path_provider
本身不提供数据存储功能,但可以与其他数据存储方案 (例如,文件读写、JSON 文件存储、数据库存储等) 结合使用,指定数据存储的路径。优点: 方便获取设备文件系统路径,与各种数据存储方案灵活搭配。 缺点: 本身不提供数据存储功能,需要与其他方案结合使用。 -
文件读写: Flutter 提供了
dart:io
库,可以进行文件读写操作,将数据存储到本地文件中,例如,文本文件、JSON 文件、图片文件等。优点: 灵活性高,可以存储各种类型的文件数据。 缺点: 需要手动处理文件读写操作,较为繁琐,不适合存储结构化数据。 -
NoSQL 数据库 (例如,Firebase Firestore, MongoDB Realm): NoSQL 数据库的 Flutter SDK,适用于存储非结构化数据、文档型数据,支持云端同步、实时数据更新 等高级功能。NoSQL 数据库通常将数据存储在云端数据库服务中。 优点: 功能强大,支持复杂数据结构和查询,支持云端同步和实时更新,适用于构建需要云端数据同步的应用。 缺点: 需要依赖云服务,学习曲线较陡峭,通常用于构建更大型、复杂的应用。
-
对象关系映射 (ORM) 库 (例如,drift): ORM 库 可以简化数据库操作,将数据库表映射为 Dart 对象,通过操作 Dart 对象来操作数据库,提高开发效率。优点: 简化数据库操作,提高开发效率,代码更简洁易读。 缺点: 学习曲线较陡峭,可能会引入额外的性能开销。
在本篇博客中,我们选择 sqflite
数据库 来构建简易笔记应用的数据持久化方案。sqflite
数据库功能强大,性能良好,能够满足简易笔记应用的数据存储需求,同时也是 Flutter 应用开发中常用的数据持久化方案。
实战步骤: 构建简易笔记应用
接下来,我们将一步步使用 sqflite
数据库构建我们的简易笔记应用。
步骤 1: 创建新的 Flutter 项目并添加 sqflite
依赖
首先,创建一个新的 Flutter 项目,命名为 simple_notes_app
。
然后在 pubspec.yaml
文件中添加 sqflite
和 path_provider
依赖:
dependencies:
flutter:
sdk: flutter
sqflite: ^2.0.0 # 使用最新版本,请查阅 pub.dev 获取最新版本号
path_provider: ^2.0.0 # 使用最新版本,请查阅 pub.dev 获取最新版本号
运行 flutter pub get
命令获取依赖。
步骤 2: 定义笔记数据模型 (Note)
我们需要定义一个 Note
类来表示笔记数据,包含笔记ID、标题、内容、创建时间等信息。
创建 lib/models/note.dart
文件,定义 Note
类:
class Note {
final int? id; // 笔记ID,自增长,可为空,数据库生成
final String title; // 笔记标题
final String content; // 笔记内容
final DateTime createdAt; // 创建时间
const Note({
// 使用 const 构造函数
this.id,
required this.title,
required this.content,
required this.createdAt,
});
Map<String, dynamic> toMap() {
// 将 Note 对象转换为 Map<String, dynamic>,方便数据库操作
return {
'id': id,
'title': title,
'content': content,
'createdAt': createdAt.millisecondsSinceEpoch, // 将 DateTime 转换为毫秒时间戳存储
};
}
static Note fromMap(Map<String, dynamic> map) {
// 从 Map<String, dynamic> 创建 Note 对象,方便从数据库读取数据
return Note(
id: map['id'],
title: map['title'],
content: map['content'],
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), // 从毫秒时间戳创建 DateTime 对象
);
}
}
代码解释:
Note
类: 定义了Note
类,包含id
(笔记ID),title
(标题),content
(内容),createdAt
(创建时间) 等属性。int? id
:id
属性为int?
类型,表示笔记 ID,允许为空。 数据库自增长的 ID 在插入数据时通常为空,由数据库自动生成。DateTime createdAt
:createdAt
属性为DateTime
类型,表示笔记创建时间。toMap()
方法: 将Note
对象转换为Map<String, dynamic>
类型,方便进行数据库插入、更新等操作。createdAt.millisecondsSinceEpoch
将DateTime
对象转换为毫秒时间戳,以便在数据库中存储。fromMap(Map<String, dynamic> map)
方法: 静态方法,从Map<String, dynamic>
类型的数据创建Note
对象,方便从数据库读取数据后转换为Note
对象。DateTime.fromMillisecondsSinceEpoch(map['createdAt'])
从毫秒时间戳创建DateTime
对象。
步骤 3: 创建数据库助手类 (DatabaseHelper)
我们需要创建一个数据库助手类 DatabaseHelper
来封装数据库操作,包括数据库初始化、表创建、CRUD 操作等。
创建 lib/helpers/database_helper.dart
文件,定义 DatabaseHelper
类:
import 'package:path/path.dart'; // 导入 path 插件
import 'package:sqflite/sqflite.dart'; // 导入 sqflite 插件
import 'package:path_provider/path_provider.dart'; // 导入 path_provider 插件
import '../models/note.dart'; // 导入 Note 类
class DatabaseHelper {
static const _databaseName = "NotesDatabase.db"; // 数据库名称
static const _databaseVersion = 1; // 数据库版本号
static const tableNotes = 'notes'; // 笔记表名称
DatabaseHelper._privateConstructor(); // 私有构造函数,单例模式
static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); // 单例模式实例
static Database? _database; // 数据库实例
Future<Database> get database async {
// 获取数据库实例,单例模式
if (_database != null) return _database!; // 如果数据库实例已存在,直接返回
_database = await _initDatabase(); // 如果数据库实例不存在,初始化数据库
return _database!;
}
_initDatabase() async {
// 初始化数据库
Directory documentsDirectory = await getApplicationDocumentsDirectory(); // 获取应用文档目录
String path = join(documentsDirectory.path, _databaseName); // 拼接数据库文件路径
return await openDatabase( // 打开数据库
path,
version: _databaseVersion,
onCreate: _onCreate, // 数据库首次创建时回调
);
}
Future _onCreate(Database db, int version) async {
// 数据库首次创建时回调
await db.execute(''' // 执行 SQL 创建表语句
CREATE TABLE $tableNotes (
id INTEGER PRIMARY KEY AUTOINCREMENT, // ID,自增长主键
title TEXT NOT NULL, // 标题,非空
content TEXT NOT NULL, // 内容,非空
createdAt INTEGER NOT NULL // 创建时间,非空,存储毫秒时间戳
)
''');
}
Future<int> insertNote(Note note) async {
// 插入笔记
Database db = await instance.database; // 获取数据库实例
return await db.insert(tableNotes, note.toMap()); // 插入数据,返回插入记录的 ID
}
Future<List<Note>> getAllNotes() async {
// 查询所有笔记
Database db = await instance.database; // 获取数据库实例
final List<Map<String, dynamic>> maps = await db.query(tableNotes, orderBy: 'createdAt DESC'); // 查询所有数据,按创建时间倒序排列
if (maps.isEmpty) {
// 如果查询结果为空,返回空列表
return [];
}
return List.generate(maps.length, (i) {
// 将 List<Map<String, dynamic>> 转换为 List<Note>
return Note.fromMap(maps[i]);
});
}
Future<Note?> getNoteById(int id) async {
// 根据 ID 查询笔记
Database db = await instance.database; // 获取数据库实例
List<Map<String, dynamic>> maps =