【Flutter】ローカルデータベース Isar 入門

## はじめに

みなさんはFlutterアプリを開発するとき、データを保存するローカルデータベースパッケージは何を使用されていますでしょうか？
私の現場ではIsarというパッケージを使用しています。
※ 読み方は *イザー* だそうです。

https://pub.dev/packages/isar

今回はそんなIsarの導入方法と基本的な使い方を説明していきたいと思います。

### 記事の対象者

- Isarを使ってローカルデータベースを構築してみたい方
- Isarがどんなものかを知りたい方
- Flutterの学習を数ヶ月行った方
- riverpodの知識がある程度ある方
- build_runnerの知識がある程度ある方

### 記事を執筆時点での筆者の環境

```
[✓] Flutter (Channel stable, 3.24.1, on macOS 14.5 23F79 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.92.2)
```

### サンプルプロジェクト

![isar_first_contact.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/1b1ad177-e190-9568-15fc-fdbda3add813.gif)

Gifではちょっとわかりづらいですが、今回のアプリでは以下の機能を実装しています。
1. アプリを起動すると保存されているユーザー情報を取得して表示する
1. プラスボタンをタップするとランダムなユーザー情報を作成して保存する
1. 炎ボタンを押すと全てのユーザー情報を削除する
1. ユーザー情報が載っているリストタイルをタップすると名前(とid)以外をランダムに更新する
1. ユーザー情報が載っているリストタイルをロングタップすると対象のユーザー情報を削除する

### ソースコード

https://github.com/HaruhikoMotokawa/isar_sample/tree/feature/first_contact

※ 今回のアプリはfeature/first_contactブランチであることに注意してください。

### アーキテクチャ
リバーポッドアーキテクチャ（レイヤードアーキテクチャ）に沿って書いています。
不明な方は以下の記事をご覧ください。

https://qiita.com/Harx02663971/items/e04b059f5148c34f7a60

https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/

### 公式ドキュメント

Isarは公式ドキュメントがとても丁寧に書かれています。
今回の記事も基本的には公式ドキュメントのクイックスタートに沿って書かれています。

https://isar.dev/ja/tutorials/quickstart.html

## 1. 準備：インストール、エンティティ、ドメインモデル
### 1-1. パッケージをインストールする

公式には以下の４つをインストールするとあります。
1. iser
1. isar_flutter_libs
1. isar_generator
1. build_runner

しかし理由は後述しますが、path_providerも入れる必要があります。

https://pub.dev/packages/path_provider

よって私的には全部で５つとなり、以下のコマンでインストールします。

```zsh
flutter pub add isar isar_flutter_libs path_provider && flutter pub add -d isar_generator build_runner
```

また、riverpod関連も入れてます。

<details><summary>pubcpec.yamlの全体はこちら</summary>

コメントアウトの部分は未使用です😇

```pubcpec.yaml
dependencies:
  cupertino_icons: ^1.0.2
  derry: ^1.5.0
  # dio: ^5.4.3+1
  # envied: ^0.5.4+1
  flutter:
    sdk: flutter
  flutter_hooks:
  flutter_riverpod:
  # flutter_appauth: ^6.0.6
  # flutter_secure_storage: ^4.2.1
  # freezed_annotation: ^2.4.1
  gap: ^3.0.1
  # go_router: ^14.1.0
  hooks_riverpod:
  isar:
  # json_annotation: ^4.9.0
  isar_flutter_libs:
  logger: ^2.2.0
  # mockito: ^5.4.4
  path_provider:
  riverpod_annotation:
  # shared_preferences: ^2.2.3
  # url_launcher: ^6.2.6
  # uuid: ^4.4.0
  very_good_analysis: ^5.1.0

dev_dependencies:
  build_runner:
  # envied_generator:
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter
  isar_generator:
  # freezed: ^2.5.2
  # go_router_builder: ^2.6.2
  # json_serializable: ^6.8.0
  riverpod_generator:
  riverpod_lint:
```
</details>

### 1-2. コレクション（エンティティ）を定義する

