9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TDCソフト株式会社Advent Calendar 2023

Day 8

"半初学者"が会社の旅行を最高に楽しくするためにアプリ作った(Android API通信+小話編)

Last updated at Posted at 2023-12-08

はじめに

この記事はTDCソフト株式会社Advent Calendarの1日目の記事で紹介した「会社の旅行を楽しくするためにアプリを作った」のAndroid版の開発話(API通信周り+小話)です!

どんなアプリなのかについては1日目の記事をお読みください。

※この記事はkotlinの基礎的なAPIリクエストの内容がメインになります。もう知ってるわという方は、小話だけ読んでそっとこの記事を閉じて上の記事を読んでください

※iOSもAndroidも開発するならFlutterでやればよかったじゃんーというのはナシです
(Flutterはもう書けるからkotlinに挑戦してみたかったんです🔥)

概要

今回のコンセプトは『初学者に API通信を 具体例あり ざっくり説明あり で解説!』です

Androidアプリの開発にあたって、初学者であればほぼ間違なくぶち当たるであろう『API通信』について、せっかく今回作成したアプリの具体例がありますのでそれを元に解説します!

今回はわかりやすく『お題一覧画面』を表示するためのデータを取得するAPIをベースに説明します。
ざっくり説明するとエリアごとにお題を表示する画面です。

コードやアーキテクチャは有識者のRvを設けながら、割と丁寧に作っているのできっと参考になるかと思います。

API通信でデータ取得

少なくともPJでアプリを作成するのであれば必須の内容でしょう。
参考にした公式ドキュメントはこちら

レスポンスモデルの作成

まずはAPIを受け取る際のModelについてです。
これは『APIのレスポンスをどのような形で受け取るの?』というものを定義したものです。
お題一覧の場合はこれだ!

// エリアのデータ
data class QuestResponse(
    val questions: List<QuestData>,
    val areaName: String,
)

// お題のデータ
data class QuestData(
    val activityId: String,
    val point: Int?,
    val questionName: String,
)

今回の場合エリアが複数あって、その中にお題が複数あるという感じなのでレスポンスとして受け取る場合はListの中にListがあるという状態を受け取る必要があります。

ということで上記ではval questions: List<QuestData>のところで複数お題データをListとしてエリアデータとして入れています。

これをList<QuestResponse>のような形で使用することによってListの中にListがあるというModelを作成することができました。

サービスの作成

次はAPIのリクエストをする部分であるサービスについてです。
今回はretrofitというものを使ってリクエストを行います。

retrofitはHTTPクライアント通信ができるライブラリで、今回使用するメソッドはGETですね。

まずはAPIを定義するために、インターフェースとメソッドを宣言します。

interface QuestApiService {

    @Headers("X-API-Key:${BuildConfig.API_KEY}", "Content-Type: application/json")
    @GET("Quest")// ここのアノテーションでretrofitが使える
    fun getQuest(): Call<List<QuestResponse>>// 前編で作成したModel(この形で受け取るよという宣言)
}

次にRetrofitのインスタンスとAPIインターフェースの実体を作成します。

ざっくりいうと『@GET("Quest")をどういう条件でリクエストするの?』みたいなことを書いていると思ってください。

object RetrofitClient {
    // どういう条件でリクエストするのか
    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
        .readTimeout(TIMEOUT, TimeUnit.SECONDS)
        .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)// リクエスト先のURL
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build()

    // ここでAPIインターフェースと繋げている
    var questApiService: QuestApiService = retrofit.create(QuestApiService::class.java)
}

『Serviceに一緒に書けばいいじゃん』となるかもしれませんが、基本的にAPIをリクエストするベースの条件はどのAPIでも共通です。
そのため切り出しておいたほうが都合がいいですよね!

リポジトリの作成

次はリポジトリです。前で作ったサービスを使用する部分ですね
こちらもざっくりいうとAPIリクエストを行った結果によって
『成功なの?』『失敗なの?』『失敗ならばどんなExceptionを返すの?』といった判定をしています。

class QuestApiRepository(private val dispatcher: CoroutineDispatcher = Dispatchers.IO) {
    suspend fun getQuest(): ResultHandler<List<QuestResponse>> {
        // 事象の切り分け
        return safeApiCall(dispatcher, listOf<QuestResponse>() ::class.java) {
            // retrofitのサービスを呼び出している
            RetrofitClient.questApiService.getQuest().execute()
        }
    }
}

