1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Android(Kotlin)からRestAPIをCallしてみました

Last updated at Posted at 2021-12-02

このドキュメントの目的

モバイルアプリは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

Remote Data Sourceに着目

今回のドキュメントの目的はサーバー接続部分である。アーキテクチャーの以下の部分となるのでここに着目して記述する
rds.png

アーキテクチャー上の役割

Repositoryの役割

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

Remote Data Sourceの役割

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

Remote Data Sourceの実装

サービスインターフェース

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

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:結果はCallにもどる

サービスパラメータクラス

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

  • jsonに含まれるリストはmutableListOfで記述する
  • jsonが階層構造の場合、親クラスのフィールドに子クラスを定義することで記述できる
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から参照され画面に表示される
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にコツやセンスは必要になるが、アプリケーションの根幹は変わらない事は認識できたので、今後も恐れずモバイルアプリ開発に挑戦したいと思う。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?