```lib/domains/entities/user_entity.dart
import 'package:isar/isar.dart';
import 'package:isar_sample/domains/home_town.dart';

part 'user_entity.g.dart'; // Isarコードジェネレーターが生成するファイル

/// Isarで扱うユーザーの情報の型
// Isarのエンティティクラスには、@collectionをつける
@collection
// @nameをつけることで、Isarのコレクション名を指定できる
@Name('User')
class UserEntity {
  // IsarのautoIncrementを使うことで、自動採番を行うことができる
  Id id = Isar.autoIncrement;
  late String name;
  late int age;
  late bool isDrinkingAlcohol;
  // EnumTypeを指定することで、列挙型を保存できる
  @Enumerated(EnumType.name)
  late HomeTown homeTown;
}
```
```lib/domains/home_town.dart
/// ユーザーの出身地
enum HomeTown {
  Tokyo,
  Osaka,
  Kyoto,
  Sapporo,
  Fukuoka,
  Sendai,
}
```
Isarで扱う情報の型を定義します。

**`@collection`アノテーション**
まずルールとして`@collection`アノテーションをつけます。
これによって後に行う自動生成を行えます。

**`@Name`アノテーション**
データベース上で別名を与えたい場合につけます。
これはフィールドにも適用できます。
用途としては、データベース内でフィールドの名前をよりわかりやすくするためや、既存のデータベーススキーマとの互換性を保つために、コード上のフィールド名とデータベース上のカラム名を別にしたい場合などが考えられます。

**`Id`を定義**
オブジェクトを識別するための一意の値です。
Isarではこれを`Id`という型で指定しますが、これは`int`をラップしています。
基本的にIsarではIdについては`int`のみ対応しているようです。
直接値を指定できますが、`Isar.autoIncrement`で自動的に割り振ってくれるのでそちらを使用しましょう。

:::note
`String`型のID、例えばUUIDなどが当たりますがそれらは基本サポートされていません。
しかし、その解決策は用意されています。
今回は説明を割愛しますので詳しくは公式をご覧ください。

https://isar.dev/ja/recipes/string_ids.html

:::

**保存できる型**

Isarに保存できる型は以下の通りです。

- bool
- byte
- short
- int
- float
- double
- DateTime
- String
- List<bool>
- List<byte>
- List<short>
- List<int>
- List<float>
- List<double>
- List<DateTime>
- List<String>

上記に加えて埋め込み型オブジェクトと列挙型(Enum)もサポートされています。

`enum`を保存する場合には'@Enumerated'アノテーションをつけます。
そして引数に`Isar`がディスク上でどのように`enum`を表すかを選択する必要があります。
今回は列挙名称を`String`として格納する`EnumType.name`を選択しています。

埋め込みオブジェクトやenumの保存方法の詳細については公式をご覧ください。

https://isar.dev/ja/schema.html

### 1-3. コードの自動生成を実行する

エンティティの定義ができたらビルドランナーをターミナルで実行してコードの自動生成を行います。

```zsh
flutter pub run build_runner build --delete-conflicting-outputs
```

:::note warn
エンティティの内容を変更したらその都度ビルドランナーを実行しないと、反映されません。
:::

すると自動で以下のようなコードが生成されます。

```lib/domains/entities/user_entity.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user_entity.dart';

// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************

// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types

extension GetUserEntityCollection on Isar {
  IsarCollection<UserEntity> get userEntitys => this.collection();
}

const UserEntitySchema = CollectionSchema(
  name: r'User',
  id: -7838171048429979076,
  properties: {
    r'age': PropertySchema(
      id: 0,
      name: r'age',
      type: IsarType.long,
    ),
    r'homeTown': PropertySchema(
      id: 1,
      name: r'homeTown',
      type: IsarType.string,
      enumMap: _UserEntityhomeTownEnumValueMap,
    ),
    r'isDrinkingAlcohol': PropertySchema(
      id: 2,
      name: r'isDrinkingAlcohol',
      type: IsarType.bool,
    ),
    r'name': PropertySchema(
      id: 3,
      name: r'name',
      type: IsarType.string,
    )
  },
  estimateSize: _userEntityEstimateSize,
  serialize: _userEntitySerialize,
  deserialize: _userEntityDeserialize,
  deserializeProp: _userEntityDeserializeProp,
  idName: r'id',
  indexes: {},
  links: {},
  embeddedSchemas: {},
  getId: _userEntityGetId,
  getLinks: _userEntityGetLinks,
  attach: _userEntityAttach,
  version: '3.1.0+1',
);

 // 大量なので以下省略
```

