Firebaseのサーバーレスアプリ開発について(Android編)

## Firebaseサーバーレスアプリのニーズ
2~3年前からサーバーレスのアプリ開発(Android/iOS)が増えています。
特にFirebaseを使っているスタートアップをよく見かけます。

アプリの競争が激しい中、今はいいアプリを作るだけで売れる時代は終わっているかも知りません。
特に資金力の弱いスタトアップだと限られた資金で短期間に成果をあげることは難しいでしょう
資金調達の為にもより早くアプリを見せないといけないです。
そういったニーズの答えがFirebaseのサービスだと思います。

## Firebaseサーバーレスのメリット&デメリット

メリット&デメリットはあくまで私の個人意見です。
以下の内容以外にもいっぱいあると思います。

### メリット
- サーバー開発期間がゼロ
- サーバー管理が不要で開発に集中できる
- 無料枠でも結構使える
- Firestoreだとほぼリアルタイムで更新するのでチャット機能も簡単に実現できる

### デメリット
- 無料枠を超えると料金がちょっと高めになること
  - Firebaseの詳細料金表：https://firebase.google.com/pricing/?hl=ja
- Firebaseサービスに依存することになる

### その他
- [Firebase Authenticationの特徴](https://www.apps-gcp.com/firebase-authentication)
- [Cloud Firestoreのメリット&デメリット](https://tomokazu-kozuma.com/about-the-advantages-and-disadvantages-of-cloud-firestore/)

## Firebaseサーバーレスの実装サンプル
本投稿ではサーバーレスの実装がどれだけ簡単にできるかをコードを中心に伝えたいと思います。
導入方法については次回に紹介します。

### 1.Firebase Authentication
会員登録とログイン関係の全ての機能を提供しています。
サーポートする認証方式は以下の画像を参考にしてください。
FirebaseでサーポートしてないLINEなどの認証は別の方法で実装ができます。
それについては次回に紹介します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/57a5f32c-2bed-9fde-d83f-779474ec19b2.png" width="800">

サーポートする認証方式は色々ありますが、
本投稿では「メール/パスワード」の認証方式だけをkotlinで書いて見ました。
- 会員登録、プロフィール変更、パスワード変更、ログイン、ログアウト、退会

##### a.会員登録

```kotlin:会員登録
    fun signup() {
        val auth = FirebaseAuth.getInstance()
        val email = "test@bizreach.co.jp"
        val password = "12345678"
        auth.createUserWithEmailAndPassword(email, password)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```
###### 会員登録後のAuthentication管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/bc783575-7f9e-1a9b-2cc7-c0826ce67664.png" width="800">



##### b.プロフィール変更

```kotlin:プロフィール変更
    fun changeProfile() {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser ?: return

        val name = "山田"
        val profileUrl = "https://example.com/jane-q-user/profile.jpg"
        val profile = UserProfileChangeRequest.Builder()
            .setDisplayName(name)
            .setPhotoUri(Uri.parse(profileUrl))
            .build()
        user.updateProfile(profile)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```

##### c.パスワード変更

```kotlin:パスワード変更
    fun changePassword() {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser ?: return
        val newPassword = "abcdefgh"
        user.updatePassword(newPassword)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```

##### d.ログイン

```kotlin:ログイン
    fun login() {
        val auth = FirebaseAuth.getInstance()
        val email = "test@bizreach.co.jp"
        val password = "abcdefgh"
        auth.signInWithEmailAndPassword(email, password)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```

##### e.ログアウト

```kotlin:ログアウト
    fun logout() {
        FirebaseAuth.getInstance().signOut()
    }
```

##### f.退会

```kotlin:退会
    fun withdrawal() {
        val auth = FirebaseAuth.getInstance()
        val user = auth.currentUser ?: return
        user.delete()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```

###### 退会後のAuthentication管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/efb6156c-5978-cf9a-4361-fe14ec4d097b.png" width="800">

### 2.Cloud Firestore
NoSQLデータベースで、データの保存・変更・削除などの機能が簡単に使えます。
サンプルコードではToDo一覧、ToDo詳細機能をベースにデータを構成して実装して見ました。

##### a.ToDo登録
```kotlin:ToDo登録
    data class Todo(val title: String, val done: Boolean = false, val createdAt: Timestamp = Timestamp(Date()))

    fun addTodo() {
        val db = FirebaseFirestore.getInstance()
        val col = db.collection("todo_list")

        col.add(Todo("todo1"))
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }

        col.add(Todo("todo2"))
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```
###### ToDo登録後のFirestore管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/4971e405-cf61-b2fd-a627-5bbb9666139e.png" width="800">

##### b.ToDo編集
```kotlin:ToDo編集
    fun changeToDone() {
        val db = FirebaseFirestore.getInstance()
        val doc = db.collection("todo_list")
            .document("Hi2mlwN3lguMmfu76ZqX")

        doc.update("done", true)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```
###### ToDo編集後のFirestore管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/60a51211-e31a-ddb2-0a81-6b79aaaf79b9.png" width="800">

##### c.ToDoの一覧取得
```kotlin:ToDoの一覧取得
    fun getTodoList() {
        val db = FirebaseFirestore.getInstance()
        db.collection("todo_list")
            .get()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result ->
                /*成功*/
                result?.forEach {
                    val title = it["title"] as String
                    val done = it["done"] as Boolean
                    val createdAt = it["createdAt"] as Timestamp
                    val todo = Todo(title, done, createdAt)
                    Log.d("TAG", "todo_list: documentId=${it.id} todo=$todo")
                }
            }
    }
---- logcat
//todo_list: documentId=1aPGHKce0Fn4Osu65ioW todo=Todo(title=todo2, done=false, createdAt=Timestamp(seconds=1573460794, nanoseconds=820000000))
//todo_list: documentId=Hi2mlwN3lguMmfu76ZqX todo=Todo(title=todo1, done=true, createdAt=Timestamp(seconds=1573460794, nanoseconds=801000000))

```

##### d.ToDo削除
```kotlin:ToDo削除
    fun removeTodo() {
        val db = FirebaseFirestore.getInstance()
        val doc = db.collection("todo_list")
            .document("Hi2mlwN3lguMmfu76ZqX")

        doc.delete()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```
###### ToDo削除後のFirestore管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/18cd74e2-e7d3-29d1-8a30-3d0bb8f12a2f.png" width="800">

### 3.Cloud Storage
ファイルの保存、変更、削除、URL参照ができます。
例えば会員のプロフィール写真などを保存して画面に表示する際に使います。
※内部的にはAWSのS3を使っています。

##### a.プロフィール画像のアップロード
```kotlin:プロフィール画像のアップロード
    fun uploadProfile() {
        val storage = FirebaseStorage.getInstance()
        val profileRef = storage.reference.child("images/profile.jpg")

        val bitmap = BitmapFactory.decodeResource(resources, R.drawable.profile)
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        val data = baos.toByteArray()

        profileRef.putBytes(data)
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```
###### プロフィール画像のアップロード後のStorage管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/9ec7c523-0d01-a70c-a5e6-20d7ff522375.png" width="800">

##### b.プロフィール画像URLの取得
```kotlin:プロフィール画像URLの取得
    fun getProfileUrl() {
        val storage = FirebaseStorage.getInstance()
        val profileRef = storage.reference.child("images/profile.jpg")
        profileRef.downloadUrl
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result ->
                /*成功*/
                Log.d("TAG", "profileUrl = $result")
            }
    }
----logcat
profileUrl = https://firebasestorage.googleapis.com/v0/b/fir-serverlessdemo.appspot.com/o/images%2Fprofile.jpg?alt=media&token=8bbb1678-76a7-470f-8773-9f3d8d8363f4
```

##### d.プロフィール画像の削除
```kotlin:プロフィール画像の削除
    fun deleteProfile() {
        val storage = FirebaseStorage.getInstance()
        val profileRef = storage.reference.child("images/profile.jpg")
        profileRef.delete()
            .addOnFailureListener { exception -> /* 失敗 */ }
            .addOnSuccessListener { result -> /*成功*/ }
    }
```

###### プロフィール画像の削除後のStorage管理画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529876/3bb0bde7-f3be-1cba-2824-b92bf92d62ff.png" width="800">


## まとめ
Firebaseを使えば認証管理、DB管理、ファイル管理がどれだけ簡単にできるか理解頂けたと思います。

伝統的なネイティブアプリの開発方法がだんだん変わっています。
昔の開発では3人(インフラエンジニア/サーバーサイドエンジニア/フロントエンジニア)でやってたのが
今はだった一人でできる時代になりました。
Firebaseを使えば質の高いアプリ(Android/iOS/Web)をたった一人で開発することができます。

以前は物理サーバーを立ててから開発することが普通でしたが、
現在ではクラウドサービス(AWS、AZUL、GCP...)を使うのが当たり前のようになっています。
それと同じ様にネイティブアプリの開発でもサーバーレスのアプリ開発が広がり、
当たり前のような時代になるかも知りません。