🍎

Flutter x Appwriteを使ってみる

2022/12/19に公開2

この記事は2022年のFlutter Advent Calendar(カレンダー1)の19日目の記事です。


普段私はFlutterでアプリを開発する際はFirebaseと組み合わせてアプリを開発するのですが、
別のBaaSの可能性を探りたいなと思い、今回はAppwriteを選んで記事にしてみます。
まだAppwriteの記事は多くないので少しでも役に立てればと思います。

今回のゴール

Authでユーザーを作成し、Databaseにユーザー情報を保存。ユーザーアイコンをStorageに保存する。

Appwriteとは

https://appwrite.io/


(公式サイトより)

Appwrite is a self-hosted backend-as-a-service platform that provides developers with all the core APIs required to build any application.

Appwriteは、あらゆるアプリケーションを構築するために必要なすべてのコアAPIを開発者に提供する、セルフホストバックエンド・アズ・ア・サービスのプラットフォームです。

for Web, Mobile & Flutter Developersと思いっきりFlutterの文字が書いてる。
そう、FlutterエンジニアのためのmBaaSともいえる!

Appwriteはセルフホスティングなので今回はDockerで環境を構築してFlutterとの接続にトライしてみます。
クラウド版はcoming soonなのでいずれFirebaseと同様にクラウドでも利用が可能になると思います。
https://appwrite.io/cloud

Appwriteの機能

提供してるサービスは下記の8種類

  • Databases ...データベース
  • Auth & Users ...認証
  • Storage ...ファイルストレージ
  • Functions ...ファンクション
  • GEO & Localization ...位置情報
  • Console ...バックエンドAPIの使用状況を視認できるコンソール
  • Privacy ...セルフホストによるプライバシー
  • Security ...バックエンドAPIのエンドツーエンドのセキュリティ

ほとんどFirebaseと変わりないかなと思います。

Appwriteのセットアップ

1. インストール

docker run -it --rm \
    --volume /var/run/docker.sock:/var/run/docker.sock \
    --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
    --entrypoint="install" \
    appwrite/appwrite:1.1.1

dockerコマンドを実行してイメージを取ってきて起動後、下記の質問が表示されるのでEnterで設定を行う(今回は全てdefaultで設定)。
Appwrite installed successfullyが表示されていればOK。

2. localhostで確認

http://localhost/login

Appwriteの画面が表示される

最初にユーザーを作成する必要があるので、Sign Upからユーザーを作成してください。

3. プロジェクト作成

Firebaseでいうところの「プロジェクトを追加」の部分です。
任意の名前でプロジェクトを作成します。

プロジェクト作成後は下記のような画面になります。

Flutterの設定

ここからFlutter側の設定に入ります。

作成するFlutter環境

zsh
% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.3.8, on macOS 12.6 21G115 darwin-arm, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.0.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.2)
[✓] VS Code (version 1.73.1)
[✓] Connected device (3 available)
[✓] HTTP Host Availability

• No issues found!

Flutterプロジェクトの設定

https://appwrite.io/docs/getting-started-for-flutter

  1. まずFlutterのプロジェクトを作ります
zsh
% flutter create flutter_appwrite
  1. Androidの設定

OAuthコールバックURLを取得するためにAndroidManifest.xmlに下記を追加

<manifest ...>
  ...
  <application ...>
    ...
    <!-- この部分のactivityを追加 -->
    <activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity" android:exported="true">
      <intent-filter android:label="flutter_web_auth_2">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="appwrite-callback-[PROJECT_ID]" />
      </intent-filter>
    </activity>
  </application>
</manifest>

[PROJECT_ID]の部分に自分の作成したAppwriteのプロジェクトIDを記述してください。
プロジェクトIDはlocalhostで起動したAppwriteの左側にSettingsメニューがありそこから拾えます。

  1. iOSの設定

iOS Deployment Targetを11.0に設定します

  1. Flutterの設定

pubspec.yamlにappwriteパッケージを入れます

dependencies:
  flutter:
    sdk: flutter
    
  ...
  
  appwrite: ^8.1.0

pub getする

zsh
% flutter pub get

SDKの初期設定

import 'package:appwrite/appwrite.dart';

void init() {
  Client client = Client();
  client
    .setEndpoint('https://localhost/v1') // AppwriteのEndpoint
    .setProject('xxxxxxxxxxxxxxxxxxxx') // プロジェクトID
    .setSelfSigned() // 自己署名SSL証明書をDevモードのみ利用する設定
  ;
}

