如何使用Hive在Flutter中存储本地数据——数据持久化存储教程

9,758 阅读11分钟

在本地存储数据并在应用启动之间持续存在是任何移动应用开发过程中的基本概念之一。几乎每一个应用程序都需要你处理数据--从存储送餐应用程序的客户信息,到游戏中的得分数量,或者一个简单的值来了解用户在最后一次访问时是否开启了黑暗模式。

Flutter提供了许多本地数据持久化选项供开发者选择。shared_preferences是一个用于本地存储小型键值对的好包,而sqflite,Flutter的SQLite包,当你处理强关系型数据,需要你在数据库中处理复杂的关系时,是一个不错的选择。

但是如果你想要一个快速安全的本地数据库,没有本地的依赖性,也能在Flutter web上运行(😉),那么Hive是一个相当不错的选择。

在这篇文章中,在我们使用Flutter构建一个简单的应用程序之前,你将学习如何开始使用Hive。我们还将研究一个允许你在Hive中处理简单关系数据的概念。

为什么是Hive?

让我们首先看看为什么你应该选择Hive而不是其他可用于Flutter本地持久化数据的解决方案。

Hive是一个轻量级和快速的键值数据库解决方案,它是跨平台的(在移动、桌面和网络上运行),并且是用纯Dart编写的。这使得它比不支持Flutter网络的sqflite更有优势--Hive没有原生的依赖性,所以它可以在网络上无缝运行。

下面是一张Hive与其他类似数据库解决方案的基准图。

Hive benchmark graph

这是一个在安卓Q系统的Oneplus 6T设备上进行的1000次读写操作的基准,你可以在Hive的GitHub上了解更多关于这个基准。

Hive还允许你使用TypeAdapters来存储自定义类。我们将在文章后面更详细地讨论这个问题

开始使用Hive

让我们建立一个基本的应用程序,在其中存储用户的详细信息,并对数据进行添加、读取、更新和删除操作。

A view of all three screens in the final sample app

使用以下命令创建一个新的Flutter项目

flutter create hive_demo

你可以用你喜欢的IDE打开这个项目,但在这个例子中,我将使用VS Code。

code hive_demo

将Hive和hive_flutter包添加到你的pubspec.yaml 文件。

dependencies:
  hive: ^2.0.4
  hive_flutter: ^1.1.0

用以下内容替换你的main.dart 文件的内容。

import 'package:flutter/material.dart';

main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hive Demo',
      theme: ThemeData(
        primarySwatch: Colors.purple,
      ),
      debugShowCheckedModeBanner: false,
      home: InfoScreen(),
    );
  }
}

InfoScreen 将显示用户的详细信息--我们一会儿就会看一下它。在此之前,让我们了解一下Hive所使用的一个重要概念。

了解盒子

Hive使用 "盒子 "的概念来存储数据库中的数据。盒子类似于SQL数据库中的表,只是盒子缺乏严格的结构。这意味着盒子很灵活,只能处理数据之间的简单关系。

在本教程中,我们将只介绍典型的Hive盒子,但值得一提的是,你也可以创建懒惰的盒子加密的盒子

初始化Hive

在进入数据库的CRUD操作之前,你必须初始化Hive并打开一个用于存储数据的盒子。

Hive应该在我们加载任何盒子之前被初始化,所以最好在你的Flutter应用程序的main() 函数中初始化它,以避免任何错误。注意,如果你在一个非Flutter的纯Dart应用程序中使用Hive,你应该使用Hive.init() 来初始化Hive。

main() async {
  // Initialize hive
  await Hive.initFlutter();
  runApp(MyApp());
}

使主函数异步化,并使用await 来初始化Hive。

现在,你必须要打开一个Hive盒子。如果你打算在你的项目中使用多个盒子,请注意,你应该在使用一个盒子之前打开它。

在这个应用程序中,我们将使用一个盒子,在Hive完成初始化后再打开。

main() async {
  // Initialize hive
  await Hive.initFlutter();
  // Open the peopleBox
  await Hive.openBox('peopleBox');
  runApp(MyApp());
}

现在我们已经准备好对本地数据库进行CRUD操作了。

执行CRUD操作

我们将在InfoScreen StatefulWidget中定义基本的CRUD操作。这个类的结构将是这样的。

import 'package:flutter/material.dart';
import 'package:hive/hive.dart';

