🍼

KotlinでHiltを用いてRetrofitをAPIにDIしていく

2023/10/31に公開

◆タイトル分析

タイトルだけだとナンノコッチャと思われるので、タイトル分析から始める。

  • Hilt

HiltはAndroid用の依存関係インジェクションライブラリ

(超ざっくり言うと)開発しやすくなるライブラリ

  • Retrofit

Retrofitとは、型安全な Android 向けの HTTP クライアントライブラリ

(超ざっくり言うと)インターネットの情報(API)を取得するためのライブラリ

  • API
    (超ざっくり言うと)インターネット上の情報

  • DI(Dependency Injection)

クラスで使用するオブジェクトの生成の自動化

(超ざっくり言うと)勝手に自動生成して、勝手に注入して、勝手に色々やってくれること

まとめると、API呼び出し処理のときにRetrofitを定義したりして、なんやかんや定義しないといけない処理を、Hiltっていうライブラリ使って、対象APIを呼び出したときに、自動的に処理しちゃうようにしておこうぜ的なこと。この内容を「DIする」とか呼んでる。

◆Hiltを用いてRetrofitをAPIにDIする実装例

ライブラリの導入等は、ここでは省きます。他の記事を参照するか、もし、他記事を参照するのが面倒の場合は、自分個人のGithubのリンクを貼るので、それを参照してください。

QiitaApi

interface QiitaApi {

    @GET("items")
    suspend fun articleItems(): Response<List<Article>>
}

これはAPI受け取るためのインターフェース。今回はQiitaの記事を取得するための関数を作成。

ApiModule

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

    @Provides
    @Singleton
    fun provideOkhttpClient(): OkHttpClient {
        val logInterceptor = HttpLoggingInterceptor()
        logInterceptor.level = HttpLoggingInterceptor.Level.BODY

        return OkHttpClient.Builder()
            .addInterceptor {
                val httpUrl = it.request().url
                val requestBuilder = it.request().newBuilder().url(httpUrl)
                it.proceed(requestBuilder.build())
            }
            .addInterceptor(logInterceptor)
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .build()
    }

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

    @Provides
    @Singleton
    fun provideRetrofit(
        okHttpClient: OkHttpClient,
        moshiConverterFactory: MoshiConverterFactory,
    ): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl("https://qiita.com/api/v2/")
            .addConverterFactory(moshiConverterFactory)
            .build()
    }

    @Provides
    @Singleton
    fun provideQiitaService(retrofit: Retrofit): QiitaApi = retrofit.create(QiitaApi::class.java)

}

これが、今回の記事で深掘りしたいメインオブジェクト。自分は最初見たとき、よくわからなかった。

最初に言うと、このオブジェクトで定義している関数は、手動で他のところで呼び出すことはない関数である(筆者はここがよく分からないポイントMAXで、理解するまで非常にこんがらかったところだった)。

Hiltアノテーション(詳細は後述)をつけて、型指定あり関数を定義すると、自動的にクラスが生成される(自動生成されたクラスは意識しなくていい)。その関数に定義した型を、別の関数の引数に定義すると、定義した型に、ApiModuleで定義した処理を自動で実行してくれるもの。

...とまぁ、自分なりに汎用的な言葉で説明してみたものの、具体例がないと、イマイチわかりにくいと思うので、具体的に上記のサンプルを用いて解説する。

■各アノテーションの意味

その前に、各アノテーションの意味について解説していく。下記表に簡単にまとめた

アノテーション ざっくり意味
@Module Hiltモジュールで有ることを指定(お作法と覚えておけばヨシ)
@InstallIn(SingletonComponent::class) インスタンスの寿命の指定。SingletonComponent::classはアプリケーション全体が寿命(※1)
@Provides このアノテーションつけた関数の型のインスタンスを作成したときに、自動的に関数内の処理もやるよっていう指定(※2)
@Singleton @InstallIn(SingletonComponent::class)指定時に@Providesと一緒に指定するアノテーション(※3)

※1:その他の指定は、下記の公式サイトの「コンポーネントのスコープ」にある表の「生成されたコンポーネント」に紐づく値を参照してください。

※2:詳細は下記の公式サイトの「@Provides を使用してインスタンスを注入する」を参照してください。

※3:@Providesと一緒につけるアノテーションは下記の公式サイトの「コンポーネントのスコープ」にある表の「生成されたコンポーネント」に紐づく値であると思われる。(例:@InstallIn(ActivityRetainedComponent::class)なら、@ActivityRetainedScopedが@Providesと一緒に指定するアノテーションになる...(はず))