今回利用するAppwriteの機能

今回下記の3つのサービスを利用してみたいと思います。

  • Auth
  • Databases
  • Storage

Authを実装する

認証の種類は豊富で、匿名/メール/電話番号/OAuthで認証可能です。

現在設定可能なOAuthのプロバイダ

今回はメール認証を利用したアカウント認証を実装します。
次回またAppwriteの記事を作成することがあれば上記のOAuthでどれかプロバイダを利用してみたいと思います。

1. ユーザーを作る

Accountクラスを利用して認証周りを制御します。
アカウントを作成するにはcreateメソッドを利用してアカウントを作成します。

final authNotifierProvider =
    StateNotifierProvider.autoDispose<AuthNotifier, User?>(
  (ref) => AuthNotifier()..init(),
);

class AuthNotifier extends StateNotifier<User?> {
  AuthNotifier() : super(null);

  late Client client;
  late Account account;

  ///
  /// 初期処理
  ///
  void init() {
    client = Client();
    client
        .setEndpoint('https://localhost/v1')
        .setProject('xxxxxxxxxxxxxxxxxxxx')
        .setSelfSigned();
    account = Account(client);
  }

  ///
  /// アカウント作成
  /// @param name: ユーザー名
  /// @param email: メールアドレス
  /// @param password: パスワード
  ///
  Future<void> create(String name, String email, String password) async {
    try {
      final result = await account.create(
        userId: ID.unique(),
        name: name,
        email: email,
        password: password,
      );

      state = User.fromJson(result.toMap());
      log('create: success');
      log(result.toMap().toString());
    } on AppwriteException catch (e) {
      log('create: ${e.message!}');
    }
  }

createメソッドで返ってくるAccountオブジェクト(※見やすくMap化し文字列加工してます)

{
    "$id": "638c32e777a3523682de",
    "$createdAt": "2022-12-04T05:40:55.644+00:00",
    "$updatedAt": "2022-12-04T05:40:55.644+00:00",
    "name": "test",
    "registration": "2022-12-04T05:40:55.644+00:00",
    "status": true,
    "passwordUpdate": "2022-12-04T05:40:55.644+00:00",
    "email": "test@example.com",
    "phone": "",
    "emailVerification": false,
    "phoneVerification": false,
    "prefs": {
        "data": {}
    }
}

ユーザー作成後のAppwrite側の画面

これでアカウントは作成できました。

2. ログインする

  ///
  /// ログイン
  /// @param email: メールアドレス
  /// @param password: パスワード
  ///
  Future<void> login(String email, String password) async {
    try {
      final result =
          await account.createEmailSession(email: email, password: password);
      _sessionId = result.$id;
      state = await _getAccount(); // セッションを作成したのちaccount.getしてアカウントを取得
      log('login: success');
      log(result.toMap().toString());
    } on AppwriteException catch (e) {
      log('login: ${e.message!}');
    }
  }
  
  ///
  /// アカウント取得
  ///
  Future<User?> _getAccount() async {
    try {
      final result = await account.get();
      return User.fromJson(result.toMap());
    } on AppwriteException catch (e) {
      log('_getAccount: ${e.message!}');
    }
    return null;
  }

createEmailSessionメソッドで返ってくるSessionオブジェクト(※見やすくMap化し文字列加工してます)
$idがセッションIDなのでそれを保持してログアウト時にそのIDを利用してセッションを切断します。

{
    "$id": "638c3739ea66b230b184",
    "$createdAt": "2022-12-04T05:59:21.964+00:00",
    "userId": "638c32e777a3523682de",
    "expire": "2023-12-04 05:59:21.960",
    "provider": "email",
    "providerUid": "test@example.com",
    "providerAccessToken": "",
    "providerAccessTokenExpiry": "",
    "providerRefreshToken": "",
    "ip": "172.20.0.1",
    "osCode": "IOS",
    "osName": "iOS",
    "osVersion": "16.0",
    "clientType": "browser",
    "clientCode": "MF",
    "clientName": "Mobile Safari",
    "clientVersion": "",
    "clientEngine": "WebKit",
    "clientEngineVersion": "",
    "deviceName": "smartphone",
    "deviceBrand": "Apple",
    "deviceModel": "iPhone",
    "countryCode": "--",
    "countryName": "Unknown",
    "current": true
}

3. ログアウトする

