數(shù)據(jù)持久化存儲方案 - Hive Flutter

Hive Flutter

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棺榔。

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) typeIdreadwrite骗污。這里官方的文檔比較簡單崇猫,因為,現(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,
        ],
      ),
    );
  }
}

效果如下:

root_page

當然,現(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,
        ),
      ),
    );
  }
}

整體代碼比較多遣铝,但拆分組件后佑刷,邏輯并沒有變得太復雜。效果如下:

todo 待辦 10.0MB
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末酿炸,一起剝皮案震驚了整個濱河市瘫絮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌填硕,老刑警劉巖麦萤,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扁眯,居然都是意外死亡壮莹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門姻檀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來命满,“玉大人,你說我怎么就攤上這事绣版〗禾ǎ” “怎么了歼疮?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诈唬。 經(jīng)常有香客問我韩脏,道長,這世上最難降的妖魔是什么铸磅? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任赡矢,我火速辦了婚禮,結果婚禮上阅仔,老公的妹妹穿的比我還像新娘吹散。我一直安慰自己,他們只是感情好霎槐,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梦谜,像睡著了一般丘跌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上唁桩,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天闭树,我揣著相機與錄音,去河邊找鬼荒澡。 笑死报辱,一個胖子當著我的面吹牛,可吹牛的內容都是我干的单山。 我是一名探鬼主播碍现,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼米奸!你這毒婦竟也來了昼接?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤悴晰,失蹤者是張志新(化名)和其女友劉穎慢睡,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铡溪,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡漂辐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棕硫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片髓涯。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哈扮,靈堂內的尸體忽然破棺而出复凳,到底是詐尸還是另有隱情瘤泪,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布育八,位于F島的核電站对途,受9級特大地震影響,放射性物質發(fā)生泄漏髓棋。R本人自食惡果不足惜实檀,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望按声。 院中可真熱鬧膳犹,春花似錦、人聲如沸签则。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渐裂。三九已至豺旬,卻和暖如春腮介,著一層夾襖步出監(jiān)牢的瞬間嗅义,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工子眶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膝捞,地道東北人坦刀。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像蔬咬,于是被迫代替她去往敵國和親鲤遥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內容