class InfoScreen extends StatefulWidget {
  @override
  _InfoScreenState createState() => _InfoScreenState();
}

class _InfoScreenState extends State<InfoScreen> {
  late final Box box;

  @override
  void initState() {
    super.initState();
    // Get reference to an already opened box
    box = Hive.box('peopleBox');
  }

  @override
  void dispose() {
    // Closes all Hive boxes
    Hive.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

首先,我们在initState() 方法里面检索一个对我们先前打开的盒子的引用。你应该在使用完之后,在关闭应用程序之前总是关闭已打开的盒子。

由于我们目前只需要这个部件里面的盒子,我们可以在这个类的dispose() 方法里面关闭这个盒子。

让我们创建一些方法来执行CRUD操作。

class _InfoScreenState extends State<InfoScreen> {
  late final Box box;

  _addInfo() async {
    // Add info to people box
  }

  _getInfo() {
    // Get info from people box
  }

  _updateInfo() {
    // Update info of people box
  }

  _deleteInfo() {
    // Delete info from people box
  }

  // ...
}

现在我们将建立一个非常基本的UI,这样我们就可以测试出这些操作是否正常工作了。

class _InfoScreenState extends State<InfoScreen> {
  // ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('People Info'),
      ),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            ElevatedButton(
              onPressed: _addInfo,
              child: Text('Add'),
            ),
            ElevatedButton(
              onPressed: _getInfo,
              child: Text('Get'),
            ),
            ElevatedButton(
              onPressed: _updateInfo,
              child: Text('Update'),
            ),
            ElevatedButton(
              onPressed: _deleteInfo,
              child: Text('Delete'),
            ),
          ],
        ),
      ),
    );
  }
}

这个应用程序将看起来像这样。

The basic UI we created to perform CRUD operations

存储数据

如果你需要存储数据,你可以使用对Hive盒子的引用并对其调用put() 。这个方法接受一个键值对。

// Add info to people box
_addInfo() async {
  // Storing key-value pair
  box.put('name', 'John');
  box.put('country', 'Italy');
  print('Info added to box!');
}

在这里,我们存储了两个键值对,即人的姓名和他们的祖国

Hive也支持整数键,所以你可以使用自动递增的键。如果你要存储多个值(有点类似于列表),并想通过它们的索引进行检索,这就很有用。你可以像这样存储。

box.add('Linda'); // index 0, key 0
box.add('Dan');   // index 1, key 1

检索数据

要读取数据,你可以使用盒子对象上的get() 方法。你只需要提供key 来检索它的值。

// Read info from people box
_getInfo() {
  var name = box.get('name');
  var country = box.get('country');
  print('Info retrieved from box: $name ($country)');
}

如果你使用的是自动递增的值,你可以使用索引来读取,像这样。

box.getAt(0); // retrieves the value with index 0
box.getAt(1); // retrieves the value with index 1

更新数据

要更新一个特定键的数据,你可以使用你最初用来存储值的相同的put() 方法。这将用新提供的值更新存在于该键的值。

// Update info of people box
_updateInfo() {
  box.put('name', 'Mike');
  box.put('country', 'United States');
  print('Info updated in box!');
}

如果你使用的是自动递增的值,你可以使用putAt() 方法来更新存在于特定索引的值。

box.putAt(0, 'Jenifer');

删除数据

对于删除数据,你可以通过提供键来使用delete() 方法。

// Delete info from people box
_deleteInfo() {
  box.delete('name');
  box.delete('country');
  print('Info deleted from box!');
}

这将删除存在于这些特定键的值。现在,如果你试图使用这些键调用get() 方法,它将返回空值。

如果你使用的是自动递增的值,你可以通过提供索引来使用deleteAt() 方法。

box.deleteAt(0);

使用TypeAdapter的自定义对象

一般来说,Hive支持所有的原始类型,如List,Map,DateTime, 和Uint8List 。但有时你可能需要存储自定义的模型类,使数据管理更容易。

要做到这一点,你可以利用TypeAdapter,它可以生成tofrom 的二进制方法。

TypeAdapters既可以手动编写,也可以自动生成。使用代码生成来生成所需的方法总是更好的,因为它有助于防止手动编写时可能出现的任何错误(同时也更快)。

我们将使用的用于存储Person 数据的模型类,如下所示。

class Person {
  final String name;
  final String country;