  ///
  /// ログアウト
  ///
  Future<void> logout() async {
    try {
      await account.deleteSession(sessionId: _sessionId);
      state = null;
      log('logout: success');
    } on AppwriteException catch (e) {
      log('logout: ${e.message!}');
    }
  }

あとはauthNotifierProviderを画面上から呼び出し、Userモデルを監視すれば一通りの動作は確認できます。

Databaseにユーザー情報を保存する

AppwriteのデータベースはFirestoreと同じNoSQLなのでFirestoreを利用したことある方はスムーズに導入可能かと思います。

1. データベースとコレクションを作成する

Appwriteの管理画面のDatabasesを開き、まずはデータベースを作成します。今回は「demo」というデータベースを作成します。
AppwriteのデータベースはFirestoreと違い、データベース自体を複数持てるようです。なので、v1/v2みたいにバージョン分けして利用することも難しくないかも。

データベースを作ったらCreate collectionでコレクションを作成します。

「users」というコレクションを作成すると、初期状態は下記のような状態になります。

2. コレクションに対するパーミッションを設定する

このままドキュメントを保存しようとしても下記のエラーメッセージが表示されます。

The current user is not authorized to perform the requested action.

なので、先にパーミッションを設定しておきます。
先ほど作成した「users」コレクションのSettingsを開き、Update Permissionsで属性を追加します。

何も設定してない状態

Usersでパーミッション指定した状態
今回はAll usersを選択し、Authで作成したユーザーは作成/読込/更新を許可する設定にしてます。

3. コレクションの属性を作成する

このままドキュメントを保存しようとしても下記のエラーメッセージが表示されます。
Firestoreの場合はドキュメントの中のフィールドは定義せず自由に作成可能ですが、Appwriteは属性を設定していないとドキュメントが作成できません。

Invalid document structure: Unknown attribute: "xxxxxx"

先ほど作成した「users」コレクションのAttributesを開き、Create attributeで属性を追加します。

今回は「iconUrl」をURL型で作成しておきます。
(URLを選択しても作成後はStringとなるんですが、URLのバリデーションが自動で付くので、URLでない場合はエラー扱いにしてくれます。なので空文字だとエラーとなる。)

当初ストレージに保存した後のURLをドキュメントに保存しようと計画してましたが、現状そのURLを取得する手段がなく(パッと探しきれなかった)、Uint8Listでの直接ファイルをダウンロードする手段しかなかったので、ストレージに保存した時のファイルIDを保存するように変更しました。
なので今回は「iconFileId」をString型で作成しておきます。StringはSizeが必須です。

4. IDを控える

データベースを扱うために、作成したデータベースとコレクションのIDをクライアント側から指定して書き込む必要があります。
先ほど作成したデータベースの画面でDatabase ID、コレクションの画面でCollection IDをコピーできるので文字列をコピーしておいたください。

これでやっとドキュメントを書き込める状態となりました。

5. Flutter側でコレクションにドキュメントを作成する

Databasesオブジェクトを生成し、createDocumentでドキュメントを作成します。
※サンプルのためメソッド内で完結する書き方にしてます。Databasesオブジェクトは初期化処理などで生成しておくといいかと。

  Future<void> createUser() async {
    try {
      final databases = Databases(_myClient.client);
      final document = await databases.createDocument(
        databaseId: 'データベースID',
        collectionId: 'コレクションID',
        documentId: 'ユーザーID',
        data: {
          'iconFileId': '',
        },
      );

      log('createUser: success');
      log(document.toMap().toString());
    } on AppwriteException catch (e) {
      log('createUser: ${e.message!}');
    }
  }

Storageにアイコンを保存する

1. ストレージのバケットを作成する

Appwriteの管理画面のStorageを開き、まずはバケットを作成します。今回は「demo」というバケットを作成します。
Create bucketでバケットを作成します。

「demo」というバケットを作成すると、初期状態は下記のような状態になります。

2. バケットに対するパーミッションを設定する

これも同じくそのままファイルをアップロードしようとすると下記のエラーが発生します。

The current user is not authorized to perform the requested action.

パーミッションを設定しておきます。
先ほど作成した「demo」バケットのSettingsを開き、Update Permissionsで属性を追加します。

何も設定してない状態

Usersでパーミッション指定した状態
今回はAll usersを選択し、Authで作成したユーザーは作成/読込/更新を許可する設定にしてます。

バケットの他の設定内容

Appwriteはパーミッションの他にアップロードするファイルサイズの制限や、拡張子の制限もかけられます。

3. Flutter側でバケットにファイルを保存する

Storageオブジェクトを生成し、createFileでファイルを作成します。
※サンプルのためメソッド内で完結する書き方にしてます。Storageオブジェクトは初期化処理などで生成しておくといいかと。

  ///
  /// アイコン作成
  /// @param originalIconUrl: アイコンURL
  /// @param fileName: ファイル名
  ///
  Future<void> createIcon(String originalIconUrl, String fileName) async {
    try {
      final storage = Storage(_myClient.client);
      final result = await storage.createFile(
        bucketId: 'バケットID',
        fileId: ID.unique(),
        file: InputFile(path: originalIconUrl, filename: fileName),
      );

      log('createIcon: success');
      log(result.toMap().toString());
    } on AppwriteException catch (e) {
      log('createIcon: ${e.message!}');
    }
  }

createFileメソッドで返ってくるFileオブジェクト(※見やすくMap化し文字列加工してます)
返却されるFileの中にはアップロード後のURLは存在せず、URLを取得するメソッドも用意されていない。(FirebaseのCloudStorageでいうところのgetDownloadURLがない)
後述でファイルの取得方法を記載します。
$idがファイルIDなのでそれを保持してファイルを取得しておきます。

{
    "$id": "638ca4c9c15c240932d0",
    "bucketId": "638c7607d52032cf26e9",
    "$createdAt": "2022-12-04T13:46:49.832+00:00",
    "$updatedAt": "2022-12-04T13:46:49.832+00:00",
    "$permissions": [
        read("user:638c9f78150e1a6f8295"),
        update("user:638c9f78150e1a6f8295"),
        delete("user:638c9f78150e1a6f8295")
    ],
    "name": "image_picker_4C2FD253-0E2F-4B0A-9B91-9A6A73D59E6D-38880-000008886381EE11.jpg",
    "signature": "ef44e2fb4ae035bb637e50cb6ce973bf",
    "mimeType": "image/jpeg",
    "sizeOriginal": 1857368,
    "chunksTotal": 1,
    "chunksUploaded": 1
}

4. Flutter側でバケットのファイルを取得する

Storageオブジェクトを生成し、getFileDownloadでファイルを取得します。
※サンプルのためメソッド内で完結する書き方にしてます。Storageオブジェクトは初期化処理などで生成しておくといいかと。

  ///
  /// アイコン取得
  ///
  Future<Uint8List?> getIcon() async {
    final storage = Storage(_myClient.client);
    final result = await storage.getFileDownload(
      bucketId: 'バケットID',
      fileId: 'ファイルID',
    );

    return result;
  }

気になったポイントまとめ

Auth

  • ユーザーの作成数制限やセッション制限ができるのはいい!
  • メール認証を実際にプロダクトで利用する場合はaccount.createでアカウントを作成してから、createVerificationで認証処理を行った方がいいかも。
  • 今回利用してないが、Teamsという概念がある。ユーザーをグルーピングし、そのグループに対し、ドキュメントやストレージのアクセス権限を与えられる!

Databases

  • URL型やEmail、IPなどの種類があり、自動でバリデーションしてくれる。
  • Attributesを設定してないと書き込み時にエラーを吐いてくれるので、とあるフィールドがこのドキュメントにあって、このドキュメントにはないみたいな現象は発生しないので助かる。
  • コレクションにパーミッションをつけられるがドキュメント単位でもカスタマイズしてパーミッションをつけられる。

Storage

  • 上記にも記載したがファイルアップロード後のURLが取得できないの地味に不便。
  • FirebaseCloudStorageと違いフォルダを作れない。
  • バケットにパーミッションをつけられるがファイル単位でもカスタマイズしてパーミッションをつけられる。

最後に

公式ドキュメントがFlutterでの記述方法を明記してくれてるので特につまることなく実装できた。
FirebaseだとパーミッションをCELライクな記述でセキュリティールールを記載するのに比べ、AppwriteはGUIで設定するので扱いやすい。
クラウド版が出るのを楽しみにしたい!

おまけ

FirebaseのAuthをAppwriteへマイグレートする際の方法の記事があったのでシェア。
https://dev.to/appwrite/migrate-firebase-users-to-appwrite-5aia

Discussion

Vincent GeVincent Ge

Amazing introduction to Appwrite! We love seeing Appwrite being picked up by developers all over the world.

If you need any help, join us on discord: appwrite.io/discord

If you'd like to help us by translating our readme to Japanese, we'd love that, too!

Thank you for the post <3

TakeTake

Thanks for the comment :)
I'll join the discode!
And I'll try to translate README into Japanese too!