Retrofit - Ki-Kobayashi/Android-Wiki GitHub Wiki

🟩

🟡公式

https://square.github.io/retrofit/

.

🟡参考コード

https://developer.android.com/codelabs/basic-android-kotlin-compose-getting-data-internet?hl=ja#6

.

🟩 Retrofitの追加

🛑Retrofit を使用するには、Jsonコンバーターも必要になる(よく使用されるのは Moshi )

🟡導入コード・バージョンの確認

(コード)
https://square.github.io/retrofit/
(バージョン)
※下記のダウンロード項目の1行目末尾に記載あり
https://github.com/square/retrofit?tab=readme-ov-file#download

.

🟡導入バージョンの確認

🛑 「lib.version.toml」を使用する場合は、下記ページを参考にする
https://github.com/Ki-Kobayashi/Android-Wiki/wiki/%E2%9C%A8%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AEVer%E7%AE%A1%E7%90%86%E3%81%AF%E3%80%8Clib.version.toml%E3%80%8D%E3%81%A7%EF%BC%81

    val retrofit_version = "2.9.0"
    implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
    implementation("com.squareup.retrofit2:converter-moshi:$retrofit_version")

    val moshi_version = "1.14.0"
    implementation("com.squareup.moshi:moshi:$moshi_version")
    implementation("com.squareup.moshi:moshi-kotlin:$moshi_version")
    ksp("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version")

   // Retrofit2でデバッグ用にログ出力するやつ
   // https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
   val okhttp_logging = "4.12.0"
   implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_logging")

.

🟩 API_KEY や BASE_URL を安全に管理する「secrets-gradle」も追加

Secrets Gradle

.

💡 app / build.gradleの上部

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    // https://github.com/google/secrets-gradle-plugin/tree/main 
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") 👈追加
    id("com.google.devtools.ksp")
}

.

💎 proj / build.gradleの上部

buildscript {
    dependencies {
        classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") 👈追加
    }
}

plugins {
    id("com.android.application") version "8.2.0" apply false
    id("org.jetbrains.kotlin.android") version "1.9.22" apply false
    id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
}

.

🟩 AndroidManifestから、インターネットを使用できるようにする

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" /> 👈追加

.

🟩 Moduleを追加する

Moduleの分け方は、以下の2種がある

  • パッケージ単位でまとめる
  • 機能単位でまとめる(以下例はコッチ)

.

🟡ApiModule.kt

import com.example.udmnewsapiclient.BuildConfig
import com.example.udmnewsapiclient.data.api.NewsApiService
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton


/**
 * Created by K.Kobayashi on 2022/05/19.
 */
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    /**
     * Retrofit2でデバッグ用にログ出力する設定
     *   https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
     *   https://square.github.io/okhttp/features/interceptors/
     *   
     * ◆ログレベル
     *      logger.levelで渡しているlevelには4種あり、出力の詳細さが変わる
     *      ★ NONE
     *        ログを出力しない
     *      ★ BASIC
     *        リクエストラインとレスポンスラインのみを出力
     *      ★ HEADER
     *        BASICのログに加えてリクエストヘッダとレスポンスヘッダが出力される
     *      ★ BODY
     *        HEADERのログに加えて、クエストボディとレスポンスボディが出力される
     */
    @Singleton
    @Provides
    fun provideOkhttpClient(): OkHttpClient {
        val logInterceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }
        return OkHttpClient.Builder()
            .addInterceptor {
                // 💡【下記処理について】
                //     特にリクエストに変更を加えたいが元のリクエストを変更せずに残したい場合に有用な処理

                // リクエストからHTTPのURLを取得
                val httpUrl = it.request().url
                // 元のリクエストからコピーを作成し、url(httpUrl)メソッドを使用して新しいURLに更新
                val requestBuilder = it.request().newBuilder().url(httpUrl)
                // 更新されたリクエストを使って処理を継続
                it.proceed(requestBuilder.build())
            }
            .addInterceptor(logInterceptor)
            // 以下3つのタイムアウト:定義なし=デフォルト10秒がセットされる(※ほとんどの場合、デフォルトで問題ない=定義不要)
            // "接続タイムアウト"の設定:
            .connectTimeout(30, TimeUnit.SECONDS)
            // 読み取りタイムアウトの設定:個々の結果取得到着時間
            .readTimeout(20, TimeUnit.SECONDS)
            // データ送信タイムアウト設定:個々の書き込み IO 操作時間
            .writeTimeout(25, TimeUnit.SECONDS)
            .build()
    }


    @Singleton
    @Provides
    fun provideMoshi(): MoshiConverterFactory {
        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
        return MoshiConverterFactory.create(moshi)
    }

    @Singleton
    @Provides
    fun provideRetrofit(moshiConverterFactory: MoshiConverterFactory): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .addConverterFactory(moshiConverterFactory)
            .build()
    }

    @Singleton
    @Provides
    fun provideNewsApiService(retrofit: Retrofit): NewsApiService =
        retrofit.create(NewsApiService::class.java)
}

