🤖

Signals.dartでObjectBoxを使ってみた

2024/11/23に公開

What ObjectBox?

Java/Kotlin, Dart/Flutter, and Pythonで使用することができるローカルDBのことです。今回は、状態管理ライブラリであるSignals.dartを組み合わせてデモアプリを作ってみました。

https://dartsignals.dev/async/stream/
https://docs.objectbox.io/

補足情報

作ったアプリは動作確認のデモアプリなので機能はすごくシンプルです。物足りないところもあるかもしれません。

完成品

  • 実装した機能
    • 追加
    • 表示
    • 削除

必要なパッケージを追加する

公式を参考にしてください。syncって書いたパッケージは不要でした。
https://docs.objectbox.io/getting-started

flutter pub add objectbox objectbox_flutter_libs:any
flutter pub add --dev build_runner objectbox_generator:any

Or when using ObjectBox Sync instead run:

flutter pub add objectbox
flutter pub add --dev build_runner objectbox_generator:any

add path_provider

flutter pub add path_provider

add Signals.dart

flutter pub add signals

モデルを作成する。user.dartに記載。

import 'package:objectbox/objectbox.dart';

()
class User {
  ()
  int id = 0;
  
  String? name;
  
  (type: PropertyType.date) // ミリ秒単位のintとして格納
  DateTime? date;

  () // このプロパティはデータベースに保存されないので無視する。
  int? computedProperty;

  User({this.name});
}

build_runner

モデルを作成したら自動生成のコマンドを実行する。

flutter pub run build_runner watch --delete-conflicting-outputs

設定ファイルを作成する。

設定ファイル

yaml file

name: object_signals
description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: ^3.5.4

dependencies:
  flutter:
    sdk: flutter


  cupertino_icons: ^1.0.8
  objectbox: ^4.0.3
  objectbox_flutter_libs: any
  signals: ^5.5.0
  path_provider: ^2.1.5

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^4.0.0
  build_runner: ^2.4.13
  objectbox_generator: any

flutter:

  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/to/resolution-aware-images

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/to/asset-from-package

  # 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/to/font-from-package

ObjectBoxにアクセスするファイル。

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);
  }
}

こちらが作成したデモ用のコード。

全体のコード
import 'package:flutter/material.dart';
import 'package:object_signals/object_box.dart';
import 'package:object_signals/objectbox.g.dart';
import 'package:object_signals/user.dart';
import 'package:signals/signals_flutter.dart';

late ObjectBox objectbox;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  objectbox = await ObjectBox.create();

  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    final usersSignal = StreamSignal<List<User>>(() {
      final builder = objectbox.store.box<User>().query()
        ..order(User_.id, flags: Order.descending);
      return builder
          .watch(triggerImmediately: true)
          .map((query) => query.find());
    });

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Signals Objectbox'),
      ),
      body: Watch((context) {
        final state = usersSignal.value;

        // エラー処理
        if (state.hasError) {
          return const Text('Something went wrong');
        }

        // ローディング処理
        if (state.isLoading) {
          return const Text('Loading');
        }

        // データ取得成功時の処理
        final snapshot = state.value;
        if (snapshot == null) {
          return const Text('No data');
        }

        return ListView(
          children: snapshot.map((document) {
            final data = document;
            return ListTile(
              trailing: IconButton(
                  onPressed: () async {
                    final userBox = objectbox.store.box<User>();
                    userBox.remove(data.id);
                  },
                  icon: const Icon(Icons.delete)),
              title: Text(data.name ?? ''),
            );
          }).toList(),
        );
      }),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final userBox = objectbox.store.box<User>();
          final user = User(name: 'Jim');
          userBox.put(user);
          print(userBox.getAll());
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

動作はこのようになっております

FutureSignalの場合

クエリの修正が必要です。Streamではないので、watchは使用しない。

final usersSignal = FutureSignal<List<User>>(() {
      final builder = objectbox.store.box<User>().getAll();
      return Future.value(builder); // findの結果をFutureで返す
    });

こちらが全体のコード

少し修正しただけです。

FutureSignal
import 'package:flutter/material.dart';
import 'package:object_signals/object_box.dart';
import 'package:object_signals/user.dart';
import 'package:signals/signals_flutter.dart';

late ObjectBox objectbox;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  objectbox = await ObjectBox.create();

  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    final usersSignal = FutureSignal<List<User>>(() {
      final builder = objectbox.store.box<User>().getAll();
      return Future.value(builder); // findの結果をFutureで返す
    });

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Signals Objectbox'),
      ),
      body: Watch((context) {
        final state = usersSignal.value;

        // エラー処理
        if (state.hasError) {
          return const Text('Something went wrong');
        }

        // ローディング処理
        if (state.isLoading) {
          return const Text('Loading');
        }

        // データ取得成功時の処理
        final snapshot = state.value;
        if (snapshot == null) {
          return const Text('No data');
        }

        return ListView(
          children: snapshot.map((user) {
            return ListTile(
              trailing: IconButton(
                onPressed: () {
                  final userBox = objectbox.store.box<User>();
                  userBox.remove(user.id);
                  // データを再取得するためにSignalを更新
                  usersSignal.refresh();
                },
                icon: const Icon(Icons.delete),
              ),
              title: Text(user.name ?? ''),
            );
          }).toList(),
        );
      }),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final userBox = objectbox.store.box<User>();
          final user = User(name: 'Jim');
          userBox.put(user);
          // データを再取得するためにSignalを更新
          usersSignal.refresh();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

最後に

Signals.dartはまだ国内では流行っていなのでこれから布教活動をしていきたいなと思っております。最近一緒にお仕事した傭兵(フリーランスと業務委託)さんたちとFlutterのコミュニティを立ち上げました。
ゆるっと活動してるFlutterの知見を共有するコミュニティです。今の所10名かな。

ご興味ある現役Flutterエンジニアの方いたらご参加ください。🔗リンクは切れてるので招待する必要があります💦

https://zenn.dev/joo_hashi/articles/1356c09853da17

Discussion