クラブハウスみたいなアプリをFlutter Riverpodを使って作ろう (Part 1 / 2)
注: 2021年7月19日コードを修正後記事を対応させるために修正しました。
注2: 2021年7月19日現在Riverpodバージョン1が開発中ですのでこの記事で使用したRiverpod APIは変更となる可能性が高いです。
注3: 2021年8月10日FirebaseAuthについての解説追加しました。
注4: 2021年8月10日Part 2からリンクするために見出しを一部変更しました。
Clubhouse日本では彗星の如く現れ大はやりしそしてあっという間に下火になっていったようですね。
ところがどっこいアメリカでは日本ほど一気に広がることもなかったのですが堅調に利用者数も推移していますしアプリを開くと様々なルームがひしめき合ってます。
今回はそんなClubhouseみたいなアプリをFlutterで、更には最新のステート管理アーキテクチャーであるRiverpodを使って作ってみたいと思います。
せっかちな方はこちらのGithubリポジトリーにあるコードをまずご参照ください。READMEのやり方を辿ればコードをクローンしアプリをビルドして使える状態にできるはずです。
長くなるのでPart 1, 2に別れます。
Part 1 - ログイン、オンボーディング実装
Part 2 - Firestore、Agoraとの連携
今回Part 1ではログイン、オンボーディングの実装をFirebaseAuthとRiverpodを使いどう実装するかを解説したいと思います。
アプリについて
アプリはユーザーがログインして数あるルームの中から一つを選びそこに入って誰が参加してるかを見極めルームに参加するというものです。参加した後は音声のチャットが共有されます。クラブハウスには当然ながら他にもいろんな機能がありますがプロフィールと音声共有というコアの部分はこのアプリで実装されます。
認証はFirebaseAuth、データベースとしてはFirestore、音声の共有にはAgoraを使います。
以下がアプリの主な機能を表したビデオです。
ご覧の通り現在あるルームのリストが表示され(ビデオでは一つ)そこに入ると誰が現在そのルームにいるのか閲覧できます。参加すると音声を共有できます。参加中ならルームから退出できます。
プロフィールページも実装されていて名前や画像、自身の説明を変えることができます。他人のアイコンをタップすると他人の詳細を閲覧できます。(当然ながら変更はできません。)
基本のアーキテクチャー
この記事はRiverpodの使い方記事なので基本的にはRiverpodとStreamを使い倒します。考え方としては常に値が変わりうる返り値をウォッチしてさらにそれらを使ったProviderをその上に作りその上にview modelを作り最終的にはview modelが変わるたびにwidgetを再描画するというものです。
レポジトリーはappのディレクトリー内は画面ごとに別れています。画面はUIとViewModelと利用されるWidgetによって構成されます。画面遷移は全てnamed routeで行っています。Flutterではウェブも実装できるので将来ブラウザーに拡張時のurl表示のため基本的には全ての画面遷移はnamed routeを使った方がいいでしょう。
他に多画面により使用されるmodel、 widget、 constantなどはそれぞれディレクトリーを作成しました。
オンボーディングとログイン機能の実装
この部分ではホーム画面描写前のロジックを作成します。
ホームの画面はRoomsFeed画面になります。(レポジトリーでのlib/app/home/rooms_feed.dart)
そこに至る分岐は以下の図のようになります。
ユーザーがログイン状態ならホームを表示(右の枝)そうでなければログイン、オンボーディングなどの処理を行います。(左の枝)
オンボーディング機能実装
プロバイダー作成
まず必要なのはアプリ内のどこからでもログイン状態やユーザーオブジェクトを参照できることです。ユーザーオブジェクトは名前やプロフィール画像はもちろんのことオンボーディングを終了したかも大事な情報です。
Riverpodよろしくアプリのどこからでも参照できるプロバイダーを作ります。
authStateChangesProviderはStreamProviderであるので内包されたFirAuth.Userが変わるごとにそれをウォッチしてるwidgetにそれを通知します。例えばアプリが立ち上がったときはFirAuth.Userはnullですがログインした状態でアプリを終了したのであればすぐにオブジェクトが生成されます。生成後にウォッチしてるwidgetに通知が行き再ビルドがなされます。(下記のAuthWidget)
オンボーディングが終わったかどうかを参照できるプロバイダーを作ります。オンボーディング終了情報はshared_preferencesを用いデバイスに書き込みます。
そのためにはまずshared_preferencesを参照できるプロバイダーが必要です。
さてここで問題なのはshared_preferencesを使えるためにはメソッドチャンネルを使ってデバイスの機能を使うためawaitを使う必要があることです。ということでプロバイダーの宣言時にはshared_preferencesは使えません。そのせいでNotImplementedになっています。
sharedPreferencesServiceProviderはmain.dartでSharedPreferencesのインスタンスを実際に作成した後上書きします。
表示画面分岐を実装
ログイン状況のラッパーとしてAuthWidgetを作り中でtop_level_providers.dartで作られたauthStateChangesProviderをウォッチします。
main.dartでログインしてない場合,ログインしている場合の画面を作成方法をAuthWidgetに指示します。
未ログインならばオンボーディング終了したかをチェックし終了していたらログイン画面、そうでなければオンボーディング画面。ログインしていたらメインのRoomsFeed画面に行きます。そのロジックはコールバックとしてAuthWidgetに渡されます。
次は個々の画面をさらに深く掘り下げてきます。
オンボーディング、ログイン機能実装にあたってはCode with AndreaのRiverpodチュートリアルを参考にしました。
オンボーディング用ViewModel
最近のアプリは複雑なのが増えているので初めて開くときにユーザー向けにアプリの説明みたいなことをするアプリが増えています。このアプリもそれに漏れなくオンボーディング画面を出します。(空の画面だけで実際の説明はありませんが...)
sharedPreferencesServiceProviderが作成されるようになったのでそれを用いオンボーディング用のview modelを作ることができます。
view modelにはオンボーディングを終了できる機能を実装します。
オンボーディング終了情報はデバイスから読み込むわけですのでアプリが立ち上がったときはOnboardingViewModelのstate(この場合はbool)はnullですがshared_preferenceが作成されたときにデバイスから値が読み込まれます。OnboardingViewModelはStateNotifierを使ってますのでstateが変わるたびにウォッチしてるwidgetは再ビルドされます。
オンボーディング画面
画面は単純にボタンを押してview modelのcompleteOnboardingを呼び出すだけです。
これでボタンを押した途端にshared_preferencesの'onboardingComplete'キーに対してtrueが書き込まれsharedPreferencesServiceProviderの返り値が変わりmainで使われているAuthWidgetがリビルドされます。そしてリビルド時にはオンボーディングが終わっているので晴れてログイン画面が出てくる算段です。
認証機能
このアプリのアカウント認証は3つの機能があります。
1. ログイン機能
2. パスワード再発行機能
3. 新規アカウント制作機能
上記全てはFirebaseAuthenticationでかなり楽に実装することができます。
ログイン機能
ログイン機能は当然バックエンドが必要なのですがそれはGoogle Firebaseを使います。FirebaseはGoogleによって提供されているサービス群で色々な機能がありますがこのアプリではFirestoreというデータベースサービスとFirebase Authという認証機能を使います。安心してください、従量課金性ですがおそらくユーザー数数千人規模になるまで無料で使えます。
FirebaseAuthではFlutterのクライアントから新ユーザー作成、ログイン、ログアウトなどの基本的なことができます。他にももっとGoogle、フェースブック連携や電話番号確認など機能はあるのですが今回は使わないので割愛します。
ログイン方法はこのアプリではEメール・パスワードの一つしかありませんのでSignInScreenではそれを表示します。単純化のために他のログイン方法はありません。(他のログイン方法を使うのであればSignInScreenにもview modelが必要でそれにログイン機能を持たせる必要があります。)
SignInScreenは特に書くことはないのでコードは割愛します。"Use email and password"のボタンを押すとEメールとパスワードでの認証画面がプッシュされます。コード全てはレポジトリーからご覧になってください。
ログインのロジックはEmailPasswordSignInModel、UIはEmailPasswordSignInScreenで実装されています。
Eメール認証画面
EmailPasswordSignInScreenは非常に長いのですが画面は三つの形を取れるようになっています。ログイン、新規アカウント作成、およびパスワード再発行です。全て同じスクリーンですがEmailPasswordSignInFormTypeによって違うUIを描画するようになっています。下のコードではmodel.formTypeで分岐しています。
コード全体はリポジトリーから見てください。完成する画面は下のようになります。
Eメール認証ViewModel
EmailPasswordSignInModelで重要なのは前述のformTypeとsubmit関数です。
27行目、32行目共にfirebaseAuthの関数を使うことによって前述のfirebaseAuthProviderの返り値が変わります。それによって自動的にAuthWidget内のUIがリビルドされます。
アカウント作成の場合はFirestoreService.createUserを呼び出していますがこれは新規アカウント登録の時にFirestoreにユーザーデータを作成しています。これはFirebase Cloud Functionsを使うことによってバックエンドでもやれます。
以上でログインロジックは終了です。次回はこのアプリをさらに発展させてFirestoreからデータを読むやり方、Agoraとの連携などを書いていきます。