Flutter x Appwriteを使ってみる
この記事は2022年のFlutter Advent Calendar(カレンダー1)の19日目の記事です。
普段私はFlutterでアプリを開発する際はFirebaseと組み合わせてアプリを開発するのですが、
別のBaaSの可能性を探りたいなと思い、今回はAppwriteを選んで記事にしてみます。
まだAppwriteの記事は多くないので少しでも役に立てればと思います。
今回のゴール
Authでユーザーを作成し、Databaseにユーザー情報を保存。ユーザーアイコンをStorageに保存する。
Appwriteとは
(公式サイトより)
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と同様にクラウドでも利用が可能になると思います。
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環境
% 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プロジェクトの設定
- まずFlutterのプロジェクトを作ります
% flutter create flutter_appwrite
- 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メニューがありそこから拾えます。
- iOSの設定
iOS Deployment Targetを11.0
に設定します
- Flutterの設定
pubspec.yamlにappwrite
パッケージを入れます
dependencies:
flutter:
sdk: flutter
...
appwrite: ^8.1.0
pub get
する
% 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へマイグレートする際の方法の記事があったのでシェア。
Discussion
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
Thanks for the comment :)
I'll join the discode!
And I'll try to translate README into Japanese too!