LoginSignup
2
5

AndroidでHiltをつかう(その2)

Last updated at Posted at 2021-09-09

Androidで主に使用される依存性注入は、コンストラクタインジェクションとフィールドインジェクション。

用語
  • コンストラクタインジェクション
    クラスの生成時に依存させるインスタンス(使うインスタンス)を渡す方法
  • フィールドインジェクション
    クラスの生成後に依存させるインスタンス(使うインスタンス)をsetterメソッドで渡す方法
    ActivityやFragmentの引数を変更することはできないのでこの方法を採る。

インジェクションの基本

  1. Applicationクラスに「@HiltAndroidApp」をつける(Hiltが使用可能になる)
  2. フィールドインジェクションされたいクラスに「@AndroidEntryPoint」をつける
    フィールドに「@Inject」をつける(フィールドインジェクション)。lateinit宣言も必要。
  3. フィールドに挿入したい(バインディングしたい)クラスのコンストラクタに「@Inject」をつける(コンストラクタインジェクションを用いることでHiltにバインディング情報を伝える)

(注意1)
クラスに@AndroidEntryPointを付けると、個別の Hilt コンポーネントが生成される
(注意2)
@AndroidEntryPointを付ける場合は、それに依存するAndroidクラスにもアノテーションを付ける必要がある。たとえば、フラグメントにアノテーションを付ける場合は、そのフラグメントを使用するアクティビティにもアノテーションを付ける必要がある
(注意3)
@AndroidEntryPointをつけれるクラスは限定されている。
Hiltでサポートされていないクラスには、フィールドインジェクションを行う。参考
HiltでサポートされているAndroidクラス

クラス名
Application @HiltAndroidApp
ViewModel @HiltViewModel
Activity @AndroidEntryPoint
Fragment @AndroidEntryPoint
View @AndroidEntryPoint
Service @AndroidEntryPoint
BroadcastReceiver @AndroidEntryPoint

(注意4)
@Inject」をつけたクラスのコンストラクタが引数を持つ場合は、引数のクラスにも「@Inject」をつける必要がある?

//基本の1と2を実施
@AndroidEntryPoint
class LogsFragment : Fragment() {
    //lateinitすると初期化後はnon-nullが保証される
    @Inject lateinit var dateFormatter: DateFormatter
    ・・・
}
//基本の3を実施
class DateFormatter @Inject constructor() { ... }

b.png

上のような依存関係となり、LogsFragmentとDateFormatterには依存関係がない。これがHiltを使用するメリット

スコープ

フィールドインジェクションができるAndroidクラスには対応するHiltコンポーネントがある。このHiltコンポーネントにバインディング(クラスのインスタンスの情報)を紐つけることでスコープ設定を実現する。
デフォルトでは、バインディングにはスコープ設定されない。アプリがバインディングをリクエストするたびに、必要な型の新しいインスタンスが作成されるが、バインディングしたいクラスの上にスコープアノテーションをつけると、対応するコンポーネントのインスタンスごとに1回だけ作成される。
例、バインディングする(コンストラクタインジェクションしている)Adapterクラスに@ActivityScopedを設定している場合、対応するアクティビティが存続する間、常に同じAdapterインスタンスが提供される。

スコープアノテーションを付けたバインディングは以下の対応関係でコンポーネントに紐付き、ライフタイムはAndroidクラスのライフサイクルと関係する。onCreate()で生成されonDestroy()で破棄等。詳細は公式ドキュメントで。

Androidクラス @InstallInで参照できるHiltコンポーネント スコープ
Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScope
View Model ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
@WithFragmentBindings アノテーションが付いたView ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

ActivityのスコープをSingletonComponentにスコープ設定したい場合、バインディングのスコープは、インストール先のコンポーネントのスコープと一致する必要があるため、@InstallIn(SingletonComponent::class)とする

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

この例あっている?
@InstallIn(SingletonComponent::class)
@Singleton
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

「コンポーネントのデフォルト バインディング」の項は謎。

参考URL
https://developer.android.com/training/dependency-injection/hilt-android#generated-components

Androidクラス コンポーネント
@InstallIn Hiltがコンポーネントを生成する際に、アノテーションされたクラスをどのコンポーネントに含めるかを宣言するアノテーションです。
@

ここから下は応用。

Hiltモジュール

インターフェース、ソースコード外のクラスは、コンストラクタインジェクションができない。このような場合は、Hiltモジュールを使用してバインディングする

(注意)
スコープが異なるコンテナに設定されている場合、同じモジュールを使用することはできない。

自分で所有していないクラスの場合

  1. @Moduleをつける
  2. @InstallInで、バインディングを使用できるコンテナ(コンポーネント)を指示
  3. 自分で所有していないクラスを返却するメソッドに@Providesをつける
@InstallIn(SingletonComponent::class)
@Module
object MyModule {
    @Provides
    fun f(@ApplicationContext appContext: Context): Context {
        return appContext
    }
}

Providesする関数の命名に法則はない。
返却する型が合致してさえいればよい。
コンストラクタインジェクションなのでバッティングすることはない。
InstallInは、SingletonComponent他、ActivityComponentがある

interfaceを返すクラスの場合

  1. Hiltモジュールファイルを作る(モジュールファイルには@Bindsアノテーションと@Providesアノテーションは共存できない)
  2. インジェクションしたいinterfaceを返すクラスに@Injectをつける(constructor()は省略できなくなる)
  3. インジェクションされる側のフィールドに@Injectをつける
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}

同じ型の異なる実装(複数のバインディング)を提供する方法

@Qualifierで識別させる

@Qualifier
annotation class InMemoryLogger

@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {

    @InMemoryLogger//独自アノテーション
    @ActivityScoped
    @Binds
    abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}

ContentProviderにインジェクションする場合

ContentProviderはHiltでサポートされないため、エントリーポイントを@AndroidEntryPointで指定できない。

  1. ContentProvider内にをバインディングしたいコンポーネントを返すインターフェースを作成する
  2. 上記インターフェイスに@EntryPointをつける
  3. エントリポイントにアクセスするには、EntryPointAccessorsを使う
class LogsContentProvider: ContentProvider() {
    @InstallIn(SingletonComponent::class)
    @EntryPoint
    interface LCPEntryPoint {
        fun logDao(): LogDao
    }
    ・・・
    private fun getLogDao(appContext: Context): LogDao {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            appContext,
            LCPEntryPoint::class.java
        )
        return hiltEntryPoint.logDao()
    }
2
5
1

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
2
5