はじめに
flutter大学では共同開発を行なっております。その際にhiveを触る機会があったのでお勉強がてら、サンプル的なものを作りました。
hive自体あまり情報が多くなく、riverpodと合わせて書かれている記事をほとんど見たことがないので今回書いてみた次第です。といっても今回はhiveがメインなのでriverpodは画面側とデータベース側の中継役にしかなっていないのですが、アプリがもっと複雑になればriverpodも活躍すると思うので練習がてら作成した感じになります。
正直あまり正解が分かっていないので参考程度に見ていただけるとありがたいです。
ソースコードは以下になります。
https://github.com/shunsukenoguchi/hive_todo_sample
記事を書くのは初めてなのでお手柔らかにお願いします。
今回参考にした資料
日本語の資料は調べた当初は見てもあまりよくわからなかったので以下のYoutubeを参考にしました。こちらはsetStateで書かれているのでこちらを参考にriverpodに書き換えています。あとは、出来るだけ構造をシンプルかつ簡単にしています。
正直、こちらが理解できれば僕の記事は見なくても大丈夫かなと思います。
僕が勉強していた時はなかった記事ですが、わかりやすそうだったのでこちらも載せて置きます。
hiveを最初に理解するには良い記事かと思います。今回は出来るだけ丁寧に説明するつもりですが、こちらを見ておくと理解が早いかもしれません。
まず最初にやること
パッケージのインポート
以下の4つをpubspeck.yamlに追加します。
hive
hive_flutter
hive_generator
build_runner
// 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
を作成しています。
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/#/
ドキュメントのように以下でデータの書き込みと読み取りができるようになっています。
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アプリなので以下を作成します。
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
// 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
に以下を追加する必要があります。
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
にデータを保存できるようになります。
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
メゾットを作成します。
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に値を追加したの際に値の更新の検知し、再描画してくれます。
ValueListenableBuilder<Box<Todo>>(
valueListenable: Boxes.getTodos().listenable(), // ここが重要
builder: (context, box, _) {
final todos = box.values.toList().cast<Todo>();
return buildContent(todos, todoModel);
},
),
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.dart
のaddTodo
が実行されます。
ElevatedButton(
onPressed: () => {
todoModel.addTodo(_contentsController.text),
_contentsController.text = ''
},
child: Text('追加')
),
todo_model.dart
ではtodoRepository
ファイルのaddTodoRepository
を実行しています。正直ここはcontents
渡してるだけであまり意味ないですがアプリが複雑で値をいじる必要があったら意味あるかなという感じですかね。
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
で行なっていて以下になります。
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);
}
}
受け取ったcontents
をtodo
に代入しています。
final todo = Todo()..contents = contents;
Todo()..contents
←この記法はカスケードというやつらしいです。詳しくは以下で。
前述してありますがこれでtodos
のBoxを取得します。
final box = Boxes.getTodos();
add
することでデータを追加できます。
box.add(todo);
hiveの基本的な使い方はデータ追加する時はput
を使うようですが、todo.dart
でHiveObject
を継承した際は、add
、delete
、save
などメゾットっぽく扱えるらしいです。
@HiveType(typeId: 0)
class Todo extends HiveObject ←これ! {
@HiveField(0)
//HiveFieldはクラス内でユニークである必要がある
late String contents;
}
Boxの値を削除する
最後にBoxの値を削除します。
削除ボタンは以下です。処理の順番は追加時と変わらず、処理を読んでいるだけなのでコードを載せるだけにします。
Widget buildButtons(BuildContext context, Todo todo, TodoModel todoModel) =>
ElevatedButton(
onPressed: () => {todoModel.deleteTodo(todo)}, child: Text('削除'));
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);
}
}
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
するだけで出来ます。
めちゃめちゃ簡単ですね。
終わり
なんだか、冗長になってしまいましたが、誰かの助けになれば幸いです。
誤字脱字やコード間違いなど在りましたら、ご指摘していただけるとありがたいです。