FlutterでSupabaseを使ってみる

## はじめに

こんにちは。　Flutterで個人アプリを開発しているssと申します☺️

今回は、FlutterでSupabaseを使ってとあるWebアプリを作った際に学んだ知識や感じたことをメモがてら書いていこうと思います。

私のように普段FlutterでFirebaseを使っていて、Supabaseに興味がある人に参考になれば幸いです！

https://supabase.com/

## アプリの内容と作ろうと思ったきっかけ

とある目的で、友人と複数人(20人前後)で共用できる「タスク管理アプリ」をFlutterで開発し、スマホやPCから使いたいと考えていました。

FlutterにおいてDBといえばFirebaseのFirestoreですよね。

しかし、無料枠だけでアプリを作りたい私は

 - FirebaseのFirestoreでは読み取り回数が5万回（無料枠の上限）を超えるかもしれない
 - でもリアルタイムなタスク管理アプリが作りたい
 - (データ量が多くなければ)無料で使うことのできるSupabaeを使おう！

という発想でSupabaseを使ってWebアプリ開発を始めることとなりました。
Supabaseは無料プランでも無制限にAPIを叩けるので、Firebaseのように読み取り回数の制限にビクビクしながらタスク管理アプリを使用する必要がなくなるのが素晴らしいですね。


## アプリの全体像

このWebアプリはSupabaseをDBとして用いて作成し、HostingをFirebaseで行うことでWebアプリをスマホやPCのブラウザからアクセスできるようにします。
「FirebaseとSupabaseどっちも使うのかよ」と思われるかもしれませんが、Supabase自体にWebをホスティングする機能は調べた限りないようです(2023年9月19日現在)。VercelやNetlifyなどのホスティングができるサービスを利用するのが一般的なようですが、私は使ったことがないため使い慣れているFirebaseのホスティングサービスを利用することにします。

## データ取得方法について

DBの変更をリアルタイムでアプリのUIに反映させるためには、Firestoreを利用するときと同様に`StreamBuilder`を用います。

ここでは、Firestoreからデータを取得する場合とSupabaseからデータを取得する場合のコードを比較してみます。

### Firestore

`tasks`というコレクションから全てのドキュメントの`Stream`を取得する場合：
```
_stream = FirebaseFirestore.instance
                      .collection('tasks')
                      .snapshots();
```

### Supabase

`tasks`というテーブルから全ての行の`Stream`を取得する場合：

```
_stream = Supabase.instance.client
                .from('tasks')
                .stream(primaryKey: ["task_id"]);        
```


どちらも書き方は似ていますが、Supabaseの場合はプライマリーキーを指定する必要があります。

:::note warn
注意点

Firestoreではデフォルトでリアルタイムの更新が反映されますが、Supabaseのテーブルはデフォルトでリアルタイムに更新が反映されるわけではありません。

Supabaseのダッシュボードにおいて、リアルタイムにデータの更新を行いたいテーブルの`Database Replication`を有効にする必要があります。

