ヒルトとは何ですか?
Hilt は、2020 年に Jetpack ファミリに加わった、強力で使いやすい依存関係注入フレームワークです。これは、Dagger2 チームと協力して Android チームによって開発された Android 専用の依存関係注入フレームワークです。Dagger2 と比較すると、Hilt の最も明白な特徴はそのシンプルさであり、Android 固有の API を提供します。
Hilt をプロジェクトに導入する
このセクションでは、例として Java 17 を使用する新しい Jetpack Compose プロジェクトを取り上げ、開発ツールは Android Studio 2023.1.1 Canary バージョンを使用します。2023年5月現在の情報です。
最初のステップは、gradle/libs.versions.toml ファイルを開き、Hilt の Gradle プラグインの関連構成を追加することです。
[versions]
hilt = "2.46.1"
[plugins]
hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
次に、プロジェクトの build.gradle.kts ファイルを開いてプラグインをインポートします。
plugins {
alias(libs.plugins.hiltAndroid) apply false
}
2 番目のステップは、Hilt のプラグインと依存ライブラリを libs.versions.toml に追加することです。
[libraries]
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
Hilt はコンパイル時のアノテーションに基づいて実装されるため、kotlin-kapt プラグインを追加する必要があります。次の構成をアプリの build.gradle.kts ファイルに追加します。
plugins {
kotlin("kapt")
}
android {
compileOptions {
// 这里设置为 Java 8 或以上即可
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}
これで、Hilt がプロジェクトに正常に導入されました。
ヒルトの基本的な使い方
準備
Hilt を使用する場合は、Application クラスをカスタマイズする必要があります。カスタマイズしないと、Hilt が正しく動作しません。カスタム Application クラスにコードを記述する必要はありませんが、@HiltAndroidApp アノテーションを追加する必要があります。
@HiltAndroidApp
class MyApplication : Application() {
}
次に、MyApplication を AndroidManifest.xml に登録します。
<application
android:name=".MainApplication">
</application>
準備作業は完了しました。次のタスクは、特定のビジネス ロジックに従って Hilt を使用して依存関係を注入することです。
エントリーポイント
Hilt は Dagger2 の操作を簡素化するため、@Component アノテーションを使用してブリッジ層ロジックを記述する必要がなくなります。また、注入関数がいくつかの Android 固定エントリ ポイント (アプリケーション、アクティビティ、フラグメント、ビュー) からのみ開始されるように制限されます。 、サービス、ブロードキャストレシーバー。
このうち、Application エントリ ポイントのみ @HiltAndroidApp アノテーションを使用して宣言され、他のすべてのエントリ ポイントは @AndroidEntryPoint アノテーションを使用して宣言されます。たとえば、アクティビティで依存関係の注入を行う場合は、次のように宣言するだけです。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
パラメータなしの依存性注入
次のように、クラスを定義し、そのコンストラクターで @Inject アノテーションを宣言してみます。
class MusicPlayer() @Inject constructor() {
fun init() {
Log.d("MusicPlayer", "init")
}
}
Activity に注入すると、上記で記述した init() メソッドを正常に呼び出すことができます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var musicPlayer: MusicPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
musicPlayer.init()
}
}
パラメーターを使用した依存関係の注入
次のように、プレーヤー コンポーネントが依存するシステム オーディオ ドライバーを表す AudioDriver パラメーターを上記の MusicPlayer クラス コンストラクターに追加します。
class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {
fun init() {
Log.d("MusicPlayer", "init, audioDriver=$audioDriver")
}
}
AudioDriver クラスを宣言するときは、そのコンストラクターに @Inject アノテーションも追加します。
class AudioDriver @Inject constructor() {}
コードを変更せずに、init() メソッドを正常に呼び出して、audioDriver の hashCode を正常に出力できます。
インターフェース依存性の注入
オーディオの再生時に必要なオーディオ デコーダを表す IDecoder インターフェイスを定義します。インターフェイスには 2 つのメソッドが実装されており、それぞれデコーダの作成と破棄、およびメモリの解放に使用されます。
interface IDecoder {
fun create()
fun destroy()
}
WAV ファイルをデコードするために WavDecoder を実装し、コンストラクターに @Inject アノテーションを追加します。
class WavDecoder @Inject constructor() : IDecoder {
override fun create() {
Log.d("WavDecoder", "create")
}
override fun destroy() {
Log.d("WavDecoder", "destroy")
}
}
さらに、MP3 ファイルをデコードするために Mp3Decoder を実装するには、 @Inject アノテーションを宣言する必要もあります。
class Mp3Decoder @Inject constructor() : IDecoder {
override fun create() {
Log.d("Mp3Decoder", "create")
}
override fun destroy() {
Log.d("Mp3Decoder", "destroy")
}
}
DecoderModule という名前の新しい抽象クラスを作成します。このモジュールでは、抽象関数を定義して、IDecoder インターフェイスの必要なインスタンスを提供します。
@Module
@InstallIn(ActivityComponent::class)
abstract class DecoderModule {
@Binds
abstract fun bindDecoder(wavDecoder: WavDecoder): IDecoder
}
MusicPlayer クラスのコードを変更して、先ほど提供したデコーダーを呼び出します。
class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {
@Inject
lateinit var decoder: IDecoder
fun init() {
decoder.create()
Log.d("MusicPlayer", "init, audioDriver=$audioDriver")
decoder.destroy()
}
}
このとき、再度 init() メソッドを呼び出すと、TAG が WavDecoder のログが表示されます。
同じタイプの異なるインスタンスを挿入する
@Qualifer インターフェースは、同じタイプのクラスまたはインターフェースの異なるインスタンスを注入するために使用されます。次のように 2 つのアノテーションを個別に定義します。
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindWavDecoder
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindMp3Decoder
DecoderModule に戻り、2 つの抽象関数を定義し、その 2 つの関数の上に定義した 2 つのアノテーションを追加します。
@Module
@InstallIn(ActivityComponent::class)
abstract class DecoderModule {
@BindWavDecoder
@Binds
abstract fun bindWavDecoder(wavDecoder: WavDecoder): IDecoder
@BindMp3Decoder
@Binds
abstract fun bindMp3Decoder(mp3Decoder: WavDecoder): IDecoder
}
MusicPlayer クラスに戻ると、プレーヤーで 2 つのデコード形式を同時にサポートさせることができます。
class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {
@BindWavDecoder
@Inject
lateinit var wavDecoder: IDecoder
@BindMp3Decoder
@Inject
lateinit var mp3Decoder: IDecoder
fun init() {
wavDecoder.create()
mp3Decoder.create()
Log.d("MusicPlayer", "init, audioDriver=$audioDriver")
wavDecoder.destroy()
mp3Decoder.destroy()
}
}
サードパーティクラスの依存関係の注入
OkHttp が提供する MainActivity に OkHttpClient を注入したい場合、そのコンストラクターに @Inject アノテーションを追加することはできません。この場合、@Module アノテーションを使用して非抽象クラス (ここでは NetworkModule という名前) を定義する必要があります。
次のように、このクラスでメソッドを定義し、 @Provides アノテーションを追加して、関数本体に OkHttpClient のインスタンスを指定します。
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
}
MainActivity に戻り、@Inject を使用して OkHttpClient を注入すると、正常に実行されます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var okHttpClient: OkHttpClient
}
開発者の便宜を図るため、NetworkModule で Retrofit タイプのインスタンスを提供し、次のコードを記述します。
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}
}
ProvideRetrofit() メソッドの okHttpClient パラメーターは、provideOkHttpClient() メソッドを使用して Hilt によって自動的に作成されます。この時点で、MainActivity に Retrofit を再度注入してみてください。これも正常に実行できます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var retrofit: Retrofit
}
ヒルト内蔵コンポーネント
@Module を使用して注入されたクラスは、@InstallIn アノテーションを使用して注入の範囲を指定する必要があります。Hilt は、さまざまなシナリオに挿入するために使用される合計 7 種類のコンポーネントを提供します。
コンポーネント名 | 射出範囲 |
---|---|
アプリケーションコンポーネント | 応用 |
アクティビティ保持コンポーネント | ビューモデル |
アクティビティコンポーネント | アクティビティ |
フラグメントコンポーネント | 断片 |
ビューコンポーネント | 意見 |
ViewWithFragmentComponent | @WithFragmentBindings を使用して定義されたビュー |
サービスコンポーネント | サービス |
上記で定義した NetworkModule をプロジェクト全体で使用したい場合は、次のように変更します。
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
}
ヒルトコンポーネントのスコープ
デフォルトでは、Hilt は依存関係注入動作ごとに異なるインスタンスを作成します。前述の 7 つの組み込みコンポーネントに対応して、Hilt は次の 7 つのコンポーネント スコープ アノテーションも提供します。
コンポーネントの範囲 | 対応内蔵コンポーネント |
---|---|
@シングルトン | アプリケーションコンポーネント |
@ActivityRetainedScope | アクティビティ保持コンポーネント |
@ActivityScoped | アクティビティコンポーネント |
@FragmentScoped | フラグメントコンポーネント |
@ViewScoped | ビューコンポーネント |
@ViewScoped | ViewWithFragmentComponent |
@ServiceScoped | サービスコンポーネント |
NetworkModule でグローバルに提供される Retrofit と OkHttpClient のインスタンスを 1 つだけ作成したい場合は、 @Singleton アノテーションを追加するだけです。
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}
}
スコープ アノテーションは、前に追加した AudioDriver クラスなどの注入可能なクラスのすぐ上で宣言することもできます。
@Singleton
class AudioDriver @Inject constructor() {
}
これは、AudioDriver が同じインスタンスをグローバルに共有し、AudioDriver クラスに依存関係をグローバルに注入できることを意味します。
上図のように、あるクラスに対して、あるスコープのアノテーションを宣言した後、そのアノテーションの矢印が指しているところであれば、そのクラスに対して依存性注入を行うことができ、同時にクラス内で同じインスタンスを共有することができます。スコープ。
プリセット修飾子
上記で定義された AudioDriver クラスに Context パラメーターが必要な場合は、パラメーターの前に @ApplicationContext アノテーションを追加する必要があります。これにより、Hilt が Application type Context を AudioDriver クラスに提供し、コードをコンパイルして渡すことができます。
@Singleton
class AudioDriver @Inject constructor(@ApplicationContext val context: Context) {
}
アクティビティまたは他のタイプのコンテキストが必要な場合は、Hilt によってプリセットされた別の修飾子を使用できます。
@Singleton
class AudioDriver @Inject constructor(@ActivityContext val context: Context) {
}
このとき、AudioDriver クラスは Singleton であり、Qualifier のスコープと一致しないため、コードのコンパイル時にエラーが報告されます。
Hilt にはアクティビティとアプリケーションの 2 種類のインジェクション機能がプリセットされています。クラスがアクティビティまたはアプリケーションに依存している場合、Hilt は次のようにアノテーションを追加せずにクラスを自動的に識別できます。
class AudioDriver @Inject constructor(val application: Application) {
}
class AudioDriver @Inject constructor(val activity: Activity) {
}
2 つのタイプは Application と Activity でなければならないことに注意してください。それらのサブタイプが宣言されている場合でも、コンパイルは失敗します。
HiltViewModelの使用
まず、次のように libs.versions.toml で関連する依存関係を宣言します。
[versions]
hilt-lifecycle-viewmodel = "1.0.0-alpha03"
[libraries]
androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-lifecycle-viewmodel" }
build.gradle.kts に依存関係を追加します。
dependencies {
kapt(libs.androidx.hilt.compiler)
}
@HiltViewModel アノテーションを介して ViewModel を提供します。
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
}
次に、 @AndroidEntryPoint のアノテーションが付けられたアクティビティまたはフラグメントは、ViewModelProvider または viewModels() 拡張機能を使用して、通常どおり ViewModel インスタンスを取得できます。
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
}