🗃️

ObjectBox × Freezed × StateNotifier のあれこれ

2022/12/02に公開

この記事は Flutter大学 Advent Calendar 2022 の2日目の記事です。

浜省ファンのfenです。

本記事は こんぶさんの記事の Flutter で ObjectBox つかってみた を参考にしています。そのため説明を省略する箇所が多々ありますので本記事を見る前に こんぶさんの記事を見ることをおすすめします。

ObjectBox × Freezed

最近、 Riverpod を学び始めまして個人開発に取り入れようと Provider から Riverpod の StateNotifier と個人開発アプリで使っている ローカルDBの Hive とでリファクタリングしようと試みたものの Hive × Freezed × StateNotifier に関する情報が少なかったため、他の ローカルDB を探すことにしました。そこで ObjectBox に出会い調べてみるとかなり良さそうだったので今回、記事にしてみました。

Freezed の Entity

Freezed を使った Entity の書き方です。本来の Freezed の記述方法とほぼ変わりませんが
@Entity(realClass: Person)@Id(assignable: true) を記述することで lib 配下に objectbox-model.jsonobjectbox.g.dart の2つのファイルが生成されます。ID にしたいプロパティに @Id() アノテーションを追記することでそのプロパティが ID であることを明示することになります。 assignable はデフォルトでは false になっており false の場合 ID を割り当てられません。 Freezed を使う場合 ID を自分で割り当てる必要があるので assignable は true にする必要があります。


class Person with _$Person {
  (realClass: Person) /// これ大事
  factory Person({
      (assignable: true) required int id, /// ここも大事
      required String name
    }) = _Person;
}

https://pub.dev/documentation/objectbox/latest/objectbox/Entity/realClass.html

String 型の ID を使いたい

こんぶさんの記事にもあるように ObjectBox は Hive と違って ID を指定しないといけません。そこで uuid を使おうとしたのですが ObjectBox の ID は int 型のためファイルが自動生成されないようです。uuid などを使いたい場合、 int 型の ID とは別に String 型の別の ID を用意する必要があります。調査不足ではありますが @Index@Unique の2つのアノテーションがあり String 型の ID と併用して使うのですがイマイチ理解できなかったので分かり次第、記事を更新しようと思います。

結局のところ int型の ID をインクリメントするようにして使えばいいと思います。
他にいい案があれば教えて下さい!

まとめ

Riverpod も Freezed も ObjectBox も最近、勉強し始めたもので拙い説明だったかもしれませんが最後まで読んでいただきありがとうございます。これからも情報発信は続けていけたらなと思います。引き続き Flutter大学 Advent Calendar 2022 を楽しんでいきましょう!! ありがとうございました!

サンプルアプリ

lib 配下の構成

lib/ 
   ┗ ━ ┳ ━ features/ ━ user_controller.dart
       ┠ ━ models/ ┳ ━ user.dart
       ┃           ┗ ━ user.freezed.dart /// 自動生成
       ┠ ━ pages/  ━ ━ home_page.dart
       ┗ ┳ ━ app.dart
         ┠ ━ main.dart
	 ┠ ━ objectbox-model.json /// 自動生成
	 ┗ ━ objectbox.g.dart /// 自動生成

パッケージのインストール

pubspec.yaml
name: flutter_objectbox_riverpod_sample_app
description: A new Flutter project.

publish_to: 'none' 

version: 1.0.0+1

environment:
  sdk: '>=2.18.2 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
+ freezed_annotation: ^2.2.0 
+ objectbox: ^1.6.2
+ objectbox_flutter_libs: ^1.6.2
+ flutter_riverpod: ^2.1.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
+ build_runner: ^2.0.0 
+ freezed:
+ objectbox_generator:

flutter:
  uses-material-design: true

dependencies :
freezed_annotation
objectbox
objectbox_flutter_libs
flutter_riverpod

dev_dependencies :
build_runner
freezed
objectbox_generator

main.dart

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'app.dart';
import 'objectbox.g.dart';

late Store store;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  store = await openStore();

  runApp(
    const ProviderScope(child: App()),
  );
}

app.dart

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

import 'pages/home_page.dart';

class App extends StatelessWidget {
  const App({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomePage(),
    );
  }
}

home_page.dart

home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../features/user_controller.dart';
import '../main.dart';
import '../models/user.dart';

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    /// state を監視
    final data = ref.watch(userStateNotifierProvider);

    /// userController を監視
    final notifier = ref.watch(userStateNotifierProvider.notifier);

    /// Box を取得
    final userBox = store.box<User>();

    return Scaffold(
      appBar: AppBar(
        title: const Text('ObjectBox'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(32),
              child: TextFormField(
                controller: notifier.nameController,
                decoration: const InputDecoration(hintText: 'ユーザー名'),
              ),
            ),
            ElevatedButton(
              onPressed: () => notifier.putUser(),
              child: const Text('ユーザーを追加する'),
            ),
            const SizedBox(height: 16),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: userBox
                  .getAll() 
                  .map(
                    (e) => ListTile(
                      title: Text('ID: ${e.id}, name: ${e.name}'),
                      trailing: IconButton(
                        onPressed: () {
                          notifier.removeUser(e.id);
                        },
                        icon: const Icon(Icons.delete),
                      ),
                    ),
                  )
                  .toList(),
            ),
          ],
        ),
      ),
    );
  }
}

user.dart

user.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:objectbox/objectbox.dart';

part 'user.freezed.dart';


class User with _$User {
  (realClass: User)
  factory User({
    /// @Id() アノテーションで ID であることを明示する
    /// assignable 自分で ID を割り当てる場合は true にしなければならない
    /// Freezed の場合は自動インクリメントされないので true にしないとファイルが自動生成されない
    (assignable: true) required int id,
    ('') String name,
  }) = _User;
}

user_controller.dart

user_controller.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../main.dart';
import '../models/user.dart';

final userStateNotifierProvider =
    StateNotifierProvider.autoDispose<UserStateNotifier, User>(
  ((ref) => UserStateNotifier()),
);

class UserStateNotifier extends StateNotifier<User> {
  UserStateNotifier() : super(User(id: 0));
  final nameController = TextEditingController(); 
  
  /// Box を取得
  final userBox = store.box<User>();

  
  void dispose() {
    nameController.dispose();
    super.dispose();
  }

  ///追加メソッド
  void putUser() {
    /// 保存されている ID の中で一番大きな値を取得
    /// 何も保存されてない場合は 0 を取得
    final fetchUserBoxId =
        userBox.getAll().isEmpty ? 0 : userBox.getAll().last.id;

    final newUser = User(
      /// max値 9223372036854775807 2^63
      /// 一番大きな id + 1 で常に id が被らないように実装
      id: fetchUserBoxId + 1,
      name: nameController.text,
    );

    state = state.copyWith(id: newUser.id, name: newUser.name);
    userBox.put(state);
  }

  ///削除メソッド
  void removeUser(int id) {
    state = state.copyWith(id: id);
    userBox.remove(state.id);
  }
}

参考

https://zenn.dev/pressedkonbu/articles/flutter-object-box

https://docs.objectbox.io/

Discussion