https://developer.android.com/training/dependency-injection/hilt-android?hl=ja

■具体的な実装を用いた解説

前述した、ApiModuleの中身を、切り取りながら解説していく。

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

    @Provides
    @Singleton
    fun provideOkhttpClient(): OkHttpClient {
        val logInterceptor = HttpLoggingInterceptor()
        logInterceptor.level = HttpLoggingInterceptor.Level.BODY

        return OkHttpClient.Builder()
            .addInterceptor {
                val httpUrl = it.request().url
                val requestBuilder = it.request().newBuilder().url(httpUrl)
                it.proceed(requestBuilder.build())
            }
            .addInterceptor(logInterceptor)
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .build()
    }

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

    @Provides
    @Singleton
    fun provideRetrofit(
        okHttpClient: OkHttpClient,
        moshiConverterFactory: MoshiConverterFactory,
    ): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl("https://qiita.com/api/v2/")
            .addConverterFactory(moshiConverterFactory)
            .build()
    }
    
    .......
    
}

ここでやってることは、Retrofit型のインスタンスが生成されたときに、自動的に処理したい内容を定義すること である。上記の定義では、「Retrofit型のインスタンス作ったら、QiitaのAPIに自動で接続してくれ〜。」という処理を定義している。Retrofitを使うために、OkHttpClientMoshiConverterFactoryもインスタンスが生成されたときに自動的に処理するように、@Providesのアノテーションをつけた関数を作成している。OkHttpClientMoshiConverterFactoryのインスタンス作成時に自動的に処理する関数の内容は、一般的に使用するための前準備の処理(ビルド)だと思ってくれればいい。

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

        .......

    @Provides
    @Singleton
    fun provideQiitaService(retrofit: Retrofit): QiitaApi = retrofit.create(QiitaApi::class.java)

}

ここでやってることは、QiitaApi型のインスタンス作成するときは、Retrofitを使ったQiitaのAPIに自動接続(Retrofit型の自動処理)すること である。その関数を作ることによって、「Hiltっていうライブラリ使って、対象APIを呼び出したときに、自動的に処理しちゃうようにしておこうぜ」が実現されることになる。

◆使用例

自動処理を包括させた、QiitaApi型(DIした型)を使用した具体例を下記に記載する。

QiitaRepository

class QiitaRepository @Inject constructor(
    private val qiitaApi: QiitaApi
) {
    suspend fun getArticle(): List<Article>? {
        var qiitaData: List<Article>? = null
        withContext(Dispatchers.IO) {
            runCatching {
                qiitaData = qiitaApi.articleItems().body()

            }.onFailure {
                // 例外処理は割愛
            }
        }
        return qiitaData
    }
}

このクラスでは、クラス名のあとに@Inject constructorを付与した上で、クラスにQiitaApi型コンストラクタを定義することで、ApiModuleで定義したRetrofitを使ったQiitaのAPIに自動接続(Retrofit型の自動処理)することが自動処理されるようになる。そのため、上記のqiitaApiは、定義した時点で、Retrofitを用いてQiitaのAPIに自動接続されている状態になっている。あとは、QiitaApiで定義したarticleItems()関数を呼び出すだけで、Qiitaの記事を取得できるようになっている。

★DIしない場合

DIしないと下記のようなコードになるはず。

class QiitaRepository() {
    private var service: QiitaApi = Retrofit.Builder()
        .baseUrl("https://qiita.com/api/v2/")
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
        .create(QiitaApi::class.java)

    suspend fun getArticles(): List<Article>? {
        var qiitaData: List<Article>? = null
        withContext(Dispatchers.IO) {
            runCatching {
                val response = service.articleItems().execute()
                if (response.isSuccessful) {
                    qiitaData = response.body()
                } else {
                    // 取得失敗処理は割愛
                }
            }.onFailure {
                // 例外処理は割愛
            }
        }
        return qiitaData
    }
}

と、こんな感じになる。QiitaのAPIに自動接続しないため、Retrofitの定義からしないといけない。これだと、1つならいいが、複数のリポジトリを作成する場合に、リポジトリごとにRetrofitの定義をしないといけないため、非常に面倒になってしまう(共通化させる他の方法はあると思うが、個人的にはHiltでDIする方法は、かなりスッキリしていてわかりやすい方法だと思う)。

