開発初心者がサーバーレスにアプリを作ってみた（Kotlin）

## はじめに
<a href="https://qiita.com/advent-calendar/2022/android">Android Advent Calender2022</a>の21日目の記事です。

普段は<a href="https://mbaas.nifcloud.com/" target="_new">ニフクラ mobile backend</a>というアプリ開発のサービスを<b>企画職</b>として運営している立場なんですが、アドベントカレンダーを機にアプリを開発してみました。

初心者なのでコードが変だったりすると思いますが、<b>「自分も最初はこんなもんだったな～」</b>とか思って年末を締めくくるのもたまには良いですよね。そういうのんびりした気持ちでお読みください。

## 開発するもの
開発なんもわからん。コードもわからん。どうすっかなぁ～と悩んだ結果、<b>4択のクイズアプリ</b>を開発する事にしました。コードは最後に載せておきます。
※ファイル減らすために1ファイルにたくさん書いてます。下手くそなコードですが一旦動けばよし。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/fb0e065e-e156-11a2-b87c-b56c4c9ada35.png" width="400px">

今回は以下の機能が使えるアプリにしたいと思います。筆者はこれが実装出来たら拍手レベル。頑張ろう。
- 新規会員登録/ログイン機能
- データベースに入ってる問題と選択肢などを拾ってきて出題
- 正誤判定

今回開発予定のアプリの全体像はこんな感じです。基本的にサーバー側の処理は`ニフクラ mobile backend`の機能を使ってます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/c98ce966-b935-5953-fea6-485d926659c7.png)

## 開発環境
- MacOS Monterey（バージョン 12.5.1） 
- Android Studio（dolphin）
    - 検証端末：Android emulator (Pixel2 API28)
- mBaaSサービス
    - <a href="https://mbaas.nifcloud.com/"target="_new">ニフクラ mobile backend</a> （Kotlin SDK v1.2.0）
        - データストア機能、クエリ検索機能
        - 会員管理、認証機能（会員登録/ログイン機能のため）
- 気合い（多分一番必要）

## 使った機能

- <b>Intent</b>
今回はログイン画面（ホーム）からクイズ画面への画面遷移も実装したいので、`Intent`を用います。
↓この記事が凄く参考になりました。書いてくれた人に感謝。
<a href="https://howcang.com/2021/05/19/kt-intent/" target="_new">【はじめてのKotlinプログラミング(7)】intent（画面遷移)</a>

- <b>ViewBinding</b>
問題と選択肢の情報を画面に表示するために利用。以前は`findViewById`が使われていたようですが、現在はこれがスタンダードらしいですね。モジュール単位で有効かどうか設定して、モジュールの`build.gradle`ファイルに以下を記述して使用します。

```Kotlin:app/build.gradle
android {
        ..(省略)
        viewBinding {
            enabled = true
        }
    }
```

また、実際に使用する時はActivityに以下を記述して使います。

```Kotlin:各Activity.kt

class MainActivity : AppCompatActivity(){
  // lateinit で宣言しておくとonCreateメソッド内（あとで）で使える
  private lateinit var binding: ActivityMainBinding

//onCreate内
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

  //layoutファイル(xml)がactivity_mainなので、キャメル型 + Bindingで命名する
  binding = ActivityMainBinding.inflate(layoutInflater)
  setContentView(binding.root)
  }
}
```

アプリにボタンやテキストを実装する時に、idで呼び出して管理できるのは分かりやすいし超便利でした。

- <b>データストア機能</b>
ニフクラ mobile backendからAPI経由で呼び出す機能で、データの保存や取得、検索も簡単に出来ます。何よりサーバーレスに使えてSNSのアカウントですぐ始められるのでとてもラク。筆者くらい初心者のうちは確実に使った方が効率が良いんじゃなかろうか。

- <b>新規会員登録・ログイン機能</b> (会員管理・認証機能)
データストア同様にニフクラ mobile backendから呼び出して使います。今回は新規会員登録とログイン機能の部分で活用しますが、ユーザーごとに権限設定できるのも熱い。

## 開発の流れ

今回はバックエンド機能は開発せずに`ニフクラ mobile backend`を使用しています。アコーディオンを開くとコードや実装手順を確認出来るようにしたので、時間がある人は開いて手順を確認してみてください。

