Android（Kotlin）からRestAPIをCallしてみました

# このドキュメントの目的 
モバイルアプリはUIツールやライブラリーが充実しており、スマホ利用者は視覚的・操作的になじみやすいアプリが提供されている。とはいえ、スマホ単体で完結するモバイルアプリはほぼなく、サーバーに接続（APIなど）する事で機能要件を満たせるものばかりである。筆者もモバイルアプリ開発にかかわる機会を得たが、サーバー接続に関する情報が少なく苦労したので、備忘録の意味をふくめここに整理しておく。
# 使用環境
- Android 8.0+ (APIレベル26+)
- Android Studio 4.2.1
- Kotlin 1.5.10
- Retrofit 2.9.0
- gson 2.8.6

# Googleの公式アーキテクチャガイド
Googleではアーキテクチャーガイドを制定して開発者に提供している。モバイルアプリの構造としては以下が規定されており、リリース審査の観点にもなるのでなるべくこのアーキテクチャーの沿ってアプリを開発すべきである。
![GoogleArcGuide.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334253/bea05425-7e9f-6626-0886-4aa473090dd0.png)

### Remote Data Sourceに着目
今回のドキュメントの目的はサーバー接続部分である。アーキテクチャーの以下の部分となるのでここに着目して記述する
![rds.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334253/7a025301-dea3-f5b7-4879-1f6233cf0949.png)


## アーキテクチャー上の役割
### Repositoryの役割
- アプリ上位レイヤに提供するデータの格納を行う
- 状況に応じてスマホ内の永続化データまたはサーバーからのデータの取得を行う
- サーバーから取得したデータを永続化データに格納して、次の要求に備える役割も担う

### Remote Data Sourceの役割
- Repositoryからの要求に応じてサーバーからのデータ所得・格納を行う。
- サーバー側はRestAPIで窓口を開けている事が多いので、Restクライアント機能を実装する
    - このためにRetrofitを使う
- Remote Data Sourceのクラスは単数でも複数でもよいが、筆者は３つのクラスに分けて開発した
    - サービスインターフェース：Retrofitの機能を用いてサーバーが提供するAPIを定義する
        - login
        - logout
        - getAppData
        - など
    - サービスパラメータクラス：APIでやりとりするJson文字列をデータ構造として解釈するクラス
        - loginReqest
        - loginResponse
        - logoutRequest
        - など
    - サービス実装クラス：ビジネスロジックを含む実装クラス。API毎にメソッド（ファンクション）を記述してRepositoryから適宜利用する
        - login
        - logout
        - getAppData
        - など

## Remote Data Sourceの実装
### サービスインターフェース
ここにRetrofitのアノテーションを用いてRestAPIのインターフェースを記述する。
以下はloginの場合

```kotlin:TestApiService.kt
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.*

interface TestApiService {
    @POST("{action}")
    fun login(@Path("action") action: String, @Header("Authorization") basicAuth:String, @Body body: RequestBody): Call<ResponseBody>
}
```

- アノテーション
    - ＠POST：loginはサーバー側のデータに作用するのでメソッドはPOSTを使用する
    - ＠path：APIのURI
    - ＠Header：リクエストヘッダーに設定するパラメータ。loginAPIはベーシック認証を使用するのでAuthorizationを定義する
    - ＠Body：リクエストJson
- 引数
    - action：APIのURIを示す文字列
    - basicAuth：ベーシック認証に使用する暗号化文字列
    - body：リクエストJsonの文字列
- 戻り値
    - Call<ResponseBody>：結果はCallにもどる

### サービスパラメータクラス
ここに各APIで使用するReq/ResのJsonを格納するKotlinデータクラスを定義する

- jsonに含まれるリストはmutableListOfで記述する
- jsonが階層構造の場合、親クラスのフィールドに子クラスを定義することで記述できる

```kotlin:TestParameters.kt

class TestParameters {

    /**
     * logoinリクエスト
     */
    class LoginReqData {
        var appVer = ""
        var mailID = ""
        var password = ""
    }
    /**
     * logoinレスポンス
     */
    class LoginResData {
        var errors = mutableListOf<ResError>()
        var access_token = ""
        var refresh_token = ""
        var result = ResResult_login()
    }

    /**
     * 共通エラー
     */
    class ResError {
        var code = 0
        var message = ""
        var field = ""
        var title = ""
    }

    /**
     * login正常
     */
    class ResResult_login {
        var message = ""
        var title = ""
        var data = ResData_login()
    }
}

```