🟩 Retrofit2の自動生成用のInterfaceを作成

interface NewsApiService {

    @GET("/v2/top-headlines")
    suspend fun getTopHeadlines(): Response<ApiResponse>
}

🟡

.

🟩 戻り値型の「Call」「Response」の違い

どちらも非同期処理としたい時に、使用する戻り値の型。

  • Call:  Retrofit 2.6 ”より前”
  • Response: Retrofit 2.6 ”以降”

※非同期にする必要がないなら、戻り値の型は、Response とはせず、ただの T

🟡

.

🟩 以降の引用参考サイト

https://qiita.com/SYABU555/items/3b280a8e81d2cc897383

Retrofit2公式サイトに謳われているとおり、簡単にAPIコールするためのライブラリです。 また、バックグラウンドで行なってコールバックを指定することも簡単にできます。

.

🟩アノテーション

APIを表現、定義します。   例えば、こちらのフリーAPIを参考にさせてもらいます   https://randomuser.me/api      このとき、ここで定義するのは規定URL https://randomuser.me/ 以降の部分になります  

interface NewsApiService {

    @GET("/v2/top-headlines")
    suspend fun getTopHeadlines(
        @Query("country")
        country: String,
        @Query("page")
        page: Int,
        @Query("apiKey")
        apiKey: String = BuildConfig.NEWS_API_KEY,
    ): Response<NewsResponse>
}

もちろん、@POST, @PUT も用意される。

パラメータの渡し方、パスの作成方法など、
細かい使い方にも触れておきます

.

🟩 REQUEST METHOD

もっともシンプルな形

@GET("users/list")

このように直接Queryパラメータを書くことも可能です

@GET("users/list?sort=desc")

これはGETに限らず、POST、PUTも同様です

.

🟡 @Path

URLのパス途中に何かを入れたい場合はこのように
{}で囲んで命名し、@Path をNameと一緒に定義することで、渡したパラメータがURLパスの中に代入されます

@GET("group/{id}/users")
fun groupList(@Path("id") groupId : Int) : Response<List<User>>

.

🟡 @Query

URLの最後につく?以降の要素はこのように書きます   

@GET("group/{id}/users")
fun groupList(@Path("id") groupId: Int, @Query("sort") sort: String): Response<List<User>>

これを実行すると、以下のようなURLになる

https://randomuser.me/group/3/users?sort=desc
注意:例なので、動くURLではないです

.

🟡 @BODY

あるインスタンスがもつ要素をkey, value変換して
APIで送りたい、というときには、そのインスタンスクラスを引数にするインターフェースを用意し、@Bodyをつけます。

@POST("users/new")
fun createUser(@Body user: User): Response<User>

ここでのUserは、後述する自作Modelクラスであり、
どのKeyがどのインスタンス変数に該当するかというのを記載する必要がありますが、
そうすることで自動的に変換してJsonに変換して送ることができます

.

🟡 @FormUrlEncoded + @Field

変数をkey, valueで変換して送りたい場合(Form指定)は
以下のように@FormUrlEncodedをつけ、送りたいデータには@Fieldをつけます。

@FormUrlEncoded
@POST("user/edit")
fun updateUser(@Field("first_name") first: String, @Field("last_name") last: String): Response<User>

また、Multipart との併用はできません

.

🟡 @Multipart

Maltipart指定してデータを送りたい場合にも 以下のように@Multipartをつけ、送りたいデータには@Partあるいは@PartMapをつけます。

@Multipart
@PUT("user/photo")
fun updateUser(@Part("photo") photo: RequestBody, @Part("description") description: RequestBody): Response<User>

また、FormUrlEncodedとの併用はできません

.

🟡 @Headers

ヘッダの指定もできます

  • 静的指定
  • 単体指定
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
fun widgetList(): Response<List<Widget>>

.

💎複数指定

@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
fun getUser(@Path("username") username: String): Response<User>

.

💎動的指定

@GET("user")
fun getUser(@Header("Authorization") authorization: String): Response<User>

.

🟩 Model

受け取り(あるいは送信)するデータのModelクラスを作成します。
今回は例として、こちらのvalueがランダムになるフリーAPIをお借りします

https://randomuser.me/api

