アプリ開発未経験者がFlutterでアプリをリリースした話


今年の冬ごろに友人とFlutterでアプリをリリースしたのでそのときの備忘録を残しておこうと思います。
自分も友人もアプリ開発経験はありませんでしたが、Flutterの力により実開発期間は2週間ほどと爆速で開発することができました。

概要

アプリを作ろうと考えた際にコロナ渦で自分が孤独だったという背景もあり、友達作りアプリにしようと考えました。
そこで予め設定したシュミと同じシュミを持つ人と繋がれる、チャット機能を備えたアプリを作成しました。
今回はそのアプリの技術的側面についてまとめていこうと思います。
ちなみにここからダウンロードできるので是非ダウンロードしてください!!

1.ログイン

ログインの処理は基本的にFirebase Authenticationに任せました。今回はメール/パスワード認証を用いたので、大まかには

// インスタンスを取得する
final FirebaseAuth auth = FirebaseAuth.instance;
// サインイン
await auth.signInWithEmailAndPassword(
    email: usermail,
    password: userpassword,
);

とするだけで簡単にログイン処理を実装することができました。(実際はエラー処理が大変ですが、、)
参考(https://firebase.flutter.dev/docs/auth/usage)

実際のログイン画面は

このようになっています。
ちなみに、一度ログインしたらログアウトするまで認証をパスをできるようにログイン成功時にshared_preferencesにログイン情報を保存するようにしています。

新規登録について

続いてアカウント作成についてです。アカウント作成画面は

以上のようになっていて、入力されたメールアドレスとパスワードをFirebase Authenticationに保存し、
ユーザー名、メールアドレス、作成された日時はFirebase Cloud Firestoreに保存するようにしています。
画像については、Image Pickerを用いて画像を取り込み、それをFirebase Storageにアップロードするようにしています。アップロード後はそのURLを取得し、それをCloud Firestoreに保存しています。

パスワードの再設定につい

これは以下のような専用のフォームを作り、メールアドレスを送信させるようにしました。

後は、先程張ったここでも紹介されているsendEmailVerification()メソッドを使うことで簡単に実装することができます。

2.ホーム画面

ホームの画面は、レイアウトはGridViewを用いています。それぞれの要素をタップすることで詳細画面に飛ばすようにしました。

この画面ではTabBarをつけているのですが、"最近"の方のタブを選ぶと登録された時間が遅い順に、"おなじシュミ"を選択すると自分の登録したシュミと同じものを持つユーザーを表示します。この処理にはCloud Firestoreのクエリを用いています。

同じシュミをもつユーザーのインスタンスを取得する例
FirebaseFirestore.instance
                 .collection('user')
                 .where("hobby", arrayContainsAny: hobbylist) //hobbylistはシュミを保存しているlist
                 .snapshots()

また右上の検索のアイコンをクリックするとユーザーの検索をすることができます。この検索はいわゆるlike検索に対応しています。
これはfirestoreのクエリに

.startAt([input])
.endAt([input + '\uf8ff'])

を追加することで実装することができました。これの理由についてはFirebaseのドキュメントに以下のような記述があります。

上記のクエリで使用されている \uf8ff 文字は Unicode 範囲内の非常に高いコードポイントです。この文字は Unicode のほとんどの通常文字より後に来るため、クエリは b で始まるすべての値に一致します。

3.チャット

続いてこのアプリのチャット機能についてです。

チャットルームについて

アプリの仕様としてホーム画面からユーザーを選択することでチャットルームが生成されます。
このチャットルームを生成する処理の一部は以下のようになっています。ルーム名は一意になるようにお互いのユーザー名をCompareToメソッドで比較し、ユーザー名を結合させて作成しています。
このルームというコレクションは、トーク内容を保存するサブコレクションと'member'、'block'などのフィールドを持ちます。

  Future makeChat() async {
    //
    roomname = compstring(username, me);
    //既に存在していないかチェック,存在していなかったらルーム作成
    DocumentSnapshot docSnapshot = await FirebaseFirestore.instance.collection('room').doc(roomname).get();
    if (!docSnapshot.exists) {
      await FirebaseFirestore.instance.collection('room').doc(roomname).set(
        {
          'member': [username, me],
          'block': false,
          'blockuser': [],
        },
      );
// ... 略

  compstring(String a, String b) {
    if (a.compareTo(b) == 1) {
      return b + a;
    } else if (a.compareTo(b) == -1) {
      return a + b;
    } else {
      print("user name error");
    }
  }
// ... 略

この処理の後、ルームが作成がされると、'member'に含まれるアカウントのトーク画面にルームが表示されるようになります。

これを実装するためにトーク画面では、Cloud Firestoreからクエリを用いてルームを取得しています。
参考(https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja)
また、ここで使われているStreamBuilderについてはこちらを参照

StreamBuilder<QuerySnapshot>(
  stream: FirebaseFirestore.instance
    .collection('room')
    .where("member", arrayContainsAny: [username])
    .orderBy('createdAt', descending: true)
    .snapshots()
  builder:  (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
    if (snapshot.hasError) {
       //略
    }
//略

チャットについて

以下が実際のチャットの画面です。

基本的な処理としては、FireStoreのインスタンスを取得し、それぞれの要素をListView()で表示しています。
また、チャットの機能としては以下のものがあります。

  • 画像送信
  • ブロック
  • 送信取り消し

画像送信

これはアカウント作成と同様にFirebase StorageにアップロードしてそのURLを内容として持つことで画像のやり取りを可能にしています。

ブロック

ユーザーをブロックすることで、コレクション"room"のフィールド"blockuser"にブロックをしたアカウントのユーザー名が保存されます。
そのユーザーに対しては、チャット画面が表示されないようになります。(ブロックをしていてもチャット内容はCloud Firestoreに保存される。)

送信取り消し

これは某L〇NEなどに存在する機能ですね。実装としてはテキスト自体をGestureDetectorでwrapし、タップイベントにチャットを削除する通知画面をポップする処理を入れています。

その他

通知

アプリの通知についてはアプリにログインした際にその端末のFCMトークンをfirestoreに保存するようにしています。
また、別でCloud Functionsで通知用の関数を設定しました。
アプリ側でメッセージの内容とその送信先をCloud FunctionsにJSONでPOSTすることで、その関数がfirestoreからFCMトークンを読み取り、通知を送信するようになっています。

アイコン

アプリ内で使用したアイコンのほとんどがdartのIcons classで賄うことができました。公式のドキュメントに画像も掲載されていて使いやすいです。

shared_preferences

shared_preferencesはローカルのデータを保存する手段としてアプリ全体で使用しています。
このアプリでは、シュミは5つまで選択することができ、クエリとして使うときなどプログラム内ではListとして処理しています。
しかし、shared_preferencesでは配列を保存することができないので、保存するときはカンマ区切りで文字列に変換してから文字列として保存しています。
また、データを読み取る際は、以下のようにデータを読み取る関数を定義それをFutureBuilderで待機しています。

  Future getData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    username = prefs.getString('name') ?? '';
    usermail = prefs.getString('mail') ?? '';
    userimage = prefs.getString('image') ?? '';
  }
  //略
  FutureBuilder(
          future: getData(),
          builder: //略

あとがき

Flutterを実際に使ってみて、アプリ開発の初めのSDKや専用のソフトウェアを使い方を覚えるといった部分のハードルがとても下がっていると感じました。ただ英語の資料が多く英語を読んでいかないと開発が進まない場面も多くありました。この記事が他の初心者に対してFlutterの日本語の記事として参考になれば幸いです。