参考: [Managing database replication](https://supabase.com/docs/guides/database/replication)

:::


## データ保存方法について

### Firestore

`tasks`というコレクションにドキュメントとしてデータを保存する場合：

```
 await FirebaseFirestore.instance
                      .collection('tasks')
                      .doc()
                      .set({
                   // 保存したいデータ
                  });
```

### Supabase

`tasks`というテーブルにデータを保存（テーブルに行を挿入）する場合：

```
await Supabase.instance.client
                    .from('tasks')
                    .insert({
                    // 保存したいデータ
                    })       
```

データを保存する方法はFirestoreでもSupabaseでもほとんど同じですね。

## アプリを作っていて感じたこと

### データ更新の早さ

まず、初めに気になっていたのがSupabaseでのデータ更新がどのくらいリアルタイムでアプリに反映されるかということです。

FirestoreはFirebaseの`Realtime Database`の強化版[^1]と言われているだけあって、DB上でデータ更新があった際にほとんどタイムラグなくアプリに反映されるのは当たり前だと思っていました。

一方で私がSupabaseに抱いていたイメージは「リアルタイム機能も使えるRDB」という印象だったため、アプリへの反映がFirestoreよりも目に見えて遅かったらどうしようなどと考えていました。

ところが実際に使ってみると、アプリへのデータ更新の反映スピードは体感Firestoreと変わらず想像より遥かに使いやすかったです。


[^1]:[Firebaseのドキュメント](https://firebase.google.com/docs/database/rtdb-vs-firestore?hl=ja)より


### NoSQLとRDB

普段FirestoreでNoSQLの形式でデータを扱っているため、RDBでの扱いは最初は慣れませんでした。
しかし、Supabaseのダッシュボードが非常に使いやすく、またドキュメントもかなり充実しているためすぐに慣れることができました。
ただ、今回はFirestoreで実装していたとしてもサブコレクションなどを使うほどそこまで多くのデータは扱っていないので、さらに複雑なデータを扱うアプリ作る場合は（私にとっては）難易度が上がるかもしれないという印象でした。

### 型の扱いなど

テーブル上の型を`timestamp`型としているのに、Dart側で受け取ると`String`型になっているようなことがありました。
`Datetime("FORMAT").format()`のようなメソッドを使って`String`型 → `Date`型に変換することができましたが、Firestoreで`timestamp`型のデータを取り出すときと挙動が違ったため少し困惑する部分がありました。

また、Supabaseでテーブルを作る際、カラム名を決める際に
 > Recommended to use lowercase and use an underscore to separate words e.g. column_name

とダッシュボードに書かれています。

簡単にいえば、たとえばあるデータにそのデータの作成日時を持たせたいとき、私はFirestoreで`createdAt`というフィールドに格納していましたがSupabaseのテーブルでは`created_at`というカラム名にすることが推奨されているということです。

推奨されているだけで別に従う必要はない（たぶん）とは思うのですが、愚直に従ってしまった私はデータを取得する際にカラム名を間違えているせいでデータを取り出せないという事態に陥ってしまいました。

そんなのすぐ気づけるだろうと思われるかもしれませんが、初めてデータを取得した際にはコードが悪いのかSupabase上の設定が悪いのか分からない状態で、気づくのに意外と時間がかかってしまいました。

その上で、普段Firestoreを使っている方が初めてSupabaseを利用する際には
 - データを受け取るときの**型**には慎重になる
 - **Typo**（打ち間違え）や変数名には気をつける

ことが重要だと感じました。ただこの部分は慣れたら全く気にならないかと思います。


### RLS(Row Level Security)について

Firebaseではドキュメントごとにセキュリティールールを書きますよね。それに当たる部分としてSupabaseにはRLS(Row Level Security)というものが存在します。行レベルセキュリティとも呼ばれます。

RLSはテーブルごとに適用することができ、RLSを有効にすると全ての読み取り・書き込みが**ブロック**されます。
つまり、あるテーブルにおいてRLSを有効にすると、Firestoreのセキュリティールールにおいて
```
allow read, write: if false;
```
と記述したことになります。

Firestoreでは、この記述を出発点として読み取り・書き込みを許可するルールを記述していくわけですが、RLSも同様です。
RLSを有効にした状態を出発点として、「誰に何を許可するのか」を**ポリシー**(Policy)としてテーブルに追加します。

たとえば、`tasks`というテーブルにあるデータを、ログイン済み（`authenticated`)の人が読み取り(`Select`)可能にするポリシーはSQLで次のように記述可能です。

```
create policy "ログイン済みの人はtasksテーブルのデータを読み取り可能"
on tasks for select
to authenticated
using ( true );
```

これをFirestoreのセキュリティルールに置き換えるとするならば

```
// tasksコレクションにある全てのドキュメントに対して
 allow read: if request.auth != null;
```
となります。

テーブルの読み取りについて例を挙げましたが、
- **Insert**（挿入）
- **Update**（更新）
- **Delete**（削除）

についても同じようにポリシーを作成します。

詳細については以下の公式ドキュメントより確認可能です。

https://supabase.com/docs/guides/auth/row-level-security

また、分かりやすくまとめてくださっている以下のサイトがとても参考になりました。🙏

https://zenn.dev/hrtk/scraps/a0381fa829dfab






## まとめ

今回はSupabaseのRDBをDBに使ってタスク管理アプリを作ってみました。
リアルタイムなアプリに対応できるかどうか不安でしたが、問題なくリアルタイムに動くアプリを作成することができました。

先ほども書きましたが、無料プランで無制限にAPIを呼び出せるのはかなり魅力的だと思います。

データ量に関する制約はありますが、個人利用〜小規模のアプリを作る程度であれば無料プランでも十分かと思います。

ただ、やはりFirebaseではプッシュ通知やCloud Messagingといった機能が非常に簡単に実装できるので、そのあたりの機能を使っていきたいアプリを開発する際はやはりFirebaseを使うのが簡単で便利なのかなと思います。その辺りを上手く使い分けてアプリを開発していきたいです。

次回はFlutterでGoogleスプレッドシートにデータを保存する方法について、メモがてら簡単にまとめたいと思っています！

読んでいただきありがとうございました！
間違っている点などありましたら指摘していただけると助かります。