美文网首页Flutter
数据持久化存储方案 - Hive Flutter

数据持久化存储方案 - Hive Flutter

作者: djyuning | 来源:发表于2021-01-13 00:44 被阅读0次
    Hive Flutter
    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

    RootPage

    3.2 明确概念

    Hive 中有三个概念需要了解,分别是:Box、Object、Adapter。

    • Box:数据通常都存放在 Box 中,看上去很像数据库中的 Table;但是,Hive 中,我们可以直接使用 Box 操作数据,比如:Box.addBox.delete 等,所以,Box 更像是一个 Module
    • Object:Object 就像数据库中的 Entity(实体)。 Hive 可以存储绝大多数的数据类型,例如:Box.add('小米')Box.put('platform', '安卓'),如果需要存储复杂的数据,就需要自定义一个对象,通常对象需要继承自 HiveObject,如:class MyObj extends HiveObject
    • Adapter:是自定义对象的适配器,需要实现 typeIdreadwrite。这里官方的文档比较简单,因为,现实中我们的对象不可能只有一个字段,多个字段如何使用,官方没有在文档中演示,另外,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,
            ],
          ),
        );
      }
    }
    

    效果如下:

    root_page

    当然,现在的数据都是静态的,接下来我们一步步实现动态数据展示。

    首先,我们实例化一个 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,
            ),
          ),
        );
      }
    }
    

    整体代码比较多,但拆分组件后,逻辑并没有变得太复杂。效果如下:

    todo 待办 10.0MB

    相关文章

      网友评论

        本文标题:数据持久化存储方案 - Hive Flutter

        本文链接:https://www.haomeiwen.com/subject/dzwxwktx.html