この自動生成されたコードの中に作成、保存、取得、削除のほかにデータの並び替えやフィルターなどのメソッドが生成されています。

### 1-4. ドメインモデルを定義する
```lib/domains/user.dart
/// アプリ上で扱うユーザーの情報の型
/// 正確にはアプリケーション層とプレゼンテーション層で扱うユーザーの情報の型
class User {
  User({
    required this.name,
    required this.age,
    required this.isDrinkingAlcohol,
    required this.homeTown,
    this.id,
  });

  /// ランダムな値でUserオブジェクトを生成するファクトリメソッド
  User.random()
      : name = String.fromCharCodes(
          List.generate(5, (_) => Random().nextInt(26) + 65),
        ),
        age = Random().nextInt(43) + 18,
        isDrinkingAlcohol = Random().nextBool(),
        homeTown = HomeTown.values[Random().nextInt(HomeTown.values.length)],
        id = null;

  final int? id;
  final String name;
  final int age;
  final bool isDrinkingAlcohol;
  final HomeTown homeTown;

  /// ユーザー情報の一部を変更して新しいUserオブジェクトを作成するメソッド
  /// 指定されたプロパティのみが更新され、指定されていないプロパティは元の値が引き継がれる
  User copyWith({
    int? id,
    String? name,
    int? age,
    bool? isDrinkingAlcohol,
    HomeTown? homeTown,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      age: age ?? this.age,
      isDrinkingAlcohol: isDrinkingAlcohol ?? this.isDrinkingAlcohol,
      homeTown: homeTown ?? this.homeTown,
    );
  }

  /// ランダムな値で指定されたユーザー情報を更新するメソッド
  /// IDと名前はそのままに、他のプロパティをランダムに更新します
  User createUpdatedUser() {
    final randomUser = User.random();
    return copyWith(
      age: randomUser.age,
      isDrinkingAlcohol: randomUser.isDrinkingAlcohol,
      homeTown: randomUser.homeTown,
    );
  }
}
```

先ほど定義したUserEntityはあくまでデータベース上での型です。
そこからアプリ上で使う型に変換するのでその型を定義しています。

本来であればユーザーを新規作成する際にはユーザーが入力するものですが、
今回は「ランダムに作る」という仕様ですので、そのランダムなユーザーを作る関数はファクトリメソッドとしてここに定義しています、

## 2. Isarのインスタンス生成

次にアプリでIsarを使えるように最初にインスタンス化して設定する処理を書いていきます。

### 2-1. リバーポッドでインスタンス生成を定義

Isarのインスタンス定義はdata層の`local_sources`に定義していきます。

```lib/data/local_sources/isar.dart
/// Isarデータベースのインスタンスを提供するプロバイダー。
/// このプロバイダーは`keepAlive`が`true`に設定されており、アプリケーションのライフサイクル全体で
/// Isarインスタンスを保持します。
///
/// 戻り値:
/// Isarインスタンスを非同期で返します。
@Riverpod(keepAlive: true)
Future<Isar> isar(IsarRef ref) async {
  // アプリケーションのドキュメントディレクトリを取得
  final appDocumentDir = await getApplicationDocumentsDirectory();
  // データベースファイルのディレクトリパスを取得
  final dbPath = appDocumentDir.path;

  return openIsar(dbPath);
}

/// 指定されたディレクトリパスにIsarデータベースを開きIsarインスタンスを返す
///
/// [dbPath] - データベースファイルのディレクトリパス。
/// [name] - （オプション）Isarインスタンスの名前。デフォルトは[Isar.defaultName]。
///
/// 戻り値:
/// Isarインスタンスを非同期で返します。
Future<Isar> openIsar(String dbPath, {String name = Isar.defaultName}) async {
  return Isar.open(
    [UserEntitySchema], // 使用するIsarスキーマを指定
    directory: dbPath, // データベースのディレクトリパスを指定
    name: name, // データベースの名前を指定（デフォルトはIsarのデフォルト名）
  );
}
```

ここで設定しているのは`Isar`で作ったデータをデバイスのどこに保存するかを設定しています。
ここで必要になってくるのが`path_provider`です。
```dart
  // アプリケーションのドキュメントディレクトリを取得
  final appDocumentDir = await getApplicationDocumentsDirectory();
  // データベースファイルのディレクトリパスを取得
  final dbPath = appDocumentDir.path;
```
デバイスにアプリがインストールされると、デバイス上でそのアプリが使うデータを保存する場所（ディレクトリ）が作成されます。
そのディレクトリの場所を示すパスを取得して、そのパスにIsarを保存するように設定しています。

また、今回はUser情報だけですが、今後保存するものが増えればここに追加していく必要があります。
```dart
// 例えばこんな感じ
Future<Isar> openIsar(String dbPath, {String name = Isar.defaultName}) async {
  return Isar.open(
    [UserEntitySchema, HogeEntitySchema, HugaEntitySchema], 
```

### 2-2. 初期化画面でIsarをインスタンス化する

```lib/app.dart
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: AppStartupScreen(
        onLoaded: (context) => const HomeScreen(),
      ),
    );
  }
}
```

```lib/presentations/app_start_up/app_start_up_screen.dart
/// 初期化を待つための画面
class AppStartupScreen extends ConsumerWidget {
  const AppStartupScreen({required this.onLoaded, super.key});

  final WidgetBuilder onLoaded;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appStartupState = ref.watch(appStartupProvider);
    return switch (appStartupState) {
      AsyncData() => onLoaded(context),
      AsyncError(:final error) => _AppStartUpError(error: error),
      _ => const _AppStartUpLoading(),
    };
  }
}
```
```lib/presentations/app_start_up/provider.dart
@Riverpod(keepAlive: true)
Future<void> appStartup(AppStartupRef ref) async {
  // アプリ起動前に初期化したい処理を書く
  await ref.read(isarProvider.future);
}
```

アプリ起動直後に`AppStartupScreen`が表示され、その中で`appStartupProvider`を`watch`しています。
`appStartupProvider`はアプリの初期化処理を担当するプロバイダーで、必要な初期化処理が行われます。

具体的には`isarProvider`を呼び出しているため、アプリの起動時に`Isar`データベースがインスタンス化されデータベースの準備が整います。
これによりデータベース操作が可能な状態でアプリケーションの主要画面（HomeScreenなど）が表示されます。

## 3. CRUD処理の作成

ここからはIsarを使ってデータを作成、読み込み、更新、削除の処理を定義していきます。
定義場所は`data`層の`repository`に定義していきます。
今回のアプリの仕様を満たすための機能は以下の４つです。
```lib/data/repositories/user_repository/repository.dart
abstract interface class UserRepositoryBase {
  /// 全てのUser情報を検索する
  Future<List<User>> findAll();

  /// Userを保存する
  Future<void> save(User user);

  /// Userを削除する
  Future<void> delete(int userId);

  /// 全てのUserを削除する
  Future<void> deleteAll();
}
```
### 3-1. 準備：モデルの変換
```lib/data/repositories/user_repository/repository.dart

extension UserEntityMapper on UserEntity {
  /// UserEntityからUserへの変換
  User toDomain() {
    return User(
      id: id,
      name: name,
      age: age,
      isDrinkingAlcohol: isDrinkingAlcohol,
      homeTown: homeTown,
    );
  }
}

extension UserMapper on User {
  /// UserからUserEntityへの変換
  UserEntity toEntity() {
    return UserEntity()
      ..id = id ?? Isar.autoIncrement
      ..name = name
      ..age = age
      ..isDrinkingAlcohol = isDrinkingAlcohol
      ..homeTown = homeTown;
  }
}
```

データを受け渡す際にそれぞれのモデルに変換するメソッドを定義しておきます。
`toDomain`はIsarから取り出した情報をpresentation層やapplication層で使うための変換なので、主に情報を読み込む際に使用します。
一方`toEntity`は逆にdata層で使うための変換なので作成、更新、削除の際に使用します。

:::note
`toEntity`のidはnullだった場合に`Isar.autoIncrement`で自動的に割り振られるようになっています。
ここが後の保存の処理に関係してきます。
:::

### 3-2. 読み込み
```lib/data/repositories/user_repository/repository.dart
  final ProviderRef<dynamic> ref;
 
  @override
  Future<List<User>> findAll() async {
    final isar = await ref.read(isarProvider.future);
    final userEntitys = await isar.userEntitys.where().findAll();
    return userEntitys.map((entity) => entity.toDomain()).toList();
  }
```
Userの情報には`Isar`データベースのインスタンスに対して`userEntitys`を通してアクセスできます。
これは自動生成されたコードによって可能になっています。

`userEntitys`プロパティを使用して`UserEntity`のコレクションにアクセスします。
その後、`.where()`メソッドで条件を指定を宣言し、`.findAll()`で全件取得が可能です。
取得したエンティティのリストを`toDomain()`メソッドを使ってドメインモデルであるUserオブジェクトに変換し、返却しています。

:::note
残念ながらIsarの自動生成コードでは、`Entities`ではなく`Entitys`という誤った複数形が使用されます😰
これはスペルチェックツールなどで指摘されることがありますが、自動生成されたコードを手動で修正することは推奨されません。
この場合、スペルチェックの設定ファイルに`project-words`として例外を登録しておくと良いでしょう。
:::

### 3-3. 新規作成と更新 = 保存
```lib/data/repositories/user_repository/repository.dart
  @override
  Future<void> save(User user) async {
    final isar = await ref.read(isarProvider.future);
    final userEntity = user.toEntity();
    await isar.writeTxn(() async {
      await isar.userEntitys.put(userEntity);
    });
  }
```

保存するする内容を定義した`User`のドメインモデルを引数で受け取ります。
このモデルのIdの有無によって新規作成なのか、はたまた更新なのか挙動が変わります。

まず受け取った`User`を`Isar`で扱えるようにするため`toEntity()`メソッドで変化します。

次に変換したエンティティを書き込むみます。
書き込み処理を行う場合は`isar.writeTxn`というトランザクションの中で行います。
Isarデータベースの中にデータを送る、という意味合いで書き込みは`.put()`メソッドで行います。
`.put()`メソッドの引数はUserEntity型です。
ここでエンティティのIdがコレクション内に存在しないIdだった場合は新規作成となります。
逆にエンティティのIdがコレクション内に存在する場合は更新となります。

つまり、**Isarでは新規作成と上書き更新ともに`.put()`メソッドで行います。**

### 3-3. 削除
```lib/data/repositories/user_repository/repository.dart
  @override
  Future<void> delete(int userId) async {
    final isar = await ref.read(isarProvider.future);
    await isar.writeTxn(() async {
      await isar.userEntitys.delete(userId);
    });
  }
```

削除する場合は削除対象のエンティティの`id`さえ分かればいいので、引数でidを受け取ります。
保存の時と同じように`isar.writeTxn`のトランザクション内で処理を実行します。
`userEntitys`コレクションに対して`delete(userId)`メソッドを実行します。
この時引数に入れている`userId`に対して削除処理を実行しています。

### 3-3. 全て削除
```lib/data/repositories/user_repository/repository.dart
  @override
  Future<void> deleteAll() async {
    final isar = await ref.read(isarProvider.future);
    await isar.writeTxn(() async {
      await isar.userEntitys.clear();
    });
  }
```

全てのUserを削除するのは簡単です。
`userEntitys`コレクションに対して`.clear()`メソッドを実行するだけです。

## 4. プレゼンテーション層で呼び出す
あとは画面で必要な処理を実装すれば完了です。
まずは`ViewModel`でそれぞれ必要な処理を`UserRepository`を通して呼び出します。

