Hive 是一個純 Dart 編寫的鹊漠、基于文件存儲的声登、輕量且功能強大的 Key-Value 型數(shù)據(jù)庫健芭。適用于 Flutter 生態(tài)的各端(本文以 Flutter 移動端為例分享)。
Hive 官方文檔 https://docs.hivedb.dev/#/
一慈迈、為什么用 Hive ???
Flutter 端實現(xiàn)持久化存儲的方案很多谴麦,比如 shared_preferences(以下簡稱 SP)伸头,SP 也是 Key-Value 格式的數(shù)據(jù)存儲方案,但它更像是一個原子型的存儲方案恤磷,很多常用的功能需要自己去實現(xiàn)面哼;再比如 sqflite,它是一個輕巧的數(shù)據(jù)庫扫步,支持原生數(shù)據(jù)庫的絕大多數(shù)功能魔策,但需要使用者熟悉 SQL 操作,上手曲線很陡峭河胎。當然闯袒,還有很多其他的數(shù)據(jù)存儲方案,我暫時還沒了解到,不再舉例搁吓。
Bloc 是狀態(tài)管理方案原茅,狀態(tài),意味著 APP 一旦關閉堕仔,其狀態(tài)就會丟失。但應用使用期間晌区,其狀態(tài)是可以實時更新摩骨、跨頁面、跨組件同步更新的朗若。
那為什么用 Hive 呢恼五?正如上面提到的三個 package,Hive 正是集成了三者的優(yōu)點哭懈,一站式解決了數(shù)據(jù)持久存儲和實時響應的問題灾馒。它完全沒有 SP 的簡陋、sqflite 的陡峭曲線遣总,同時還兼具了 Bloc 的數(shù)據(jù)同步睬罗。
如果你的應用不需要后端支持、需要存儲一定數(shù)量的數(shù)據(jù)花盐,又不想項目過于復雜,Hive 絕對值得試試熙揍。
? 注意:總歸總诈嘿,Hive 還是文件型數(shù)據(jù)存儲方案,內存壓力和 CPU 性能是繞不開的話題昔字。所以陨囊,Hive 不適合存儲過多的數(shù)據(jù),Hive 的作者在 issue 中建議 1000 ~ 5000压语;超過這個值胎食,性能會逐漸降低。更有建設性的方案粥航,建議仔細閱讀 isuse,其中的幾個大佬還給出了其他合理方案映之。
二杠输、例外
Hive 雖然可以解決部分數(shù)據(jù)存儲的問題以及一些狀態(tài)同步問題,但并不意味著它可以完全替代 SP鹦牛、sqflite 和 Bloc曼追;
- 多個設備同步數(shù)據(jù):這種情況考慮使用 后端 + Bloc 的方案解決。以【音樂】應用為例晶伦,如果你是做一個播放器族沃,Hive 很值得推薦竭业,如果你是做云音樂窟绷,建議還是后端存儲吧兼蜈。
- 大數(shù)據(jù)讀寫:考慮使用索引處理以提高性能歼郭,參考:issue-170病曾;
- 圖片存儲:Hive 支持二進制格式的圖片存儲,但建議圖片體積不要過大(建議 2M 以下)逼蒙,我覺得還是使用 OSS 存儲比較合理。
-
分頁查詢:Hive 通常會一次性加載所有數(shù)據(jù)到內存中驳棱,不支持類似 SQL 的分頁查詢,如果需要實現(xiàn),可以使用
List
的 Api却汉。
三、舉個例子 ??
本文以 2 個小例子演示如何上手 Hive。
3.1 新建項目并安裝依賴
使用 flutter create hive_demo
創(chuàng)建一個 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:數(shù)據(jù)通常都存放在 Box 中任连,看上去很像數(shù)據(jù)庫中的 Table;但是,Hive 中爆土,我們可以直接使用 Box 操作數(shù)據(jù),比如:
Box.add
坏瘩、Box.delete
等,所以哪自,Box 更像是一個Module
壤巷。 -
Object:Object 就像數(shù)據(jù)庫中的 Entity(實體)寄症。 Hive 可以存儲絕大多數(shù)的數(shù)據(jù)類型,例如:
Box.add('小米')
剪决、Box.put('platform', '安卓')
,如果需要存儲復雜的數(shù)據(jù)渗鬼,就需要自定義一個對象,通常對象需要繼承自HiveObject
堰乔,如:class MyObj extends HiveObject
驶冒。 -
Adapter:是自定義對象的適配器苟翻,需要實現(xiàn)
typeId
、read
和write
骗污。這里官方的文檔比較簡單崇猫,因為,現(xiàn)實中我們的對象不可能只有一個字段需忿,多個字段如何使用诅炉,官方?jīng)]有在文檔中演示,另外,write 的用法也沒有完善仲翎,其實我們可以在 write 的時候對數(shù)據(jù)進行 默認值 處理包晰。
注意:自定義的對象瀑罗,必須要使用 Adapter 注冊,參考:https://docs.hivedb.dev/#/custom-objects/type_adapters。
有了基本的概念姐刁,我們就可以嘗試敲一下代碼了辆苔。
3.3 掛載
在 Hive 中磁携,如果需要存儲數(shù)據(jù),就需要使用到 Box
,比如:
// 偽代碼
Box box = await Hive.box('users');
但跨組件或頁面使用時绒疗,新頁面中如果不定義 Box账千,則會出現(xiàn)變量未定義的錯誤矢沿,如果定義了,就會報 Box 已經(jīng)打開的錯誤,也就是說瞒窒,一個 Box 如果已經(jīng)打開巴比,就不能再次打開。
如果上次調用完成后再調用 box.close()
竟贯,數(shù)據(jù)又會無法同步。
所以,我們需要建立一個單例類凯砍,以確保應用初始化時就已經(jīng)實例化好需要的 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 數(shù)據(jù)操作
class DBUtil {
/// 實例
static DBUtil instance;
/// 初始化,需要在 main.dart 調用
/// <https://docs.hivedb.dev/>
static Future<void> install() async {
/// 初始化數(shù)據(jù)庫地址
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 個靜態(tài)(異步)方法:
-
DBUtil.install()
:該方法會在應用啟動時調用蛋哭,用于初始化 Hive 的狀態(tài)县习; -
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 實例样勃,所以,現(xiàn)在沒有任何的效果性芬。
3.4 簡單數(shù)據(jù)存取
首先峡眶,我們嘗試一下簡單的 Box 數(shù)據(jù)存儲,做一個新增標簽的功能植锉。修改我們的 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 = ['設計', '開發(fā)', '運維', '測試', '產(chǎn)品'];
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,
],
),
);
}
}
效果如下:
當然,現(xiàn)在的數(shù)據(jù)都是靜態(tài)的俊庇,接下來我們一步步實現(xiàn)動態(tài)數(shù)據(jù)展示狮暑。
首先,我們實例化一個 Box辉饱,為了統(tǒng)一管理搬男,我們在單例類中新建,修改單例類彭沼,新增 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 數(shù)據(jù)操作
class DBUtil {
/// 實例
static DBUtil instance;
/// 標簽
Box tagsBox;
/// 初始化姓惑,需要在 main.dart 調用
/// <https://docs.hivedb.dev/>
static Future<void> install() async {
/// 初始化數(shù)據(jù)庫地址
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 創(chuàng)建成功。
修改標簽列表渲染組件唯沮,使其可以動態(tài)渲染列表脖旱。ValueListenableBuilder
組件不需要 setState
堪遂,可以實時渲染數(shù)據(jù)。
/// ./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 _) {
/// 數(shù)據(jù)為空
if (tags.keys.length == 0)
return Container(
child: Text('暫無數(shù)據(jù)'),
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: () {
// 刪除操作
},
);
}),
);
},
);
}
完善輸入表單佳恬,使其可以正常添加數(shù)據(jù)捏境。
/// ./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();
},
);
輸入文本,標簽已經(jīng)可以正常添加毁葱、刷新列表了垫言。
我們打印一下數(shù)據(jù),看下每個數(shù)據(jù)長什么樣子倾剿!在遍歷 tags 前筷频,添加一行 print(tags.toMap());
,打開控制臺,可以看到數(shù)據(jù)格式:
flutter: {0: abc, 1: asd, 2: abcd, 3: eee, 4: fff, 5: ggg, 6: hihi}
可以看出前痘,Box 存儲的數(shù)據(jù)是一個 Map凛捏,其中的 key 可以理解為數(shù)據(jù)庫中的自增 ID。接下來芹缔,我們實現(xiàn)刪除坯癣,就需要使用到這個 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 _) {
/// 數(shù)據(jù)為空
if (tags.keys.length == 0)
return Container(
child: Text('暫無數(shù)據(jù)'),
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);
},
);
}),
);
},
);
}
最后,實現(xiàn)清空操作芝硬!
/// ./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,
],
),
);
}
至此蚜点,我們已經(jīng)簡單體驗了 Hive 的基本玩法。
3.5 自定義對象數(shù)據(jù)存取
很明顯拌阴,上面的例子還是很單一的绍绘,現(xiàn)實中,我們存儲的數(shù)據(jù)可能比這復雜的多迟赃。接下來脯倒,我們創(chuàng)建一個簡單的 TODO 待辦。
我們需要建立一個待辦條目對象捺氢,每個條目都包含 內容(String)藻丢、創(chuàng)建日期(DateTime)、完成日期(DateTime)摄乒、優(yōu)先級(int) 等幾個屬性悠反。
首先残黑,在 ./lib/db/
目錄下建立我們的 Object。
/// ./lib/db/todo_item_db.dart
import 'package:hive/hive.dart';
@HiveType()
class TodoItem extends HiveObject {
/// 內容
String content;
/// 優(yōu)先級
int level;
/// 創(chuàng)建日期
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 數(shù)據(jù)操作
class DBUtil {
/// 實例
static DBUtil instance;
/// 標簽
Box tagsBox;
/// 待辦
Box todoBox;
/// 初始化奇徒,需要在 main.dart 調用
/// <https://docs.hivedb.dev/>
static Future<void> install() async {
/// 初始化數(shù)據(jù)庫地址
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);
}
}
/// 創(chuàng)建待處理列表
Widget defaultsList = ListView.builder(
itemCount: defaults.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext contenx, int index) => row(defaults[index]),
);
/// 創(chuàng)建已完成列表
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) {
/// 是否存在優(yōu)先級
bool inLevel = item.level != null && item.level > 0;
/// 是否已完成
bool isCompletion = item.completionAt != null;
/// 優(yōu)先級圖標
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('確定'),
),
],
),
);
}
/// 無數(shù)據(jù)
Widget get empty {
return Container(
child: Text('暫無數(shù)據(jù)'),
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: '請?zhí)顚懘k事項',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
onChanged: (String value) {
setState(() {
content = value;
});
},
),
],
),
);
}
/// 優(yōu)先級
Widget get levelPicker {
return Row(
children: [
SizedBox(width: 20),
Expanded(
child: Text(
'優(yōu)先級',
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,
),
),
);
}
/// 創(chuàng)建按鈕
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 ? '保存' : '創(chuàng)建',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
}
整體代碼比較多遣铝,但拆分組件后佑刷,邏輯并沒有變得太復雜。效果如下: