🗃️

OBJECTBOXを使ってみた

2022/12/29に公開

LocalDBに入門してみる

FlutterでLocalDBといえば、sqflite、Hiveが良く使用されているみたいですが、ObjectBoxなるものが人気が出ているようで、気になって使ってみました。
数日前から、チュートリアルをやったりしましたが、Riverpodに書き換えたら、Runボタンを押してアプリを再起動しないと画面にデータが映らないのに、悩まされました😱

多分これは、アプリのライフサイクルというものでしょうね...
公式のサンプルを見てみたのですが、Streamを型に使っていて、画面に描画するときは、Streambuiderを使っていました!
Hiveでもやっていたような?

こんなアプリ作りました

完成したソースコード

https://github.com/sakurakotubaki/ObjectboxApp

こちらが今回参考にした公式のサンプル
https://github.com/objectbox/objectbox-dart/tree/main/objectbox/example/flutter/event_management_tutorial/event_manager

こちらが公式の環境構築の方法が紹介されたページ
https://docs.objectbox.io/getting-started

  • アプリ作成で躓いたところ
    • あまり情報がない
      • どうやって対応した?
      • 公式のGithubのサンプルを動かしてみる.
      • コードの型が何かを見る.
      • 公式ドキュメントを見てqueryについて調べた.

Sier事業をやっている企業で働いたときに、手を動かす前に、考えるのが先と社長に教わりました。
最近は、このやり方が役に立っている💡
でやったことは、ドキュメントは見たけど、それだけじゃ分からないので、動く生のソースコードを使って仕組みを理解して、Riverpodに書き換えてみました。

画面に、ObjectBoxから取得したデータを追加したり削除すると、画面が変化する動作を2〜3日前は、どうすればできるのか分かりませんでしたが、Streamを使うとデータが変化すると通知する機能があるそうで、これによって、データの追加、削除をすると画面がリアルタイムに変化するのだなと、思いました!


Stream<T>Classについて

https://api.flutter.dev/flutter/dart-async/Stream-class.html
今回、Streamをどこで使いたいかというとObjectBoxのqueryを使うところです。

こちらが公式のqueryについてのドキュメント

https://docs.objectbox.io/queries

Finding objects

There are a couple of find methods to retrieve objects matching the query:
クエリにマッチするオブジェクトを取得するために、いくつかの検索メソッドがあります。
翻訳してみると、findというメソッドを使って、オブジェクトを検索することができるようです。

// クエリにマッチする全てのエンティティを返す
List<User> joes = query.find();

// 最初の結果のみを返すか、何もなければnullを返す
User joe = query.findFirst();

// 唯一の結果を返すか、無ければnullを返し、複数の結果があればthrowを投げる
User joe = query.findUnique();

Ordering results

In addition to specifying conditions, you can order the returned results using the order() method:

条件を指定するだけでなく、order() メソッドを使用して返される結果を並べ替えることもできます。

// in ascending order, ignoring case
final qBuilder = box.query(User_.firstName.equals('Joe')..order(User_.lastName);
final query = qBuilder.build();

You can also pass flags to order() to sort in descending order, to sort case sensitive or to specially treat null values. For example to sort the above results in descending order and case sensitive instead:

order() にフラグを渡すと、降順でソートしたり大文字小文字を区別したり null 値を特別に扱ったりすることができます。たとえば、上記の結果を降順で並べ替え、大文字小文字を区別して並べ替えたい場合などです。

.order(User_.lastName, flags: Order.descending | Order.caseSensitive)

ドキュメントを参考に作成した、Streamを使って、queryでidを指定してデータを画面に表示するロジックは、こんな感じなりました。

// objectboxをインスタンス化して、DBにアクセスできるようにするProvider.
final objectboxProvider = Provider((ref) => objectbox.store.box<User>());

// StreamでListを使って、Userを型にしてモデルの情報を取得するProvider
final objectStreamProvider = StreamProvider<List<User>>((ref) {
  final builder = ref.watch(objectboxProvider).query()
    ..order(User_.id, flags: Order.descending);
  return builder.watch(triggerImmediately: true).map((query) => query.find());
});

アプリを作成するときに気をつけること

モデルクラスを作った後に、自動生成されるobjectbox.g.dartとobjectbox-model.jsonは、main.dartが配置されている箇所から動かすと、ファイルを読み込めないようなので、他のディレクトリに移動させないようにする。
object_box.dartも違うディレクトリに移動させると、importができなくなるので、main.dartと同じディレクトリから移動させない。

ObjectBoxに関係したファイルを自動生成するコマンド

flutter pub run build_runner build

今回作成したアプリのコード

ディレクトリ構成は以下のようになっております
objectbox-model.jsonとobjectbox.g.dartは、自動生成されたファイルです。
object_box.dartは自分で作成しました。

lib
├── main.dart
├── model
│   └── user.dart
├── object_box.dart
├── objectbox-model.json
├── objectbox.g.dart
└── service
    └── provider.dart

pubspec.yamlに必要なpackageを追加する

ObjectBoxに関係したpackageは、versionは同じにする。
^1.7.0なら、全部同じ^1.7.0にする。

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

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=2.18.0 <3.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
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.2
  objectbox: ^1.7.0
  objectbox_flutter_libs: ^1.7.0
  flutter_riverpod: ^2.1.1


dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0
  build_runner: ^2.3.3
  objectbox_generator: ^1.7.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

まずは、modelを作成します。
リファクタリングしたときに、modelディレクトリを作成して移動させたら、コードを読み込めないエラーにハマりました!
ObjectBoxでは、idはint型にして、初期値を入れて使うようです。
idを自動生成するauto incrementなるものがあるのですけど、ObjectBoxはidを自動で生成してくれないようです。

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

// ユーザークラスを作成
()
class User {
  User({this.name});
  int id = 0;
  String? name;
}

build_runnerのコマンドを実行してファイルを自動生成する

flutter pub run build_runner build

自動生成されたファイル

objectbox.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// This code was generated by ObjectBox. To update it run the generator again:
// With a Flutter package, run `flutter pub run build_runner build`.
// With a Dart package, run `dart run build_runner build`.
// See also https://docs.objectbox.io/getting-started#generate-objectbox-code

// ignore_for_file: camel_case_types
// coverage:ignore-file

import 'dart:typed_data';

import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:objectbox/internal.dart'; // generated code can access "internal" functionality
import 'package:objectbox/objectbox.dart';
import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart';

import 'model/user.dart';

export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file

final _entities = <ModelEntity>[
  ModelEntity(
      id: const IdUid(1, 177988199567256856),
      name: 'User',
      lastPropertyId: const IdUid(3, 207964790487088115),
      flags: 0,
      properties: <ModelProperty>[
        ModelProperty(
            id: const IdUid(1, 2595935614700075911),
            name: 'id',
            type: 6,
            flags: 1),
        ModelProperty(
            id: const IdUid(2, 602322561039849242),
            name: 'name',
            type: 9,
            flags: 0)
      ],
      relations: <ModelRelation>[],
      backlinks: <ModelBacklink>[])
];

/// Open an ObjectBox store with the model declared in this file.
Future<Store> openStore(
        {String? directory,
        int? maxDBSizeInKB,
        int? fileMode,
        int? maxReaders,
        bool queriesCaseSensitiveDefault = true,
        String? macosApplicationGroup}) async =>
    Store(getObjectBoxModel(),
        directory: directory ?? (await defaultStoreDirectory()).path,
        maxDBSizeInKB: maxDBSizeInKB,
        fileMode: fileMode,
        maxReaders: maxReaders,
        queriesCaseSensitiveDefault: queriesCaseSensitiveDefault,
        macosApplicationGroup: macosApplicationGroup);

/// ObjectBox model definition, pass it to [Store] - Store(getObjectBoxModel())
ModelDefinition getObjectBoxModel() {
  final model = ModelInfo(
      entities: _entities,
      lastEntityId: const IdUid(1, 177988199567256856),
      lastIndexId: const IdUid(0, 0),
      lastRelationId: const IdUid(0, 0),
      lastSequenceId: const IdUid(0, 0),
      retiredEntityUids: const [],
      retiredIndexUids: const [],
      retiredPropertyUids: const [207964790487088115],
      retiredRelationUids: const [],
      modelVersion: 5,
      modelVersionParserMinimum: 5,
      version: 1);

  final bindings = <Type, EntityDefinition>{
    User: EntityDefinition<User>(
        model: _entities[0],
        toOneRelations: (User object) => [],
        toManyRelations: (User object) => {},
        getId: (User object) => object.id,
        setId: (User object, int id) {
          object.id = id;
        },
        objectToFB: (User object, fb.Builder fbb) {
          final nameOffset =
              object.name == null ? null : fbb.writeString(object.name!);
          fbb.startTable(4);
          fbb.addInt64(0, object.id);
          fbb.addOffset(1, nameOffset);
          fbb.finish(fbb.endTable());
          return object.id;
        },
        objectFromFB: (Store store, ByteData fbData) {
          final buffer = fb.BufferContext(fbData);
          final rootOffset = buffer.derefObject(0);

          final object = User(
              name: const fb.StringReader(asciiOptimization: true)
                  .vTableGetNullable(buffer, rootOffset, 6))
            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);

          return object;
        })
  };

  return ModelDefinition(model, bindings);
}

/// [User] entity fields to define ObjectBox queries.
class User_ {
  /// see [User.id]
  static final id = QueryIntegerProperty<User>(_entities[0].properties[0]);

  /// see [User.name]
  static final name = QueryStringProperty<User>(_entities[0].properties[1]);
}

objectbox-model.json