またこちらも細かい事象の判定をsafeApiCallというものに切り出して行っています。
理由はretrofitと同じで、この判定の条件は基本どのAPIでも共通的に使用するものだからです。

suspend fun <T> safeApiCall(dispatcher: CoroutineDispatcher, type: Type, apiCall: suspend () -> Response<T>): ResultHandler<T> {
    return withContext(dispatcher) {
        try {
            val service = apiCall()
            // 成功
            if (service.isSuccessful) {
                val resultResponse = service.body()
                // 成功ならこれを返す!
                return@withContext ResultHandler.Success(resultResponse)
            // 失敗1
            } else if (service.code() == 401) {
                // 失敗1ならこのExceptionを返す!
                val unAuthorisedResponse = Gson().fromJson<T>(service.errorBody()?.string(), type)
                return@withContext ResultHandler.UnauthorisedResponse(unAuthorisedResponse)
            // 失敗2
            } else {
                return@withContext ResultHandler.UnexpectedException
            }
        } catch (e: Exception) {
            // その他の失敗
            when (e) {
                // 独自で定義したException
                is SocketTimeoutException -> ResultHandler.TimeoutException
                is IOException -> ResultHandler.NetworkException
                is JsonSyntaxException -> ResultHandler.DecodeException
                else -> {
                    ResultHandler.UnexpectedException
                }
            }
        }
    }
}

実際に使ってみる

ではここまで実装してきたものを実際に使ってみましょう!
下記のコードは_questDataの中に取得したエリアごとのお題データを格納する

// お題データ管理用LiveData
val questData: LiveData<List<QuestResponse>> get() = _questData
private var _questData = MutableLiveData<List<QuestResponse>>()

// 作成したRepository
var repository = QuestApiRepository()

private fun fetchQuestData(context: Context) {
        viewModelScope.launch {
            // APIを呼び出す
            try {
                _onLoading.postValue(true) // ローディング表示用LiveDataを更新

                // Repositoryを使ってAPIリクエスト
                when (val result = repository.getQuest()) {
                
                    // 成功or失敗によって処理を分ける
                    is ResultHandler.Success<List<QuestResponse>> -> {
                        if(result.data != null) {
                        
                            // 成功ならLiveDataを取得結果に更新
                            _questData.postValue(result.data)
                        }
                    }
                    // エラーダイアログ表示用LiveDataを更新(Repositoryで判定したExceptionによってさらに処理を分けることも可能)
                    else -> _errorDialogMsg.postValue(context.getString(R.string.unexpected_error_dialog))
                }
                _onLoading.postValue(false)
            } catch (e: Exception) {
                _onLoading.postValue(false)
                _errorDialogMsg.postValue(context.getString(R.string.unexpected_error_dialog))
            }
        }
    }

上記はMVVMアーキテクチャで使用した場合の実装例です。
LiveDataやMVVMなどのアーキテクチャもkotlin開発においてはmustで抑えるところですね!
今回のメインではないので割愛します。

小話1

今回のアプリは画像を投稿してそれに対してチームに点数が入るという方式でしたが、APIの制約として画像サイズの上限が決まっていました。

ですが、画像圧縮度が足りず一部の高解像度端末で画像が投稿できないという不具合が。。。(画像を投稿するアプリなのに。。)

雑魚いAndroidの実機しか所持しておらずテストが不十分になってしまいました。
みなさんはカメラ、端末内のファイルにアクセスするなど、端末独自の機能を使用する場合は特にシミュレータだけでなく様々な端末でテストするようにしましょう。

小話2

※ここからはあくまで個人の見解です

純粋に技術力をつけるのであれば『何か作ってみる』だと思っています。

何か作ってみることには特別な能力は要らなくて、調べて手を動かせば正直誰でも作ることができます。(今はchatGPTなど便利なツールもあるので)

ただ『何か作ること』は20時間くらいでできるかもしれないし、200時間かけてもできないかもしれない、その状況下で『完成するまで手を動かし続けることができるか』が重要だと思っています。

それをクリアした結果に技術がついてきて、100時間かかっていたものが次は20時間でできる!となるのだと。
(ここの上がり幅はかなりデカいと思っています)

今回のアプリは学習も込みで70時間くらいでした。
ただ次に同じようなものを作れと言われたら、一から作成してコピペなしでも15〜20時間ぐらいで作れる自信があります!

『完成するまで手を動かし続けることができるか』
つまり何が言いたいかというとアプリ開発は 技術 の前に パッション です🔥笑

9
0
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
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?