また、筆者が遭遇したパターンでは、他の部分をHiltでDI化している状態で、APIを受け取るRepositoryクラスを上記のようにRetrofitから定義して取得を試みると、必ず例外処理になってしまい、取得にこけてしまうという現象に陥った。この現象は、moshiでパースするときのみ発生することを確認。Gsonでは取得できた。そのため、moshiとHiltも何らかの兼ね合いがあると推測される(詳細の原因はよくわかっていない)。Retrofitの接続をDI化したら取得できるようになったことも確認。最初、ApiModule部分は、何をしているかよくわからなかったため、実装を敬遠したら起こってしまった現象であった。

今回、記事を書いた経緯は、moshiを使用すると、APIの取得に必ずコケてしまうため、敬遠していたApiModuleの中身を自分なりに落とし込むという目的で調査して、備忘録気味に記事を作成した。

◆今回の不明点&解消

●不明点

ApiModuleで定義した関数が他のところで呼び出されていないこと

●解消

ApiModuleで定義した関数はHiltによって、依存注入して自動実行する(DIする)ために定義するもの。関数の戻り値に指定した型のインスタンスを作成するときに自動実行される関数の定義なので、他のところで呼び出すものではないということ。

定義した関数は、Hiltによって自動生成されるクラス内で使用されているので、自分で呼び出す必要がないだけで、全く使用されていないということではない。基本的に定義した関数は他で呼び出される原則は変わらない。ただ、今回のケースでは、インスタンス生成時にHiltによって自動的に関数が呼び出されるという特殊ケースであったということ。

ApiModule_ProvideQiitaServiceFactory.java(自動生成クラス)

@ScopeMetadata("javax.inject.Singleton")
@QualifierMetadata
@DaggerGenerated
@Generated(
    value = "dagger.internal.codegen.ComponentProcessor",
    comments = "https://dagger.dev"
)
@SuppressWarnings({
    "unchecked",
    "rawtypes",
    "KotlinInternal",
    "KotlinInternalInJava"
})
public final class ApiModule_ProvideQiitaServiceFactory implements Factory<QiitaApi> {
  private final Provider<Retrofit> retrofitProvider;

  public ApiModule_ProvideQiitaServiceFactory(Provider<Retrofit> retrofitProvider) {
    this.retrofitProvider = retrofitProvider;
  }

  @Override
  public QiitaApi get() {
    return provideQiitaService(retrofitProvider.get());
  }

  public static ApiModule_ProvideQiitaServiceFactory create(Provider<Retrofit> retrofitProvider) {
    return new ApiModule_ProvideQiitaServiceFactory(retrofitProvider);
  }

  public static QiitaApi provideQiitaService(Retrofit retrofit) {
    return Preconditions.checkNotNullFromProvides(ApiModule.INSTANCE.provideQiitaService(retrofit));
  }
}

上記は、ApiModuleで定義した関数名(provideQiitaService)で、AndroidStudioから全体検索をかけると出てくる。

◆調査してみての感想

各アノテーションの意味や、呼び出し先がないのに定義されている関数の使い道がよくわからないことで、使用を敬遠してたけど、今回、じっくり調査して、内容理解までできてよかった。自分の中でしっくり来ると、自分で使えるようになるし、応用もできるので、こういう内容の記事は積極的に投稿していきたいと思った。

また、Hiltや、DIについては、なんとなくしか理解してなかったが、今回調査して基本概念は掴めた手応えはある。やっぱり自分の言葉で落とし込むことが大切だと実感した(ただ、正しく伝わるかどうかは一定の不安はある)。

◆今後の展望

  • 別のAPIをRetrofitを用いて取得する方法。
    • オリジナルのアノテーションを追加で定義して、どのAPIなのか識別させる。
      • 追加したアノテーションをRetrofit型のインスタンス指定に付与して指定する
  • API取得して加工した結果を判定するテストコードの作成

◆その他

自分なりの言葉に落とし込んで解説してるので、間違っている可能性あります。間違いの指摘はコメント等で教えてくださりますと幸いです。また、補足等、なにかございましたらコメントいただきますと幸いです。

◆個人Githubリンク・参考記事まとめ

https://github.com/KZTkotlin/TryJetpackCompose/tree/develop
https://developer.android.com/training/dependency-injection/hilt-android?hl=ja
https://qiita.com/sudachi808/items/a05237e1294639ea41dd
https://qiita.com/shirahon/items/7fc23cef206536ad0636

Discussion