### サービス実装クラス
ここにRepositoryから起動されるファンクションを定義しAPI起動する前処理・後処理を記述する。

- 前処理：Repositoryから受け取ったデータクラスをjsonにする
    - モバイルアプリの場合、ActivityやFragmentから渡された項目をデータクラスにRepositoryで格納している
- 後処理：サーバーから受け取ったJsonをデータクラスにしてRepositoryに返却する
    - モバイルアプリの場合、Repositoryで永続化データとして格納したり、ActivityやFragmentから参照され画面に表示される

```kotlin:TestRemote.kt
import android.util.Log
import com.google.gson.Gson
import okhttp3.MediaType
import okhttp3.RequestBody
import retrofit2.Retrofit

class TestRemote {

    private val TAG = TestRemote::class.java.simpleName

    // Retrofit本体
    private val retrofit = Retrofit.Builder().apply {
        baseUrl("https://hogehoge.com/api/")
    }.build()

    // サービスクラスの実装オブジェクト取得
    private val service = retrofit.create(TestApiService::class.java)

    // 通信全体で利用するMediaType (Json)
    private val mediaTypeJson: MediaType = MediaType.parse("application/json; charset=utf-8")!!

    //Basic認証で使用するワードのBase64文字列
    private val basicAuth = "Basic xxxxxYYYYYzzzzz1111122222"

    fun login(loginReq: TestParameters.LoginReqData): TestParameters.LoginResData {
        val TAG_fun = "login"
        var loginRes = TestParameters.LoginResData()
        //ReqデータクラスをJsonに変換
        val jsonString = Gson().toJson(loginReq)

        //リクエスト設定
        val request = RequestBody.create(mediaTypeJson, jsonString)
        Log.d("$TAG $TAG_fun", "request.contentType = ${request.contentType().toString()}")
        Log.d("$TAG $TAG_fun", "request.content = ${request}")

        //retrofitの動作定義
        val apiAction = service.login("Login", basicAuth, request)
        Log.d("$TAG $TAG_fun", "scope.launch")

        //リクエスト実行
        val response = apiAction.execute()
        Log.d("$TAG $TAG_fun", "post.request().headers() = ${apiAction.request().headers()}")
        Log.d("$TAG $TAG_fun", "responseBody.isSuccessful= ${response.isSuccessful}")
        Log.d("$TAG $TAG_fun", "responseBody.headers = ${response.headers()}")
        Log.d("$TAG $TAG_fun", "responseBody.code = ${response.code()}")
        var resCode = response.code()

        if (resCode != 200) {
            //Error応答の場合
            response.errorBody()?.let {
                //応答の一時保存
                val jsonString = it.string()
                Log.d("$TAG $TAG_fun", "response.errorBody = ${jsonString}")

                //応答Jsonのパース
                // URL不正・サーバー無応答など、errorBodyがjsonではない場合があるのでtry-catchをかける
                try {
                    loginRes = Gson().fromJson(
                        jsonString, //ここにit.string()をいれてはいけない。一旦文字列にしないとNull関連例外になる
                        TestParameters.LoginResData::class.java
                    )
                    Log.d("$TAG $TAG_fun", "loginRes.errors = ${loginRes.errors}")
                } catch (e: Exception) {
                    Log.d("$TAG $TAG_fun", "errorBody is not Json-Strings")
                }
            }
        } else {
            //正常応答の場合
            response.body()?.let {
                //応答の一時保存
                val jsonString = it.string()
                Log.d("$TAG $TAG_fun", "response.responseBody = ${jsonString}")

                //応答Jsonのパース
                loginRes = Gson().fromJson<TestParameters.LoginResData>(
                    jsonString,
                    TestParameters.LoginResData::class.java
                )
            }

        }
       //loginResの項目を辿ればサーバーが返却した値が取得できる。
        Log.d("$TAG $TAG2", "refresh_token = ${loginRes.refresh_token}")
        Log.d("$TAG $TAG2", "expiresIn = ${loginRes.expiresIn}")
        return loginRes
    }
}
```

# まとめ
初のモバイルアプリ開発ということで気負う部分も多かったが、サーバー接続、データアクセスなどはWebアプリケーションとそれほど差異はない。
UI/UXにコツやセンスは必要になるが、アプリケーションの根幹は変わらない事は認識できたので、今後も恐れずモバイルアプリ開発に挑戦したいと思う。