```lib/presentations/home_screen/home_view_model.dart
/// ホーム画面のViewModel
@riverpod
class HomeViewModel extends _$HomeViewModel {
  @override
  void build() {}

  /// ユーザー情報の作成と取得
  Future<List<User>> createAndFetchUser() async {
    final user = User.random();
    await ref.read(userRepositoryProvider).save(user);
    return fetchAllUsers();
  }

  /// ユーザー情報の取得
  Future<List<User>> fetchAllUsers() async =>
      ref.read(userRepositoryProvider).findAll();

  /// ユーザー情報の更新と取得
  ///
  /// 更新内容はidとname以外の項目をランダムに更新する
  Future<List<User>> updateAndFetchUser(User user) async {
    final updatedUser = user.createUpdatedUser();
    await ref.read(userRepositoryProvider).save(updatedUser);
    return fetchAllUsers();
  }

  /// ユーザー情報の削除と取得
  Future<List<User>> deleteAndFetchUser(User user) async {
    final userId = user.id;
    if (userId == null) return fetchAllUsers();
    await ref.read(userRepositoryProvider).delete(userId);
    return fetchAllUsers();
  }

  /// ユーザー情報の初期化
  Future<List<User>> initUser() async {
    await ref.read(userRepositoryProvider).deleteAll();
    return [];
  }
}
```

:::note
画面に表示するユーザー情報の取得について
本来であればユーザー情報に変更があるたびに`Stream`などで受け取るのが一般的です。
今回はあくまで入門編ということで、各変更処理の最後にユーザー情報を取得して返すようにしています。
:::
あとはScreenでViewModelを呼び出せば実装は完了です。
長いので折りたたんでおきます
<details><summary>HomeScreen全体はちら</summary>

- 取得したユーザー情報はuseStateで管理
- 画面が表示された時にuseEffectで初回表示のためにユーザー情報を取得
- それぞれのWigetのアクションは`extension on`にてプライベートメソッドで定義

```lib/presentations/home_screen/home_screen.dart
class HomeScreen extends HookConsumerWidget {
  const HomeScreen({
    super.key,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final viewModel = ref.watch(homeViewModelProvider.notifier);

    final users = useState<List<User>>([]);

    useEffect(
      () {
        viewModel.fetchAllUsers().then((fetchUsers) {
          users.value = fetchUsers;
        });
        return null;
      },
      [],
    );
    return Scaffold(
      appBar: AppBar(
        title: const Text('isar_sample'),
        backgroundColor: Colors.lightBlueAccent,
      ),
      body: ListView.builder(
        itemCount: users.value.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            decoration: BoxDecoration(
              border: Border(
                top: const BorderSide(color: Colors.grey),
                bottom: index == users.value.length - 1
                    ? const BorderSide(color: Colors.grey)
                    : BorderSide.none,
              ),
            ),
            child: _UserListTile(
              user: users.value[index],
              onTap: () => updateUserAction(
                context,
                viewModel,
                users,
                users.value[index],
              ),
              onLongPress: () => deleteUserAction(
                context,
                viewModel,
                users,
                users.value[index],
              ),
            ),
          );
        },
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => initUsersAction(context, viewModel, users),
            backgroundColor: Colors.lightBlueAccent,
            child: const Icon(Icons.local_fire_department),
          ),
          const Gap(10),
          FloatingActionButton(
            onPressed: () => createUserAction(context, viewModel, users),
            backgroundColor: Colors.lightBlueAccent,
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}

extension on HomeScreen {
  /// ユーザーを追加するアクション
  Future<void> createUserAction(
    BuildContext context,
    HomeViewModel viewModel,
    ValueNotifier<List<User>> users,
  ) async {
    users.value = await viewModel.createAndFetchUser();
    if (!context.mounted) return;
    showSnackBar(context, 'ユーザーを新規作成しました');
  }

  /// ユーザーデータを初期化するアクショ
  Future<void> initUsersAction(
    BuildContext context,
    HomeViewModel viewModel,
    ValueNotifier<List<User>> users,
  ) async {
    users.value = await viewModel.initUser();
    if (!context.mounted) return;
    showSnackBar(context, '全てのユーザーを削除しました');
  }

  /// ユーザー情報を更新するアクション
  Future<void> updateUserAction(
    BuildContext context,
    HomeViewModel viewModel,
    ValueNotifier<List<User>> users,
    User updateUser,
  ) async {
    users.value = await viewModel.updateAndFetchUser(updateUser);
    if (!context.mounted) return;
    showSnackBar(context, '${updateUser.name}を更新しました');
  }

  /// ユーザー情報を削除する処理
  Future<void> deleteUserAction(
    BuildContext context,
    HomeViewModel viewModel,
    ValueNotifier<List<User>> users,
    User deleteUser,
  ) async {
    users.value = await viewModel.deleteAndFetchUser(deleteUser);
    if (!context.mounted) return;
    showSnackBar(context, '${deleteUser.name}を削除しました');
  }

  /// SnackBarを表示する
  void showSnackBar(BuildContext context, String content) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(content),
      ),
    );
  }
}

class _UserListTile extends StatelessWidget {
  const _UserListTile({
    required this.user,
    required this.onTap,
    required this.onLongPress,
  });

  final User user;

  final VoidCallback onTap;
  final VoidCallback onLongPress;
  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: const Icon(Icons.person),
      title: Text(user.name),
      subtitle: Text('Age: ${user.age}, Hometown: ${user.homeTown.name}'),
      trailing: Icon(
        user.isDrinkingAlcohol ? Icons.local_bar : Icons.no_drinks,
      ),
      onTap: onTap,
      onLongPress: onLongPress,
      tileColor: switch (user.homeTown) {
        HomeTown.Fukuoka => Colors.red,
        HomeTown.Osaka => Colors.brown,
        HomeTown.Tokyo => Colors.green,
        HomeTown.Kyoto => Colors.yellow,
        HomeTown.Sapporo => Colors.purple,
        HomeTown.Sendai => Colors.orange,
      },
    );
  }
}
```
</details>