ObjectBoxを使用するための設定ファイル
日本語の翻訳を付け加えておきました。

object_box.dart
import 'objectbox.g.dart'; // created by `flutter pub run build_runner build`

// BoxStore(Java)またはStore(Dart)は、
// ObjectBoxを使用するためのエントリーポイントです。
// データベースへの直接のインターフェイスであり、Box を管理します。
// 通常、Store は 1 つだけ(1 つのデータベース)にして、アプリの実行中に開いておきたいものです。
class ObjectBox {
  /// The Store of this app.
  late final Store store;

  ObjectBox._create(this.store) {
    // ビルドクエリなど、追加の設定コードを追加します.
  }

  /// Create an instance of ObjectBox to use throughout the app.
  static Future<ObjectBox> create() async {
    final store = await openStore();
    return ObjectBox._create(store);
  }
}

ObjectBoxを操作するための設定が書かれたProvider

Riverpodを組み合わせてアプリを作っている人をあまり見かけなかったので、思いつきで作ってる感があります。これでいいのかと未だに思ってます😅

service/provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:object_box_app/main.dart';
import 'package:object_box_app/model/user.dart';
import 'package:object_box_app/objectbox.g.dart';

// TextEditingControllerを使うProvider
final textProvider = StateProvider.autoDispose((ref) {
  // riverpodで使うには、('')が必要
  return TextEditingController(text: '');
});
// objectboxをインスタンス化して、DBにアクセスできるようにするProvider.
final objectboxProvider = Provider((ref) => objectbox.store.box<User>());

// StreamでListを使って、Userを型にしてモデルの情報を取得するProvider
final objectStreamProvider = StreamProvider<List<User>>((ref) {
  final builder = ref.watch(objectboxProvider).query()
    ..order(User_.id, flags: Order.descending);
  return builder.watch(triggerImmediately: true).map((query) => query.find());
});

アプリを実行するmain.dart
main.dartで、ObjectBoxにアクセスするファイルを読み込み、データの保存と追加を行います。
今回は、StateNotifire使ってないです😇
なくても、メソッドを実行できるので必要ないと思いました。😅

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:object_box_app/object_box.dart';
import 'package:object_box_app/model/user.dart';
import 'package:object_box_app/service/provider.dart';

/// アプリ全体を通してObjectBox Storeにアクセスできるようにします。
late ObjectBox objectbox;

Future<void> main() async {
  // これは、ObjectBox がデータベースを格納するアプリケーション ディレクトリを取得するために必要です。
  // に格納するためです。
  WidgetsFlutterBinding.ensureInitialized();

  objectbox = await ObjectBox.create();

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Builder(
        builder: (context) {
          return const DemoPage();
        },
      ),
    );
  }
}

class DemoPage extends ConsumerWidget {
  const DemoPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final boxProvider = ref.watch(objectboxProvider);
    final controllerProvider = ref.watch(textProvider);
    final stream = ref.watch(objectStreamProvider);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blueGrey,
        title: const Text('ObjectBox'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(32),
            child: TextFormField(
              controller: controllerProvider,
              decoration: InputDecoration(hintText: 'ユーザー名'),
            ),
          ),
          ElevatedButton(
              onPressed: () {
                // Userクラスのnameプロパティにcontrollerの値を保存する.
                final user = User(name: controllerProvider.text);
                // objectboxにデータを保存する.
                boxProvider.put(user);
              },
              child: Text('ユーザーを追加する')),
          const SizedBox(height: 16),
          ElevatedButton(
              style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
              onPressed: () {
                // Userクラスを全て削除する.
                boxProvider.removeAll();
              },
              child: Text('ユーザーを全て削除する')),
          const SizedBox(height: 16),
          Expanded(
              child: stream.when(
            loading: () => const CircularProgressIndicator(),
            error: (err, stack) => Text('Error: $err'),
            data: (users) {
              return ListView.builder(
                  itemCount: users.length,
                  itemBuilder: (context, index) {
                    final user = users[index];
                    return Card(
                      child: ListTile(
                        title: Text(user.name!),
                        trailing: IconButton(
                          color: Colors.red,
                          icon: const Icon(Icons.delete),
                          onPressed: () {
                            // idで指定したユーザーを削除する.
                            boxProvider.remove(user.id);
                          },
                        ),
                      ),
                    );
                  });
            },
          ))
        ],
      ),
    );
  }
}

最後に

今回は、今までにListとか型について、理解していなかったので、コードを書いたりロジックを考えることができないのだなと改めて気付かされました!
どうやって理解したかというと、とにかく何かを作る!
このサイクルを繰り返すと、アプリを作れるようになりました。
最初は分からないので、コツコツやっていれば、どんな風に作れば自分の作りたい機能を作れるのか、分かるようになると思います。
勘違いしてはいけないのは、コードを書き写すのではなくて、理解すること!

Flutter大学

Discussion