【Flutter】supabaseとbrick併用でオフラインサポートが可能になった


# supabaseとは

supabese（スパベース）とは、Firebaseの代替Baasサービスとなります。
Firebaseのコレクション・ドキュメントの形式で管理するものとは違い、PostgreSQLを用いたRDB型のデータ管理が可能です。詳しくは別記事にて紹介しています。

https://qiita.com/kurogoma939/items/e658e52b12ddcac81938

ただし、Supabaseはどんどん成長しているため詳しくはドキュメントを確認してください。

https://supabase.com/

また、supabaseのエンジニアであるタイラーさんもブログや動画で新機能の紹介を積極的にしてくれています。

https://x.com/dshukertjrjp

# オフラインサポート（オフラインファースト）の必要性

ビジネスや趣味であっても、ユーザーは電波の届きにくい環境にいることがしばしばあります。
- 地下鉄の中
- オフィスビルの地下
- オフィスビルの高層階
- 地方の山や海

この時に、アプリを利用したいのにネットワークが遅いせいで使えなかった場合ユーザーにとってストレスになり、時には離脱の原因となってしまいます。

そのため、少し労力はありますが上記のケースに該当しそうな場合はオフライン対応を考慮しても良いと思います。

# 参考ブログ

今回、英文ですがブログで公開されているので、そこからピックアップして紹介します。

https://supabase.com/blog/offline-first-flutter-apps

# 実装

## 1. Supabaseプロジェクトのセットアップ

まずは無料枠で良いのでsupabaseプロジェクトを作成してください。
https://supabase.com/dashboard/projects

supabaseにはローカル開発の環境も整っているのですが、オフラインにする手段が面倒になってしまうため一旦リモートでプロジェクトは用意しましょう。

## 2. 必要パッケージの導入

`pubspec.yaml`に以下のパッケージを導入しましょう。
ブログに書いてあるものより追加で必要なようなのでまとめて記載します。

```yaml: pubspec.yaml
dependencies:
  brick_offline_first_with_supabase: ^1.0.0
  sqflite: ^2.3.0
  brick_sqlite: ^3.1.0
  uuid: ^3.0.4
  brick_supabase: ^1.1.0
  supabase_flutter: ^2.2.0
  sqflite_common: ^2.5.4+5

dev_dependencies:
  brick_offline_first_with_supabase_build: ^1.0.0
  build_runner: ^2.4.0
```

## 3. brick関連のディレクトリ作成

以下のコマンドでbrick関連のコードを格納するディレクトリを作成します。
また、モデル等を自動生成するためそちらのコードも格納されます。

```bash
mkdir -p lib/brick/adapters lib/brick/db;
```

## 4. モデルクラスを作成

オフライン対応したいテーブル用のモデルを作成します。
コメントにもある通り、対象ファイルは`.model.dart`という拡張子としてください。

まずはファイルを作りましょう。

```bash
mkdir lib/models && touch lib/models/note.model.dart 
```

```dart
// Your model definition can live anywhere in lib/**/* as long as it has the .model.dart suffix
// Assume this file is saved at my_app/lib/src/users/user.model.dart

import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import 'package:uuid/uuid.dart';

@ConnectOfflineFirstWithSupabase(
  supabaseConfig: SupabaseSerializable(tableName: 'memos'),
)
class Memo extends OfflineFirstWithSupabaseModel {
  // 主キーの設定
  @Supabase(unique: true)
  @Sqlite(index: true, unique: true)
  final String id;

  final String text;
  final DateTime createdAt;
  
  Memo({
    String? id,
    required this.text,
    required this.createdAt,
  }) : this.id = id ?? const Uuid().v4();
}
```

完了したらbuild_runnerで自動生成します

```bash
dart run build_runner build
```

自動生成が完了すると、`lib/brick/adapters`と`lib/brick/db`にそれぞれadapter用のクラスとマイグレーションファイルが生成されます。

## 5. Repositoryクラスの実装

こちらはブログに記載のものを流用して実装します。
主にSupabaseとBrickの機能を用いるためのものなので命名はカスタムしてください。

ではファイルの作成です。

```bash
touch lib/brick/repository.dart
```

```dart
// Saved in my_app/lib/src/brick/repository.dart
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
// This hide is for Brick's @Supabase annotation; in most cases,
// supabase_flutter **will not** be imported in application code.
import 'package:brick_supabase/brick_supabase.dart' hide Supabase;
import 'package:sqflite_common/sqlite_api.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'brick.g.dart';
import 'db/schema.g.dart';

class Repository extends OfflineFirstWithSupabaseRepository {
  static late Repository? _instance;

  Repository._({
    required super.supabaseProvider,
    required super.sqliteProvider,
    required super.migrations,
    required super.offlineRequestQueue,
    super.memoryCacheProvider,
  });

  factory Repository() => _instance!;

  static Future<void> configure(DatabaseFactory databaseFactory) async {
    final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
      databaseFactory: databaseFactory,
    );

    await Supabase.initialize(
      url: supabaseUrl,
      anonKey: supabaseAnonKey,
      httpClient: client,
    );

    final provider = SupabaseProvider(
      Supabase.instance.client,
      modelDictionary: supabaseModelDictionary,
    );

    _instance = Repository._(
      supabaseProvider: provider,
      sqliteProvider: SqliteProvider(
        'my_repository.sqlite',
        databaseFactory: databaseFactory,
        modelDictionary: sqliteModelDictionary,
      ),
      migrations: migrations,
      offlineRequestQueue: queue,
      // Specify class types that should be cached in memory
      memoryCacheProvider: MemoryCacheProvider(),
    );
  }
}
```

## 6. `main.dart`の修正

主にsupabaseの初期化処理が必要です。

```dart
import 'package:my_app/brick/repository.dart';
import 'package:sqflite/sqflite.dart' show databaseFactory;

Future<void> main() async {
  await Repository.configure(databaseFactory);
  await Repository().initialize();
  runApp(MyApp());
}
```

## 7. データのやり取りの方法

### データ取得（一覧）
```dart
await Repository().get<List<Memo>>();
```

### データ取得（単体）
```dart
await Repository().get<Memo>(query: Query.where('id', 'xxxx'));
```

### データ取得（リレーションで絞り込み）
- user_idカラムでusersテーブルとリレーション設定をしている場合
```dart
await Repository().get<Memo>(query: Query.where('user', Where.exact('id', 'xxxx'));
```

### Streamでの取得
```dart
final Stream<List<Memo>> memosStream = Repository().subscribe<Memo>();
```

### データ登録
```dart
await Repository().upsert<Memo>(
    Memo(
        text: 'xxxx',
        createdAt: DateTime.now().toIso8601String(),
    )
);
```

### データ削除
```dart
await Repository().delete<Memo>(Memoモデルを指定);
```

## 7. 動作確認

#### 1. まずは登録してみる
#### 2. PCのWi-Fiをオフにする
#### 3. 登録処理をしてみる
- ここで登録処理が成功し、FutureやStreamでの取得も成功することを確認
#### 4. supabaseダッシュボードを確認
- ここではまだsupabaseにデータがないことを確認
#### 5. オンラインにする
#### 6. 少ししたらsupabaseにもデータが入ることを確認

以上です！

既に導入済みの場合モデルの変更は面倒かもしれませんが、ぜひチャレンジしてみてください！