Flutter

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

2021-01-13  本文已影响0人  djyuning
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;

三、举个例子 🌰

本文以 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。

注意:自定义的对象,必须要使用 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 个静态(异步)方法:

首先,我们需要在 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
上一篇 下一篇

猜你喜欢

热点阅读