### ニフクラ mobile backend の初期設定
<details><summary>①ニフクラ mobile backendに無料登録</summary><div>
SNSアカウントを持っている人であれば以下の動画通りで1~2分でサクッと使えるようになります。

<iframe width="300" height="400" src="https://www.youtube.com/embed/5PddRPlUALg" title="ニフクラ mobile backend登録方法" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

</div></details>

<details><summary>②Kotlin SDKのインストール</summary><div>

- <a href="https://github.com/NIFCLOUD-mbaas/ncmb_kotlin/releases" target="_new">Kotlin SDK のリリースページ</a>より最新の Kotlin SDKをダウンロードする (解凍すると`NCMB.jar`となる)
- Android Studioプロジェクトのlibsフォルダに`NCMB.jar`を配置する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/7ecc3fa1-55b6-e8b4-68b1-67dddd5801bc.png)


- app/build.gradleファイルに以下を追加する

```Kotlin:app/build.gradle
dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.8.1'
    implementation 'com.google.code.gson:gson:2.3.1'
    api files('libs/NCMB.jar')

    //同期処理を使用する場合はこちらを追加していただく必要があります
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
}
```

- 追加後、画面上部に`Sync Now` (変更内容の同期)を行う※必ず実施する

</div></details>

<details><summary>③AndroidManifest.xmlの編集</summary><div>

- <application>タグの直前に以下のpermissionを追加する