  Person({
    required this.name,
    required this.country,
  });
}

生成Hive适配器

你将需要添加一些依赖项来生成Hive的TypeAdapter。在你的pubspec.yaml 文件中添加以下内容。

dev_dependencies:
  hive_generator: ^1.1.0
  build_runner: ^2.0.6

注释模型类以使用代码生成。

import 'package:hive/hive.dart';
part 'people.g.dart';

@HiveType(typeId: 1)
class People {
  @HiveField(0)
  final String name;

  @HiveField(1)
  final String country;

  People({
    required this.name,
    required this.country,
  });
}

然后你可以使用以下命令来触发代码生成。

flutter packages pub run build_runner build

注册TypeAdapter

你应该在打开使用它的盒子之前注册TypeAdapter - 否则,它会产生一个错误。由于我们只是使用一个盒子,并在main() 函数内打开它,我们必须在这之前注册适配器。

main() async {
  // Initialize hive
  await Hive.initFlutter();
  // Registering the adapter
  Hive.registerAdapter(PersonAdapter());
  // Opening the box
  await Hive.openBox('peopleBox');

  runApp(MyApp());
}

现在,你可以直接使用这个自定义类来执行数据库操作。

构建最终的应用程序

最终的应用程序将主要由三个屏幕组成。

  1. AddScreen:用于在数据库中存储用户的信息
  2. InfoScreen:用于显示存在于Hive数据库中的用户信息,以及一个用于删除用户数据的按钮
  3. UpdateScreen:用于更新数据库中的用户信息。

你不需要修改包含MyApp widget和main() 函数的main.dart 文件。

AddScreen

AddScreen 将显示一个表单,用于接受用户的数据作为输入。在我们的例子中,我们将只输入两个值,姓名原籍国。在底部会有一个按钮用于将数据发送到Hive。

The AddScreen

AddScreen 的代码如下。

class AddScreen extends StatefulWidget {
  @override
  _AddScreenState createState() => _AddScreenState();
}
class _AddScreenState extends State<AddScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text('Add Info'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: AddPersonForm(),
      ),
    );
  }
}

AddPersonForm 是创建表单用户界面的主要部件。它也包含了Hive的存储功能。

这个小组件的基本结构是这样的。

class AddPersonForm extends StatefulWidget {
  const AddPersonForm({Key? key}) : super(key: key);
  @override
  _AddPersonFormState createState() => _AddPersonFormState();
}

class _AddPersonFormState extends State<AddPersonForm> {
  late final Box box;

  @override
  void initState() {
    super.initState();
    // Get reference to an already opened box
    box = Hive.box('peopleBox');
  }

  @override
  Widget build(BuildContext context) {
    return Container(); 
  }
}

我们已经在initState() 方法中获取了对盒子的引用。现在,我们必须为表单定义一个全局键并添加一些文本编辑控制器。

class _AddPersonFormState extends State<AddPersonForm> {
  final _nameController = TextEditingController();
  final _countryController = TextEditingController();
  final _personFormKey = GlobalKey<FormState>();

  // ...
}

定义一个向Hive存储数据的方法并添加一个文本字段验证器。

class _AddPersonFormState extends State<AddPersonForm> {
  // ...

  // Add info to people box
  _addInfo() async {
    Person newPerson = Person(
      name: _nameController.text,
      country: _countryController.text,
    );
    box.add(newPerson);
    print('Info added to box!');
  }

  String? _fieldValidator(String? value) {
    if (value == null || value.isEmpty) {
      return 'Field can\'t be empty';
    }
    return null;
  }

  // ...
}

UI的代码如下。

