21
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flutter大学Advent Calendar 2021

Day 2

[flutter×riverpod×hive] MVVMで作るhiveサンプル

Last updated at Posted at 2021-12-01

はじめに

flutter大学では共同開発を行なっております。その際にhiveを触る機会があったのでお勉強がてら、サンプル的なものを作りました。
hive自体あまり情報が多くなく、riverpodと合わせて書かれている記事をほとんど見たことがないので今回書いてみた次第です。といっても今回はhiveがメインなのでriverpodは画面側とデータベース側の中継役にしかなっていないのですが、アプリがもっと複雑になればriverpodも活躍すると思うので練習がてら作成した感じになります。
正直あまり正解が分かっていないので参考程度に見ていただけるとありがたいです。

今回作ったサンプルは簡単なToDoリストです。
Simulator Screen Recording - iPhone 13 - 2021-11-30 at 22.50.45.gif

 ソースコードは以下になります。
https://github.com/shunsukenoguchi/hive_todo_sample

記事を書くのは初めてなのでお手柔らかにお願いします。

今回参考にした資料

日本語の資料は調べた当初は見てもあまりよくわからなかったので以下のYoutubeを参考にしました。こちらはsetStateで書かれているのでこちらを参考にriverpodに書き換えています。あとは、出来るだけ構造をシンプルかつ簡単にしています。
正直、こちらが理解できれば僕の記事は見なくても大丈夫かなと思います。

僕が勉強していた時はなかった記事ですが、わかりやすそうだったのでこちらも載せて置きます。
hiveを最初に理解するには良い記事かと思います。今回は出来るだけ丁寧に説明するつもりですが、こちらを見ておくと理解が早いかもしれません。

まず最初にやること

パッケージのインポート

以下の4つをpubspeck.yamlに追加します。
hive
hive_flutter
hive_generator
build_runner

pubspec.yaml
// 2021/11/28 時点のversion
dependencies:
  flutter:
    sdk: flutter
  hive: ^2.0.4
  hive_flutter: ^1.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  hive_generator: ^1.1.1
  build_runner: ^2.1.5

追加したら、pub getをする。 
コマンドで以下でもOKです。

$ flutter pub get

初期設定

まず、初期設定としてhiveの初期化を行います。
現状のmain.dartは以下のようになっており、TodoPageを作成しています。

main.dart
void main() async {
  // Hiveの初期化
  await Hive.initFlutter();  // 追加

  // 描画の開始
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoPage(), //後ほど記載します
    );
  }
}

これで初期設定が完了です。参考にしたYoutubeではあまり触れていませんでしたが、この時点でhiveは使用できるようになっています。

hiveの公式ドキュメント
https://docs.hivedb.dev/#/

ドキュメントのように以下でデータの書き込みと読み取りができるようになっています。

main.dart
import 'package:hive/hive.dart';

void main() async {
  //Hive.init('somePath') -> not needed in browser

  var box = await Hive.openBox('testBox');

  box.put('name', 'David');
  
  print('Name: ${box.get('name')}');
}

ただ、上記の記述方法だと型がないので、型をつけてあげた方が良いです。
型がないとコード書いている時にちゃんと値が取れているかわかるので安心です。

型の指定(タイプアダプターの作成)

今回はToDoアプリなので以下を作成します。

model/todo/todo.dart
import 'package:hive/hive.dart';
part 'todo.g.dart'; // todo.dartの場合

@HiveType(typeId: 0) //typeIdはユニークである必要がある
class Todo extends HiveObject {
  @HiveField(0)
  //HiveFieldはクラス内でユニークである必要がある
  late String contents;
}

上記を作成後、ターミナルで以下を実行しタイプアダプターを作成します。

$ flutter pub run build_runner build
model/todo/todo.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'todo. dart';

// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************

class TodoAdapter extends TypeAdapter<Todo> {
  @override
  final int typeId = 0;

  @override
  Todo read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return Todo()..contents = fields[0] as String;
  }

  @override
  void write(BinaryWriter writer, Todo obj) {
    writer
      ..writeByte(1)
      ..writeByte(0)
      ..write(obj.contents);
  }

  @override
  int get hashCode => typeId.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is TodoAdapter &&
          runtimeType == other.runtimeType &&
          typeId == other.typeId;
}