```Kotlin:AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
</div></details>
    
<details><summary>④Kotlin SDKの初期化</summary><div>

- ニフクラ mobile backendにアプリを作成したらAPIキーが発行されるので、.ktファイルの`onCreateメソッド内`に以下を記述し、`"YOUR_APPLICATION_KEY", "YOUR_CLIENT_KEY"`を書き換える

```Kotlin:各Activity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // APIキーの設定とSDK初期化
    NCMB.initialize(this, "YOUR_APPLICATION_KEY", "YOUR_CLIENT_KEY") //APIキーを設定して繋ぎこむ
   
    setContentView(R.layout.activity_main)
}
```
</div></details>

<details><summary>⑤必要なライブラリをインポート</summary><div>

- ログイン機能やデータストア機能を実装する時はライブラリを使用するので、以下を冒頭に追記しておきます

```Kotlin:各Activity.kt
import com.nifcloud.mbaas.core.NCMB //全機能必須
import com.nifcloud.mbaas.core.NCMBCallback //非同期処理を行う場合
import com.nifcloud.mbaas.core.NCMBObject //データストアを利用する場合
import com.nifcloud.mbaas.core.NCMBQuery //検索機能を利用する場合
import com.nifcloud.mbaas.core.NCMBException //例外処理を行う場合
import com.nifcloud.mbaas.core.NCMBUser //会員管理を利用する場合
```
</div></details>

### サーバー側にクイズの元データ(.csv)を入れておく

今回はニフクラ mobile backendのデータストアにクイズの基データを格納するので、csvファイルを用意したらデータストアへのインポートを行います。今回は以下のようにcsvファイルを用意しました。
※データストアに文字列型で格納する場合は文字を`""(ダブルクォーテーション)`で囲む

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/1357d0b9-bb21-ef68-72a2-c9c8f776a08f.png" width="600px">

これをメモ帳で開くとこんな感じで`"""室町時代"""`のようにダブルクォーテーションが3つ出現します。
このまま格納するとダブルクォーテーションが付いた状態で文字列が格納されてしまうので、メモ帳で編集します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/5f98b8c1-b2e4-51d4-650b-d866f13aa2a4.png" width="400px">
これを置換で以下のように1つに置換すれば綺麗になります。文字コードはUTF-8である事を確認して保存。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/15a1b550-bb10-7b14-b932-a38f7e121949.png" width="400px">

これをデータストアのクラス作成からインポートすると
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/40c1fa57-805b-4cbd-cb6d-a4359bdf6a28.png" width="600px">
こんな感じで格納されます。これをクイズとして呼び出せば完成ですね。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/4505b9de-ad8a-5a76-834f-b341bff9cb55.png" width="600px">
今回はExcelでcsvファイルに変換して用意しましたが、エディタなどで直接csvファイルを作ってしまえばダブルクォーテーションを修正するような作業も不要だと思います。

### ホーム画面(MainActivity.kt)の実装

- `Button`置いたり`EditText`置いたりしているだけなので、Layoutファイルに書いた内容は省略します
    - `binding.XXX.text`や`binding.XXX.setOnClickListener`の`XXX`はxmlファイルで指定した`id`です
  
<details><summary><font color= red><b>ソースコードを表示する</b></font></summary><div>

```Kotlin:MainActivity.kt
package com.example.a2022adventkotlin
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.example.a2022adventkotlin.databinding.ActivityMainBinding
import com.nifcloud.mbaas.core.NCMB
import com.nifcloud.mbaas.core.NCMBException
import com.nifcloud.mbaas.core.NCMBUser

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding 

    private fun login(id:String,pw:String){
        //　Userインスタンスの生成
        var user = NCMBUser()
        //ユーザー名・パスワードを設定
        user.userName = id //ユーザー名
        user.password = pw // パスワード
        try{
            // ユーザー名とパスワードでログイン
            user.login(id,pw)
            // ログインに成功した場合の処理
            val intent = Intent(this, QuizActivity::class.java)
            startActivity(intent)
        }
        catch(e:NCMBException){
            // ログインに失敗した場合の処理
            Toast.makeText(this, "ログイン失敗", Toast.LENGTH_LONG).show()
        }
    }
    private fun signUp(newid:String,newpw:String){
        //　Userインスタンスの生成
        var user = NCMBUser()
        // ユーザー名・パスワードを設定
        user.userName = newid
        user.password = newpw
        // ユーザーの新規登録
        try {
            user.signUp(newid,newpw)
            // 新規登録に成功した場合の処理
            AlertDialog.Builder(this)
                .setTitle("会員登録完了")
                .setMessage("ホームよりログインしてください")
                .setPositiveButton("閉じる"){dialog, which ->}
                .show()
        }
        catch(e: NCMBException){
            // 新規登録に失敗した場合の処理
            AlertDialog.Builder(this)
                .setTitle("会員登録失敗")
                .setMessage("再度お試しください")
                .setPositiveButton("閉じる"){dialog, which ->}
                .show()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val appKey = "ニフクラ mobile backendからアプリケーションキーを入力"
        val cliKey = "ニフクラ mobile backendからクライアントキーを入力"
        NCMB.initialize(this, appKey, cliKey)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.buttonlogin.setOnClickListener{
            //xmlファイルに指定したIDを使う(userIDとuserPWはタイトルのEditText部に紐づく)
            val userID = binding.userID.text.toString()
            val userPW = binding.userPW.text.toString()
            login(userID,userPW)
        }
        binding.buttonsignUp.setOnClickListener{
            //xmlファイルに指定したIDを使う(手を抜いたので上と同じIDを使いまわし)
            val newID = binding.userID.text.toString()
            val newPW = binding.userPW.text.toString()
            signUp(newID,newPW)
        }
    }
}
```
</div></details>

### クイズ画面(QuizActivity.kt)の実装

- こっちのLayoutファイルも上記同様に省略します

<details><summary><font color= red><b>ソースコードを表示する</b></font></summary><div>

```Kotlin:QuizActivity.kt
package com.example.a2022adventkotlin
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.example.a2022adventkotlin.databinding.ActivityQuizBinding
import com.nifcloud.mbaas.core.NCMB
import com.nifcloud.mbaas.core.NCMBObject
import com.nifcloud.mbaas.core.NCMBCallback
import com.nifcloud.mbaas.core.NCMBQuery

class QuizActivity : AppCompatActivity() {
    private var questionNum = 0
    private lateinit var binding: ActivityQuizBinding