class _AddPersonFormState extends State<AddPersonForm> {
  // ...

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _personFormKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Name'),
          TextFormField(
            controller: _nameController,
            validator: _fieldValidator,
          ),
          SizedBox(height: 24.0),
          Text('Home Country'),
          TextFormField(
            controller: _countryController,
            validator: _fieldValidator,
          ),
          Spacer(),
          Padding(
            padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 24.0),
            child: Container(
              width: double.maxFinite,
              height: 50,
              child: ElevatedButton(
                onPressed: () {
                  if (_personFormKey.currentState!.validate()) {
                    _addInfo();
                    Navigator.of(context).pop();
                  }
                },
                child: Text('Add'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

UpdateScreen

UpdateScreen 将类似于AddScreen ,但这里我们将传递Person 对象来显示文本字段中的当前值。

The UpdateScreen

这个屏幕的代码将如下。

class UpdateScreen extends StatefulWidget {
  final int index;
  final Person person;

  const UpdateScreen({
    required this.index,
    required this.person,
  });

  @override
  _UpdateScreenState createState() => _UpdateScreenState();
}

class _UpdateScreenState extends State<UpdateScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text('Update Info'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: UpdatePersonForm(
          index: widget.index,
          person: widget.person,
        ),
      ),
    );
  }
}

UpdatePersonForm widget的唯一区别是,它将包含一个更新Hive数据库中存在的值的方法。

class _UpdatePersonFormState extends State<UpdatePersonForm> {
  late final _nameController;
  late final _countryController;
  late final Box box;

  // ...

  // Update info of people box
  _updateInfo() {
    Person newPerson = Person(
      name: _nameController.text,
      country: _countryController.text,
    );
    box.putAt(widget.index, newPerson);
    print('Info updated in box!');
  }

  @override
  void initState() {
    super.initState();
    // Get reference to an already opened box
    box = Hive.box('peopleBox');
    // Show the current values
    _nameController = TextEditingController(text: widget.person.name);
    _countryController = TextEditingController(text: widget.person.country);
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      // ...
    );
  }
}

信息屏

InfoScreen 将负责显示存储在Hive中的Person 数据。基本上,read 的操作将在这里进行。

The InfoScreen

Hive提供了一个名为ValueListenableBuilder 的小部件,它只在数据库内的任何数值被修改时才会刷新。

这个屏幕将包含一些额外的功能。

  • 点击每个列表项旁边的删除按钮将从数据库中删除用户的数据。
  • 点击每个列表项将导航到UpdateScreen
  • 点击右下角的浮动操作按钮,就会进入到 "我是谁 "界面。AddScreen

这个屏幕的代码如下。

class InfoScreen extends StatefulWidget {
  @override
  _InfoScreenState createState() => _InfoScreenState();
}

class _InfoScreenState extends State<InfoScreen> {
  late final Box contactBox;

  // Delete info from people box
  _deleteInfo(int index) {
    contactBox.deleteAt(index);
    print('Item deleted from box at index: $index');
  }

  @override
  void initState() {
    super.initState();
    // Get reference to an already opened box
    contactBox = Hive.box('peopleBox');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('People Info'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => AddScreen(),
          ),
        ),
        child: Icon(Icons.add),
      ),
      body: ValueListenableBuilder(
        valueListenable: contactBox.listenable(),
        builder: (context, Box box, widget) {
          if (box.isEmpty) {
            return Center(
              child: Text('Empty'),
            );
          } else {
            return ListView.builder(
              itemCount: box.length,
              itemBuilder: (context, index) {
                var currentBox = box;
                var personData = currentBox.getAt(index)!;
                return InkWell(
                  onTap: () => Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (context) => UpdateScreen(
                        index: index,
                        person: personData,
                      ),
                    ),
                  ),
                  child: ListTile(
                    title: Text(personData.name),
                    subtitle: Text(personData.country),
                    trailing: IconButton(
                      onPressed: () => _deleteInfo(index),
                      icon: Icon(
                        Icons.delete,
                        color: Colors.red,
                      ),
                    ),
                  ),
                );
              },
            );
          }
        },
      ),
    );
  }
}

恭喜你🥳 ,你已经完成了使用Hive作为本地持久性数据库的Flutter应用程序

下面是最终应用程序的演示。

Final demo of the Flutter app using Hive

结语

这篇文章几乎涵盖了Hive的所有重要的基本概念。还有一些事情你可以用Hive数据库来做,包括存储简单的关系型数据。数据之间的简单关系可以用HiveList来处理,但如果你要向Hive存储任何敏感数据,那么你应该看看他们的加密盒

总而言之,Hive是你在Flutter中进行本地数据持久化的最佳选择之一,特别是考虑到它的速度非常快,而且几乎支持所有平台

感谢你阅读这篇文章如果你对文章或例子有任何建议或问题,欢迎在TwitterLinkedIn上与我联系。你也可以在我的GitHub上找到示例应用程序的存储库。

The postHandling local data persistence in Flutter with Hiveappeared first onLogRocket Blog.