ここで生成したファイルはいじってはいけないらしく、クラスを変更した際には上記のコマンドを再度実行することでアダプターを変更できます。
gitのバージョン管理に追加した場合は再実行してもエラーが出るため、todo.g.dartファイルを削除してから、再実行する必要があります。

タイプアダプターの指定

上記で生成したタイプアダプターを使うにはmain.dartに以下を追加する必要があります。

 main.dart
void main() async {
  await Hive.initFlutter();
  Hive.registerAdapter(TodoAdapter()); //追加
  // todo.g.dartで生成されたTodoAdapterとimport 'package:hive_todo_sample/model/todo/todo.dart';を追加する
  runApp(MyApp());
}

データを格納するBoxを作る

以下でtodosというhiveのBoxを作成します。
Todoクラスがインポートされてないと思うのでインポートして下さい。
これによってtodosにデータを保存できるようになります。

 main.dart
void main() async {
  await Hive.initFlutter();
  Hive.registerAdapter(TodoAdapter()); 
  await Hive.openBox<Todo>('todos');//追加
  // todo.g.dartで生成されたTodoAdapterとimport 'package:hive_todo_sample/model/todo/todo.dart';を追加する
  runApp(MyApp());
}

Boxの取得と変更の検知

Boxesクラスを作成し、todosのBoxを取得する、getTodosメゾットを作成します。

boxes.dart
import 'package:hive/hive.dart';
import 'package:hive_todo_sample/model/todo/todo.dart';

class Boxes {
  static Box<Todo> getTodos() => Hive.box<Todo>('todos');
}

そして、todo_page.dartに以下のようにすることでtodosのBoxの取得ができます。
Boxに値を追加したの際に値の更新の検知し、再描画してくれます。

todo_page.dart
 ValueListenableBuilder<Box<Todo>>(
   valueListenable: Boxes.getTodos().listenable(), // ここが重要
   builder: (context, box, _) {
     final todos = box.values.toList().cast<Todo>();
     return buildContent(todos, todoModel);
   },
 ),

todo_page.dartの全体はこんな感じになっています。

todo_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hive_todo_sample/boxes.dart';
import 'package:hive_todo_sample/model/todo/todo.dart';
import 'package:hive_todo_sample/page/todo/todo_model.dart';

class TodoPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todoModel = ref.read(todoModelProvider);
    var _contentsController = TextEditingController();
    return Scaffold(
      appBar: AppBar(
        title: Text('Hive Todo Sample'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Container(
              width: double.infinity,
              child: Column(
                children: [
                  TextField(
                    controller: _contentsController,
                    decoration: InputDecoration(
                      hintText: 'Todoを追加してください',
                    ),
                  ),
                  ElevatedButton(
                      onPressed: () => {
                            todoModel.addTodo(_contentsController.text),
                            _contentsController.text = ''
                          },
                      child: Text('追加')),
                ],
              ),
            ),
            SizedBox(
              height: 24,
            ),
            ValueListenableBuilder<Box<Todo>>(
              valueListenable: Boxes.getTodos().listenable(),
              builder: (context, box, _) {
                final todos = box.values.toList().cast<Todo>();
                return buildContent(todos, todoModel);
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget buildContent(List<Todo> todos, TodoModel todoModel) {
    if (todos.isEmpty) {
      return Center(
        child: Text(
          '',
        ),
      );
    } else {
      return SingleChildScrollView(
        child: Column(
          children: [
            Container(
              height: 300,
              child: ListView.builder(
                shrinkWrap: true,
                itemCount: todos.length,
                itemBuilder: (BuildContext context, int index) {
                  final todo = todos[index];
                  return buildTodo(context, todo, todoModel);
                },
              ),
            ),
          ],
        ),
      );
    }
  }

  Widget buildTodo(BuildContext context, Todo todo, TodoModel todoModel) {
    return Card(
        color: Colors.white,
        child: ExpansionTile(
          title: Text(
            todo.contents,
          ),
          trailing: buildButtons(context, todo, todoModel),
        ));
  }

  Widget buildButtons(BuildContext context, Todo todo, TodoModel todoModel) =>
      ElevatedButton(
          onPressed: () => {todoModel.deleteTodo(todo)}, child: Text('削除'));
}

Boxに値を追加する

見た目ができたので実際にhiveにデータを追加していきます。
追加ボタンを押したらhiveにデータが追加されるようにしていきます。

追加ボタン部分は以下ですね、ボタンが押されてtodo_model.dartaddTodoが実行されます。

todo_page.dart
ElevatedButton(
  onPressed: () => {
    todoModel.addTodo(_contentsController.text),
    _contentsController.text = ''
  },
  child: Text('追加')
),

todo_model.dartではtodoRepositoryファイルのaddTodoRepositoryを実行しています。正直ここはcontents渡してるだけであまり意味ないですがアプリが複雑で値をいじる必要があったら意味あるかなという感じですかね。

todo_model.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_todo_sample/model/todo/todo.dart';
import 'package:hive_todo_sample/repository/todo_repository.dart';

final todoModelProvider = ChangeNotifierProvider<TodoModel>(
  (ref) => TodoModel(),
);

class TodoModel extends ChangeNotifier {
  TodoRepository todoRepository = TodoRepository(); 
    // TodoRepositoryクラスのインスタンスを作る

  void addTodo(String contents) {
    todoRepository.addTodoRepository(contents);
  }
}

ここまでは呼び出しているだけなので本命のBoxにデータを追加します。
todo_repository.dartで行なっていて以下になります。

todo_repository.dart
import 'package:hive_todo_sample/boxes.dart';
import 'package:hive_todo_sample/model/todo/todo.dart';

class TodoRepository {
  Future addTodoRepository(String contents) async {
    final todo = Todo()..contents = contents;
    final box = Boxes.getTodos();
    box.add(todo);
  }
}

受け取ったcontentstodoに代入しています。

final todo = Todo()..contents = contents;

Todo()..contents←この記法はカスケードというやつらしいです。詳しくは以下で。

前述してありますがこれでtodosのBoxを取得します。

final box = Boxes.getTodos();

addすることでデータを追加できます。

box.add(todo);

hiveの基本的な使い方はデータ追加する時はputを使うようですが、todo.dartHiveObjectを継承した際は、adddeletesaveなどメゾットっぽく扱えるらしいです。

todo.dart
@HiveType(typeId: 0) 
class Todo extends HiveObject ←これ! {
  @HiveField(0)
  //HiveFieldはクラス内でユニークである必要がある
  late String contents;
}

Boxの値を削除する

最後にBoxの値を削除します。
削除ボタンは以下です。処理の順番は追加時と変わらず、処理を読んでいるだけなのでコードを載せるだけにします。

todo_page.dart
Widget buildButtons(BuildContext context, Todo todo, TodoModel todoModel) =>
      ElevatedButton(
          onPressed: () => {todoModel.deleteTodo(todo)}, child: Text('削除'));
todo_model.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_todo_sample/model/todo/todo.dart';
import 'package:hive_todo_sample/repository/todo_repository.dart';

final todoModelProvider = ChangeNotifierProvider<TodoModel>(
  (ref) => TodoModel(),
);

class TodoModel extends ChangeNotifier {
  TodoRepository todoRepository = TodoRepository();

  void addTodo(String contents) {
    todoRepository.addTodoRepository(contents);
  }

  void deleteTodo(Todo todo) { // 追加部分
    todoRepository.deleteTodoRepository(todo);
  }
}
todo_repository.dart
import 'package:hive_todo_sample/boxes.dart';
import 'package:hive_todo_sample/model/todo/todo.dart';

class TodoRepository {
  Future addTodoRepository(String contents) async {
    final todo = Todo()..contents = contents;
    final box = Boxes.getTodos();
    box.add(todo);
  }

  void deleteTodoRepository(Todo todo) { // 追加部分
    todo.delete();
  }
}

削除はtodoオブジェクトをdeleteするだけで出来ます。
めちゃめちゃ簡単ですね。

終わり

なんだか、冗長になってしまいましたが、誰かの助けになれば幸いです。
誤字脱字やコード間違いなど在りましたら、ご指摘していただけるとありがたいです。

21
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?