    private fun count(): Int{
        val query = NCMBQuery.forObject("QUIZ1")
        val size = query.count()
        return size
    }
    private fun Question(x:Int) {
        //QUIZ1クラスにある問題をデータストアから取得する
        val query = NCMBQuery.forObject("QUIZ1")
        query.findInBackground(NCMBCallback { e, objects ->
            if (e != null) {
                //error処理(省略)
            } else {
                if (objects is List<*>) {
                    var arr: Any? = objects[x]
                    if (arr is NCMBObject) {
                        //選択肢情報を格納しておく(正誤判定でも使う)
                        val a1 = arr.getString("a1")
                        val a2 = arr.getString("a2")
                        val a3 = arr.getString("a3")
                        val a4 = arr.getString("a4")
                        //出題
                        binding.TextView2.text = arr.getString("question")
                        binding.button.text  = a1
                        binding.button2.text = a2
                        binding.button3.text = a3
                        binding.button4.text = a4
                        //選択肢を選んだ時の処理
                        val answer = arr.getString("answer")
                        binding.button.setOnClickListener {
                            if (answer == a1) {
                                AlertDialog.Builder(this)
                                    .setTitle("正解！")
                                    .setMessage( answer + "です！")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            } else {
                                AlertDialog.Builder(this)
                                    .setTitle("不正解！")
                                    .setMessage("正解は" + answer +"です")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            }
                        }
                        binding.button2.setOnClickListener {
                            if (answer == a2) {
                                AlertDialog.Builder(this)
                                    .setTitle("正解！")
                                    .setMessage( answer + "です！")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            } else {
                                AlertDialog.Builder(this)
                                    .setTitle("不正解！")
                                    .setMessage("正解は" + answer +"です")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            }
                        }
                        binding.button3.setOnClickListener {
                            if (answer == a3) {
                                AlertDialog.Builder(this)
                                    .setTitle("正解！")
                                    .setMessage( answer + "です！")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            } else {
                                AlertDialog.Builder(this)
                                    .setTitle("不正解！")
                                    .setMessage("正解は" + answer +"です")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            }
                        }
                        binding.button4.setOnClickListener {
                            if (answer == a4) {
                                AlertDialog.Builder(this)
                                    .setTitle("正解！")
                                    .setMessage( answer + "です！")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            } else {
                                AlertDialog.Builder(this)
                                    .setTitle("不正解！")
                                    .setMessage("正解は" + answer +"です")
                                    .setPositiveButton("閉じる"){dialog, which ->}
                                    .show()
                            }
                        }
                    }
                }
            }
        })
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val appKey = "ニフクラ mobile backendからアプリケーションキーを入力"
        val cliKey = "ニフクラ mobile backendからクライアントキーを入力"
        NCMB.initialize(this, appKey, cliKey)

        binding = ActivityQuizBinding.inflate(layoutInflater)
        setContentView(binding.root)
        //出題を切り替えるボタン
        binding.nextButton.setOnClickListener {
            binding.nextButton.text= "次の問題へ"
            //出題
            if (questionNum >= count()) {
                //問題を最初に戻す
                questionNum = 0
                Question(questionNum)
            } else {
                //次の問題へ
                Question(questionNum)
                questionNum++
            }
        }
        binding.homeButton.setOnClickListener{
            val intent = Intent(this, MainActivity::class.java)
            startActivity(intent)
        }
    }
}
```
</div></details>

## 完成画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/66c73e79-7103-74ab-40f8-780b862a749c.png" width="300px">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/13e7e61d-5668-eaa1-43fe-e9a2f49cdcab.png" width="300px">
<br>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/cd70fad8-059f-50cd-84c1-a26feeaa5060.png" width="300px">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/83611559-ca89-7665-c956-1dbb916e8ec9.png" width="300px">

無事実装したい機能の動作が確認出来ました。タイトルの鳥はニフクラ mobile backend のイメージキャラクターの`タカノくん`です。
色が入るだけで画面が綺麗に見えるので、デザイン面も勉強しながら綺麗なUIも作っていきたいですね～。

## おわりに

いかがでしたでしょうか。簡単なアプリにはなりましたが、Androidアプリ開発者への一歩目を踏み出せたような気がして楽しくなってきました。来年のアドカレではつよつよな記事を出せるように頑張ります。<a href="https://mbaas.nifcloud.com/">ニフクラ mobile backend</a>もSNSアカウントで始められるので、まだ使ったことない人は是非。使ってくれたら私が喜びます。

筆者はこのアプリをベースに新しい技術を勉強しようと思っています。（年末に餅を食べながら頑張ります。）
この記事が誰かのモチベに繋がればいいですね。餅だけに。（ごめん）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325617/3af659b0-d30b-88d7-112d-c1d80feae8a5.png" width="400px">

## 参考
- https://howcang.com/2021/05/19/kt-intent/ (Intentでお世話になった記事)
- https://mbaas.nifcloud.com/ （ニフクラ mobile backend）
- https://developer.android.com/studio/install?hl=ja （Android Studio）