## 5. 便利な機能「Isar Inspector」

ブラウザ上でアプリ内のIsarデータベースにアクセスして情報の表示はもちろん、操作が行えるという便利機能があります。

アプリをビルドすると、デバックコンソールに以下のような文字が印刷されます。

![スクリーンショット 2024-08-25 20.54.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/f124d49c-ee2d-24bc-ae13-07050d2d1abb.png)
赤枠の部分のURLをコマンドクリックするとブラウザで*Isar Inspector*を起動することができます。

:::note
ブラウザはChromeのみ対応しています。
デフォルトのブラウザがSafariの方はChromeにURLをコピペしましょう
:::

すると以下のように現在アプリに保存されているデータを確認することができます。
便利ですね😳

![スクリーンショット 2024-08-25 20.56.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/d9f36835-1f04-bfa0-ada8-1c2e7e5e0c4c.png)

検索したり並び替えられるのはもちろんですが、なんと新規作成、更新、削除などの操作もできます。
試しに新規で作成してみます。

![スクリーンショット 2024-08-25 21.16.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/814cc27b-79fa-7f25-32c2-a9ee34aa496d.png)

プラスボタンをタップして赤枠の部分を入れてみました。
※ nameを"はるさん"で入れみましたが、日本語はうまくいきませんでした

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/5d34ed24-7320-c4ca-e3df-65ec5cfa7371.png">

アプリにもしっかり反映されてます。

用途としては以下のようになりそうです。
- データを変更する際に変更できているか確認する
- サンプルデータを手動で入れてみる
- 作りすぎたデータを削除する

## 終わりに

今回の記事ではIsarの導入方法から基本的なCRUD操作、さらにはIsar Inspectorを使ったデータの確認・操作方法まで、FlutterアプリでIsarを利用する際の流れを一通りご紹介しました。

ローカルデータベースは非常に奥が深く、そして一番設計していて楽しい領域でもあります。
今回ご紹介したIsarの機能はまだほんの一部です。
今後も現場での経験と学習を積み重ねて、Isarの使い方をご紹介できればと思っています。

この記事がFlutter学習者、データベースの選定に迷っている方のお役に立てれば幸いです。