{
    "info": {
        "page": 1, 
        "results": 1, 
        "seed": "7aed050e936ddbda", 
        "version": "1.1"
    }, 
    "results": [
        {
            "cell": "0486-036-722", 
            "dob": "1979-04-08 06:14:29", 
            "email": "[email protected]", 
            "gender": "male", 
            "id": {
                "name": "TFN", 
                "value": "023064228"
            }, 
            "location": {
                "city": "port macquarie", 
                "postcode": 1650, 
                "state": "western australia", 
                "street": "3769 dogwood ave"
            }, 
            "login": {
                "md5": "e7f8bb220a82a28ad9375081f022ff2f", 
                "password": "aaaaaaa", 
                "salt": "Q6J1Te1h", 
                "sha1": "e2ec839f88e8fdaddbb2a83877d9afd8342bf26c", 
                "sha256": "a602c4c64dc6fc1b94d6c98eedbf4383d79cd8894e21cc9f37f9fca3000cea05", 
                "username": "greenduck262"
            }, 
            "name": {
                "first": "roger", 
                "last": "holt", 
                "title": "mr"
            }, 
            "nat": "AU", 
            "phone": "09-2659-5113", 
            "picture": {
                "large": "https://randomuser.me/api/portraits/men/78.jpg", 
                "medium": "https://randomuser.me/api/portraits/med/men/78.jpg", 
                "thumbnail": "https://randomuser.me/api/portraits/thumb/men/78.jpg"
            }, 
            "registered": "2004-12-21 07:19:21"
        }
    ]
}

で、2階層目まで変換したものがこちら
それぞれのkey名が変数名に該当します

data class RandomUserDemo(var info: Info,
                          var results: List<Result>)

data class Info(var seed: String,
                var results: Int,
                var page: Int,
                var version: String)

data class Result(var gender: String,
                  var email: String,
                  var registered: String,
                  var dob: String,
                  var phone: String,
                  var cell: String)

.

🟡 Gson利用の場合のモデル

Key名と変数名を違うものにしたい、という場合は
@SerializedNameをつけることで変更可能です

data class Result(var gender: String,
                  var email: String,
                  var registered: Int,
                  var dob: Int,
                  var phone: Int,
                  var cell: Int,
                  @SerializedName("id") var idMap: IdMap)

data class IdMap(var name: String,
                 var value: String)

.

🟡 @Transient

逆に、key,value に変換したくない要素もあると思います。
そんなときはTransientを使いましょう。

JavaとKotlinで書き方が少し違うのでどちらも載せます

public class IdMap {
    public long transient id = 0;
}
data class IdMap(@Transient var id: Long,
                 var name: String,
                 var value: String)

.

🟩 HttpClient

ログやヘッダー、基底URLの定義などを行い、
HttpClientを作成します

    val httpBuilder: OkHttpClient.Builder get() {
        // create http client
        val httpClient = OkHttpClient.Builder()
                .addInterceptor(Interceptor { chain ->
                    val original = chain.request()

                    //header
                    val request = original.newBuilder()
                            .header("Accept", "application/json")
                            .method(original.method(), original.body())
                            .build()

                    return@Interceptor chain.proceed(request)
                })
                .readTimeout(30, TimeUnit.SECONDS)

        // log interceptor
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        httpClient.addInterceptor(loggingInterceptor)

        return httpClient
    }

.

🟩【Gson】Retrofit instance

HttpClientを使い、Retrofitフレームワークを経由して
APIを定義したinterfaceをインスタンス化します

    // core for controller
    val service: IApiService = create(IApiService::class.java)

    lateinit var retrofit: Retrofit

    fun <S> create(serviceClass: Class<S>): S {
        val gson = GsonBuilder()
                .serializeNulls()
                .create()

        // create retrofit
        retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create(gson))
                .baseUrl("http://randomuser.me/") // Put your base URL
                .client(httpBuilder.build())
                .build()

        return retrofit.create(serviceClass)
    }

このserviceがinterfaceであり、実際にAPIコールを行う際に利用するものです

.


以降古いやり方:🟩Execute(※注意※ Callが戻り値の古いやり方?)


実行は以下のように行います

.

🟡 同期

try {
      val response = API.apiDemo().execute()
      if (response.isSuccessful()) {
          return response.body()
      } else {
          // failed
      }
} catch (e: IOException e) {
      e.printStackTrace()
}  

.

🟡 非同期

参考ソースはJavaですが、こんな感じで非同期にします。 ただお勧めとしてはRxJavaを利用すると、スッキリかけます。

API.apiDemo().enqueue(new Callback<RandomUserDemo>() {
         @Override
         public void onResponse(Call<RandomUserDemo> call, retrofit2.Response<RandomUserDemo> response) {
            RandomUserDemo demo = response.body();
         }

         @Override
         public void onFailure(Call<RandomUserDemo> call, Throwable t) {
         }
});

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

⚠️ **GitHub.com Fallback** ⚠️