Hive 是一个纯 Dart 编写的、基于文件存储的、轻量且功能强大的 Key-Value 型数据库。适用于 Flutter 生态的各端(本文以 Flutter 移动端为例分享)。
Hive 官方文档 https://docs.hivedb.dev/#/
一、为什么用 Hive ?🧐
Flutter 端实现持久化存储的方案很多,比如 shared_preferences(以下简称 SP),SP 也是 Key-Value 格式的数据存储方案,但它更像是一个原子型的存储方案,很多常用的功能需要自己去实现;再比如 sqflite,它是一个轻巧的数据库,支持原生数据库的绝大多数功能,但需要使用者熟悉 SQL 操作,上手曲线很陡峭。当然,还有很多其他的数据存储方案,我暂时还没了解到,不再举例。
Bloc 是状态管理方案,状态,意味着 APP 一旦关闭,其状态就会丢失。但应用使用期间,其状态是可以实时更新、跨页面、跨组件同步更新的。
那为什么用 Hive 呢?正如上面提到的三个 package,Hive 正是集成了三者的优点,一站式解决了数据持久存储和实时响应的问题。它完全没有 SP 的简陋、sqflite 的陡峭曲线,同时还兼具了 Bloc 的数据同步。
如果你的应用不需要后端支持、需要存储一定数量的数据,又不想项目过于复杂,Hive 绝对值得试试。
️ 注意:总归总,Hive 还是文件型数据存储方案,内存压力和 CPU 性能是绕不开的话题。所以,Hive 不适合存储过多的数据,Hive 的作者在 issue 中建议 1000 ~ 5000;超过这个值,性能会逐渐降低。更有建设性的方案,建议仔细阅读 isuse,其中的几个大佬还给出了其他合理方案。
二、例外
Hive 虽然可以解决部分数据存储的问题以及一些状态同步问题,但并不意味着它可以完全替代 SP、sqflite 和 Bloc;
- 多个设备同步数据:这种情况考虑使用 后端 + Bloc 的方案解决。以【音乐】应用为例,如果你是做一个播放器,Hive 很值得推荐,如果你是做云音乐,建议还是后端存储吧。
- 大数据读写:考虑使用索引处理以提高性能,参考:issue-170;
- 图片存储:Hive 支持二进制格式的图片存储,但建议图片体积不要过大(建议 2M 以下),我觉得还是使用 OSS 存储比较合理。
-
分页查询:Hive 通常会一次性加载所有数据到内存中,不支持类似 SQL 的分页查询,如果需要实现,可以使用
List
的 Api。
三、举个例子 🌰
本文以 2 个小例子演示如何上手 Hive。
3.1 新建项目并安装依赖
使用 flutter create hive_demo
创建一个 App。
打开项目,在 pubspec.yaml
安装以下依赖:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0
# 目录操作,Hive 初始化时,需要指定一个存储位置
# https://pub.flutter-io.cn/packages/path_provider
path_provider: ^1.6.24
# Hive 相关依赖
# https://pub.flutter-io.cn/packages/hive
hive: ^1.4.4+1
# Hive Flutter 支持,扩展了 Flutter 组件
# https://pub.flutter-io.cn/packages/hive
hive_flutter: ^0.3.1
# Hive 自定义 Object 支持
# https://pub.flutter-io.cn/packages/hive_generator
hive_generator: ^0.8.2
为了演示代码,我们把新工程的 main.dart 文件拆分一下,其中的 MyHomePage
被我拆分到了一个独立的文件(./lib/pages/root_page.dart)中,名字也被替换成了 RootPage
。
3.2 明确概念
Hive 中有三个概念需要了解,分别是:Box、Object、Adapter。
-
Box:数据通常都存放在 Box 中,看上去很像数据库中的 Table;但是,Hive 中,我们可以直接使用 Box 操作数据,比如:
Box.add
、Box.delete
等,所以,Box 更像是一个Module
。 -
Object:Object 就像数据库中的 Entity(实体)。 Hive 可以存储绝大多数的数据类型,例如:
Box.add('小米')
、Box.put('platform', '安卓')
,如果需要存储复杂的数据,就需要自定义一个对象,通常对象需要继承自HiveObject
,如:class MyObj extends HiveObject
。 -
Adapter:是自定义对象的适配器,需要实现
typeId
、read
和write
。这里官方的文档比较简单,因为,现实中我们的对象不可能只有一个字段,多个字段如何使用,官方没有在文档中演示,另外,write 的用法也没有完善,其实我们可以在 write 的时候对数据进行 默认值 处理。
注意:自定义的对象,必须要使用 Adapter 注册,参考:https://docs.hivedb.dev/#/custom-objects/type_adapters。
有了基本的概念,我们就可以尝试敲一下代码了。
3.3 挂载
在 Hive 中,如果需要存储数据,就需要使用到 Box
,比如:
// 伪代码
Box box = await Hive.box('users');
但跨组件或页面使用时,新页面中如果不定义 Box,则会出现变量未定义的错误,如果定义了,就会报 Box 已经打开的错误,也就是说,一个 Box 如果已经打开,就不能再次打开。
如果上次调用完成后再调用 box.close()
,数据又会无法同步。
所以,我们需要建立一个单例类,以确保应用初始化时就已经实例化好需要的 Box,接下来,我们只需要调用这个类的实例,就可以拿到需要的 Box。代码如下:
/// ./lin/utils/db_util.dart
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
/// Hive 数据操作
class DBUtil {
/// 实例
static DBUtil instance;
/// 初始化,需要在 main.dart 调用
/// <https://docs.hivedb.dev/>
static Future<void> install() async {
/// 初始化数据库地址
Directory document = await getApplicationDocumentsDirectory();
Hive.init(document.path);
/// 注册自定义对象(实体)
/// <https://docs.hivedb.dev/#/custom-objects/type_adapters>
/// Hive.registerAdapter(SettingsAdapter());
}
/// 初始化 Box
static Future<DBUtil> getInstance() async {
if (instance == null) {
instance = DBUtil();
await Hive.initFlutter();
}
return instance;
}
}
该单例提供了 2 个静态(异步)方法:
-
DBUtil.install()
:该方法会在应用启动时调用,用于初始化 Hive 的状态; -
DBUtil.getInstance()
:在组件使用时调用,用于获取该类的实例,拿到实例我们就可以获取其中的 Box;
首先,我们需要在 main.dart
中调用 DBUtil.install
方法:
/// ./lib/main.dart
import 'package:flutter/material.dart';
import 'package:hive_demo/pages/root_page.dart';
import 'package:hive_demo/utils/db_util.dart';
void main() async {
/// 注意:需要添加下面的一行,才可以使用 异步方法
WidgetsFlutterBinding.ensureInitialized();
/// 初始化 Hive
await DBUtil.install();
await DBUtil.getInstance();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hive Demo',
theme: ThemeData(
platform: TargetPlatform.iOS,
primaryColor: Colors.blueAccent,
appBarTheme: AppBarTheme(elevation: 0),
),
home: RootPage(),
);
}
}
重启 App,当 App 启动时,Hive 会被初始化,我们还没有定义 Box 实例,所以,现在没有任何的效果。
3.4 简单数据存取
首先,我们尝试一下简单的 Box 数据存储,做一个新增标签的功能。修改我们的 root_page 页面,代码如下:
/// ./lib/pages/root_page.dart
import 'package:flutter/material.dart';
class RootPage extends StatefulWidget {
@override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
TextEditingController _tagEditingController;
@override
void initState() {
_tagEditingController = TextEditingController();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hive Demo'),
),
body: ListView(
children: [
tagsHeader,
Container(child: tags, padding: EdgeInsets.all(10)),
tagsCreator,
],
),
);
}
/// 标签列表
Widget get tags {
/// 标签集合
List<String> tags = ['设计', '开发', '运维', '测试', '产品'];
return Wrap(
spacing: 10,
alignment: WrapAlignment.center,
children: List.generate(
tags.length,
(int index) {
final String text = tags[index];
return Chip(
label: Text(text),
onDeleted: () {
// 删除操作
},
);
},
),
);
}
/// 新增标签
Widget get tagsCreator {
/// 输入表单
Widget input = TextField(
controller: _tagEditingController,
decoration: InputDecoration(
hintText: '标签',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 10),
),
);
/// 新增按钮
Widget submit = RaisedButton(
child: Text('新增'),
elevation: 0,
padding: EdgeInsets.all(14),
onPressed: () {
// 新增标签
},
);
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blueGrey.withAlpha(60)),
borderRadius: BorderRadius.circular(8),
),
margin: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
padding: EdgeInsets.all(6),
child: Row(
children: [
Expanded(child: input),
SizedBox(width: 10),
submit,
],
),
);
}
/// 标签操作
Widget get tagsHeader {
/// 清空按钮
Widget clearBtn = FlatButton(
child: Text(
'清空',
style: TextStyle(color: Colors.red),
),
padding: EdgeInsets.zero,
onPressed: () {
/// 清空标签
},
);
return Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(
children: [
Expanded(child: Text('标签管理')),
clearBtn,
],
),
);
}
}
效果如下:
当然,现在的数据都是静态的,接下来我们一步步实现动态数据展示。
首先,我们实例化一个 Box,为了统一管理,我们在单例类中新建,修改单例类,新增 tagsBox
Box 实例,并实例化它。
/// ./lib/utils/db_util.dart
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
/// Hive 数据操作
class DBUtil {
/// 实例
static DBUtil instance;
/// 标签
Box tagsBox;
/// 初始化,需要在 main.dart 调用
/// <https://docs.hivedb.dev/>
static Future<void> install() async {
/// 初始化数据库地址
Directory document = await getApplicationDocumentsDirectory();
Hive.init(document.path);
/// 注册自定义对象(实体)
/// <https://docs.hivedb.dev/#/custom-objects/type_adapters>
/// Hive.registerAdapter(SettingsAdapter());
}
/// 初始化 Box
static Future<DBUtil> getInstance() async {
if (instance == null) {
instance = DBUtil();
await Hive.initFlutter();
/// 标签
instance.tagsBox = await Hive.openBox('tags');
}
return instance;
}
}
同时,修改我们的 root_page 代码,在其中建立一个 dbUtil 实例。
/// ./lib/pages/root_page.dart
class _RootPageState extends State<RootPage> {
TextEditingController _tagEditingController;
DBUtil dbUtil;
@override
void initState() {
init();
_tagEditingController = TextEditingController();
super.initState();
}
Future<void> init() async {
dbUtil = await DBUtil.getInstance();
if (!mounted) return;
setState(() {});
}
/// 其他代码略
}
重新运行 App,确保 tagsBox 创建成功。
修改标签列表渲染组件,使其可以动态渲染列表。ValueListenableBuilder
组件不需要 setState
,可以实时渲染数据。
/// ./lib/pages/root_page.dart
/// 注意,需要引入下面的两个 package
/// 我在使用的时候,listenable 方法需要 hive_flutter,但它不会自动引入,每次都需要手动引入。
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
渲染标签列表的代码如下:
/// ./lib/pages/root_page.dart
/// 标签列表
Widget get tags {
/// 先判断 dbUtil 是否初始化成功
if (dbUtil == null || dbUtil.tagsBox == null)
return Container(
child: Text('Loading'),
alignment: Alignment.center,
);
return ValueListenableBuilder(
valueListenable: dbUtil.tagsBox.listenable(),
builder: (BuildContext context, Box tags, Widget _) {
/// 数据为空
if (tags.keys.length == 0)
return Container(
child: Text('暂无数据'),
alignment: Alignment.center,
);
return Wrap(
spacing: 10,
alignment: WrapAlignment.center,
children: List.generate(tags.keys.length, (int index) {
final String text = tags.getAt(index);
return Chip(
label: Text(text),
onDeleted: () {
// 删除操作
},
);
}),
);
},
);
}
完善输入表单,使其可以正常添加数据。
/// ./lib/pages/root_page.dart
/// 新增按钮
Widget submit = RaisedButton(
child: Text('新增'),
elevation: 0,
padding: EdgeInsets.all(14),
onPressed: () async {
// 新增标签
final tag = _tagEditingController.text;
if (tag == null || tag.isEmpty) return;
await dbUtil.tagsBox.add(tag);
_tagEditingController.clear();
FocusScope.of(context).unfocus();
},
);
输入文本,标签已经可以正常添加、刷新列表了。
我们打印一下数据,看下每个数据长什么样子!在遍历 tags 前,添加一行 print(tags.toMap());
,打开控制台,可以看到数据格式:
flutter: {0: abc, 1: asd, 2: abcd, 3: eee, 4: fff, 5: ggg, 6: hihi}
可以看出,Box 存储的数据是一个 Map,其中的 key 可以理解为数据库中的自增 ID。接下来,我们实现删除,就需要使用到这个 key 值。
修改标签组件,添加删除逻辑。
/// ./lib/pages/root_page.dart
/// 标签列表
Widget get tags {
/// 先判断 dbUtil 是否初始化成功
if (dbUtil == null || dbUtil.tagsBox == null)
return Container(
child: Text('Loading'),
alignment: Alignment.center,
);
return ValueListenableBuilder(
valueListenable: dbUtil.tagsBox.listenable(),
builder: (BuildContext context, Box tags, Widget _) {
/// 数据为空
if (tags.keys.length == 0)
return Container(
child: Text('暂无数据'),
alignment: Alignment.center,
);
return Wrap(
spacing: 10,
alignment: WrapAlignment.center,
children: List.generate(tags.keys.length, (int index) {
final int key = tags.keyAt(index);
final String text = tags.getAt(index);
return Chip(
label: Text(text),
onDeleted: () async {
// 删除操作
await dbUtil.tagsBox.delete(key);
},
);
}),
);
},
);
}
最后,实现清空操作!
/// ./lib/pages/root_page.dart
/// 标签操作
Widget get tagsHeader {
/// 清空按钮
Widget clearBtn = FlatButton(
child: Text(
'清空',
style: TextStyle(color: Colors.red),
),
padding: EdgeInsets.zero,
onPressed: () async {
/// 清空标签
await dbUtil.tagsBox.clear();
},
);
return Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(
children: [
Expanded(child: Text('标签管理')),
clearBtn,
],
),
);
}
至此,我们已经简单体验了 Hive 的基本玩法。
3.5 自定义对象数据存取
很明显,上面的例子还是很单一的,现实中,我们存储的数据可能比这复杂的多。接下来,我们创建一个简单的 TODO 待办。
我们需要建立一个待办条目对象,每个条目都包含 内容(String)、创建日期(DateTime)、完成日期(DateTime)、优先级(int) 等几个属性。
首先,在 ./lib/db/
目录下建立我们的 Object。
/// ./lib/db/todo_item_db.dart
import 'package:hive/hive.dart';
@HiveType()
class TodoItem extends HiveObject {
/// 内容
String content;
/// 优先级
int level;
/// 创建日期
String createAt;
/// 完成日期
String completionAt;
TodoItem({
this.content,
this.level,
this.createAt,
this.completionAt,
});
}
class TodoItemAdapter extends TypeAdapter<TodoItem> {
@override
final int typeId = 0;
@override
TodoItem read(BinaryReader reader) {
return TodoItem(
content: reader.read(),
level: reader.read(),
createAt: reader.read(),
completionAt: reader.read(),
);
}
@override
void write(BinaryWriter writer, obj) {
writer.write(obj.content);
writer.write(obj.level ?? 0);
writer.write(obj.createAt ?? DateTime.now().toString());
writer.write(obj.completionAt);
}
}
然后,在 DBUtil 单例中注册 TodoItemAdapter
。修改 db_util.dart 中的 install
方法,增加 Hive.registerAdapter(TodoItemAdapter());
,同时,我们还需要修改其中的 getInstance
方法,新增一个 todoBox
,最终如下:
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:hive_demo/db/todo_item_db.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
/// Hive 数据操作
class DBUtil {
/// 实例
static DBUtil instance;
/// 标签
Box tagsBox;
/// 待办
Box todoBox;
/// 初始化,需要在 main.dart 调用
/// <https://docs.hivedb.dev/>
static Future<void> install() async {
/// 初始化数据库地址
Directory document = await getApplicationDocumentsDirectory();
Hive.init(document.path);
/// 注册自定义对象(实体)
/// <https://docs.hivedb.dev/#/custom-objects/type_adapters>
Hive.registerAdapter(TodoItemAdapter());
}
/// 初始化 Box
static Future<DBUtil> getInstance() async {
if (instance == null) {
instance = DBUtil();
await Hive.initFlutter();
/// 标签
instance.tagsBox = await Hive.openBox('tags');
/// 待办
instance.todoBox = await Hive.openBox('todo');
}
return instance;
}
}
修改完成,重新运行我们的 App。
新建一个 TodoPage(./lib/pages/todo_page.dart),并在 main.dart 中替换我们的页面。
Hive 的 api 比较好理解,增删改的逻辑代码量通常只有几行。这里我们不在赘述,直接布局好 UI,简单调用就可以了。TodoPage 的代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hive_demo/db/todo_item_db.dart';
import 'package:hive_demo/utils/db_util.dart';
class TodoPage extends StatefulWidget {
@override
_TodoPageState createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
DBUtil dbUtil;
@override
void initState() {
init();
super.initState();
}
Future<void> init() async {
dbUtil = await DBUtil.getInstance();
if (!mounted) return;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hive Todo'),
actions: [
IconButton(
icon: Icon(Icons.clear_all),
onPressed: () async {
bool confirm = await confirmAlert('确定清空所有待办?');
if (confirm != true) return;
await dbUtil.todoBox.clear();
},
),
],
),
body: content,
floatingActionButton: createBtn,
);
}
Widget get content {
if (dbUtil == null || dbUtil.todoBox == null)
return Container(
child: Text('Loading'),
alignment: Alignment.center,
);
return ValueListenableBuilder(
valueListenable: dbUtil.todoBox.listenable(),
builder: (BuildContext context, Box todos, Widget _) {
if (todos.keys.length == 0) return empty;
return lists(todos);
},
);
}
Widget lists(Box todos) {
int total = todos.keys.length;
/// 获取未完成待办
List<TodoItem> defaults = [];
/// 获取已完成待办
List<TodoItem> completions = [];
for (int i = 0; i < total; i++) {
TodoItem item = todos.getAt(i);
if (item.completionAt != null) {
completions.add(item);
} else {
defaults.add(item);
}
}
/// 创建待处理列表
Widget defaultsList = ListView.builder(
itemCount: defaults.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext contenx, int index) => row(defaults[index]),
);
/// 创建已完成列表
Widget completionsList = ListView.builder(
itemCount: completions.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext contenx, int index) => row(completions[index]),
);
return ListView(
children: [
SizedBox(height: 10),
defaultsList,
if (completions.length > 0) completionsList,
if (total > 0)
Container(
padding: EdgeInsets.all(20),
alignment: Alignment.center,
child: Text(
'共 $total 条待办',
style: TextStyle(
color: Colors.blueGrey,
fontSize: 12,
),
),
),
SizedBox(height: 10),
],
);
}
/// 待办条目
Widget row(TodoItem item) {
/// 是否存在优先级
bool inLevel = item.level != null && item.level > 0;
/// 是否已完成
bool isCompletion = item.completionAt != null;
/// 优先级图标
Widget levelPrefix = Text(
'!' * item.level,
style: TextStyle(color: Colors.red),
);
/// 文本内容
Widget content = Expanded(
child: Text(
item.content ?? '未输入内容',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
decoration:
isCompletion ? TextDecoration.lineThrough : TextDecoration.none,
),
),
);
/// 副标题
Widget subtitle = Text(
(isCompletion ? item.completionAt : item.createAt) ?? '-',
);
/// 操作
Widget actions = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!isCompletion)
IconButton(
icon: Icon(Icons.edit, size: 20, color: Colors.green),
onPressed: () {
showDialog(
context: context,
child: TodoCreateDialog(
dbUtil: dbUtil,
item: item,
),
);
},
),
IconButton(
icon: Icon(Icons.clear, size: 20, color: Colors.red),
onPressed: () async {
bool confirm = await confirmAlert('确定删除本条待办?');
if (confirm != true) return;
await dbUtil.todoBox.delete(item.key);
},
),
],
);
return Container(
margin: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
isCompletion
? Container(width: 24)
: IconButton(
icon: Icon(Icons.check_circle,
size: 20, color: Colors.blueAccent),
onPressed: () async {
/// 已完成
item.completionAt = DateTime.now().toString();
await dbUtil.todoBox.put(item.key, item);
},
),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (inLevel) levelPrefix,
if (inLevel) SizedBox(width: 10),
content,
],
),
SizedBox(height: 8),
subtitle
],
),
),
SizedBox(width: 10),
actions,
],
),
);
}
/// 确认弹窗
Future<bool> confirmAlert(String content, {String title = '操作提示'}) async {
return await showDialog(
context: context,
child: AlertDialog(
title: Text(title),
content: Text(content),
actions: [
FlatButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('取消'),
),
FlatButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('确定'),
),
],
),
);
}
/// 无数据
Widget get empty {
return Container(
child: Text('暂无数据'),
alignment: Alignment.center,
);
}
/// 新增按钮
Widget get createBtn {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
child: TodoCreateDialog(dbUtil: dbUtil),
);
},
);
}
}
/// 弹窗
class TodoCreateDialog extends StatefulWidget {
/// 从上下文传入 DBUtil,避免再次获取实例
final DBUtil dbUtil;
/// 如果传入了一个条目,则视为编辑
final TodoItem item;
const TodoCreateDialog({
Key key,
@required this.dbUtil,
this.item,
}) : super(key: key);
@override
_TodoCreateDialogState createState() => _TodoCreateDialogState();
}
class _TodoCreateDialogState extends State<TodoCreateDialog> {
TextEditingController _contentEditingController;
String content;
int level;
@override
void initState() {
level = 0;
_contentEditingController = TextEditingController();
if (widget.item != null) {
content = widget.item?.content;
_contentEditingController.text = content;
level = widget.item?.level ?? 0;
setState(() {});
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
title,
input,
levelPicker,
SizedBox(height: 20),
Divider(),
actions,
],
),
);
}
/// 标题
Widget get title {
return Container(
padding: EdgeInsets.only(left: 20, right: 20, top: 20),
width: double.infinity,
child: Text(
widget.item != null ? '编辑待办' : '新建待办',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
}
/// 输入框
Widget get input {
return Container(
margin: EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueGrey.withAlpha(70)),
borderRadius: BorderRadius.circular(6),
),
child: Column(
children: [
TextField(
minLines: 2,
maxLines: 8,
controller: _contentEditingController,
decoration: InputDecoration(
hintText: '请填写待办事项',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
onChanged: (String value) {
setState(() {
content = value;
});
},
),
],
),
);
}
/// 优先级
Widget get levelPicker {
return Row(
children: [
SizedBox(width: 20),
Expanded(
child: Text(
'优先级',
style: TextStyle(
fontSize: 12,
color: Colors.blueGrey,
),
),
),
CupertinoSegmentedControl(
groupValue: level,
borderColor: Colors.green,
selectedColor: Colors.green,
padding: EdgeInsets.zero,
children: {
0: Padding(
child: Text('正常'),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
1: Text('高'),
2: Text('紧急'),
},
onValueChanged: (int index) {
setState(() {
level = index;
});
},
),
SizedBox(width: 20),
],
);
}
Widget get actions {
return Container(
padding: EdgeInsets.only(
right: 20,
left: 20,
bottom: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(child: cancelBtn),
Expanded(child: confirmBtn),
],
),
);
}
/// 取消按钮
Widget get cancelBtn {
return FlatButton(
minWidth: double.infinity,
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'取消',
style: TextStyle(
fontSize: 16,
color: Colors.blueGrey,
),
),
);
}
/// 创建按钮
Widget get confirmBtn {
return FlatButton(
minWidth: double.infinity,
onPressed: () async {
if (widget.item != null) {
/// 更新
await widget.dbUtil.todoBox.put(
widget.item.key,
TodoItem(
content: content,
level: level ?? 0,
createAt: widget.item.createAt,
completionAt: widget.item.completionAt,
),
);
} else {
/// 新增
await widget.dbUtil.todoBox.add(TodoItem(
content: content,
level: level ?? 0,
createAt: DateTime.now().toString(),
));
}
Navigator.of(context).pop();
},
child: Text(
widget.item != null ? '保存' : '创建',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
}
整体代码比较多,但拆分组件后,逻辑并没有变得太复杂。效果如下: