はじめに
Hiltのアルファ版が出たので調査しました。
あのわかりづらかったDagger2が非常にわかりやすくなっていました。導入の敷居がさがったと思います。そこで、「DIしたい、Hiltを使ってみたい、基本的なことを知りたい」という方に向け、サンプルアプリを作成しながら基本的なことを説明したいと思います。
なお、Dagger2からHiltへの移行方法などは記載していません。
いきなり実装
概要等の説明は省き、いきなり実装です。DIやHiltについては公式ページを参照してください。
今回作成するサンプルは、画面の中のボタンを押すとデータベースを検索し結果を表示するアプリです。
作成するクラスの構成は以下のようにします。
MainActivity --> SampleUseCase --> SampleRepository --> SampleDao --> DB
このアプリをHilt
を使って実装します。
データベースまわりはRoom
を使用しますがRoom
については触れません。
環境は以下のようになっています。
- Android Studio 4.0.1
- Android Virtual Device - android10.0 / 1080 x 2160
ライブラリの追加
まずは、ルートのbuild.gradle
の設定です。
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
// ・・・(省略)・・・
}
}
次に、app/build.gralde
の設定です。
apply plugin: 'dagger.hilt.android.plugin'
android {
// ・・・(省略)・・・
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
annotationProcessor "com.google.dagger:hilt-android-compiler:2.28-alpha"
// ・・・(省略)・・・
}
Applicationクラスの作成
次に、Applicationクラスを作成します。
Applicationクラスに@HiltAndroidApp
をつけるだけです。今まではDaggerApplication
を継承するか、HasAndroidInjector
をimplしていましたが、HiltではアノテーションをつけるだけでOKです。
Applicationクラスを作成したら、AndroidManifest.xmlに追加します。
@HiltAndroidApp
public class SampleApplication extends Application {
}
<application
android:name=".SampleApplication"
android:icon="@mipmap/ic_launcher">
・・・(省略)・・・
</application>
今まではAppComponentを作成していましたが、Hiltでは不要になりました。Dagger2からの移行の場合はバッサリ削除します。
// @Singleton
// @Component(modules={AndroidInjectionModule.class})
// public interface AppComponent extends AndroidInjector<SampleApplication> {
// ・・・(省略)・・・
// }
Activityに注入する
Activityに@AndroidEntryPoint
アノテーションを付けるとinjectできるようになります。
今までは、HasAndroidInjector
のimplや AndroidInjection.inject(this);
の実行などを行っていましたが、Hiltではアノテーションをつけるだけです。
今回のサンプルでは、MainActiviytにSampleUseCaseを注入します。
@AndroidEntryPoint // ・・・(1)
public class MainActivity extends AppCompatActivity {
@Inject // ・・・(2)
SampleUseCase useCase;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.execute);
button.setOnClickListener(v -> useCase.execute()); // ・・・(3)
}
}
(1) AndroidEntryPoint
アノテーションをつけます。
(2) Inject
アノテーションをつけます。 このuseCase
変数にHiltによってインスタンスが注入されます。
(3)ボタン押下時にuseCase
を実行します。 コード上はSampleUseCaseはnew していないのに実行できます。Hiltによってインスタンスが生成されて注入されているからです。
次に、SampleUseCaseです。
まずは、ログを出力するだけの実装です。
public class SampleUseCase {
private static String TAG = SampleUseCase.class.getName();
@Inject // ・・・(1)
public SampleUseCase() {
}
public void execute() {
Log.d(TAG, "実行!!");
}
}
(1) コンストラクタにInject
アノテーションをつけます。これがないとHilt管理のオブジェクトにならず、ActivityにInjectされません。
ここまでで実際に動きます。DIができています。
非常に簡単ですね。Dagger2のときと比べると圧倒的に簡単になっています。
Hiltモジュールの作成
上のサンプルではInjectするクラス(SampleUseCase)では、コンストラクタに@Inject
を指定しています。しかし、@Inject
を付与できないことがあります。たとえば、インターフェースの場合や、外部ライブラリのクラスなどです。
このような場合は@Module
アノテーションを付与したクラス作成し、インスタンスの生成方法をHiltに知らせます。
今回のサンプルでは、SampleUseCaseにインターフェースのSampleRepository
を呼び出すところが該当します。
SampleUseCaseクラスに実装を追加します。
public class SampleUseCase {
private static String TAG = SampleUseCase.class.getName();
@Inject
SampleRepository repository; // インターフェース
@Inject
public SampleUseCase() {
}
public void execute() {
Log.d(TAG, "実行!!");
// 実行します
List<SampleEntity> results = repository.find();
// 結果をログに表示
results.forEach(result -> {
Log.d(TAG, result.getName());
});
}
Binds
を使用してインスタンスを注入する
SampleRepository
の実装です。インターフェースと実体クラスは以下のようになります。
public interface SampleRepository {
List<SampleEntity> find();
}
public class SampleRepositoryImpl implements SampleRepository {
public static final String TAG = SampleRepositoryImpl.class.getName();
@Inject // ・・・(1)
public SampleRepositoryImpl() {
}
public List<SampleEntity> find() {
Log.d(TAG, "find!");
return null;
}
}
(1) 実体クラスのコンストラクタには@Inject
を付与します
これだけではInjectできません。
このインターフェースのインスタンスを生成する方法をHiltに教える必要があります。
Hiltモジュールは、@Module
アノテーションが付けられたクラスです。Daggerのモジュールとは異なり、Hiltは@InstallIn
アノテーションを付けて依存関係を指定します。
@Module // ・・・ (1)
@InstallIn(ApplicationComponent.class) // ・・・ (2)
abstract public class DataModule {
@Binds // ・・・ (3)
public abstract SampleRepository bindSampleRepository(SampleRepositoryImpl impl);
}
(1) @Module
アノテーションを付与しHiltモジュールクラスであることを宣言します。クラス名はなんでもよいです。
(2) このModuleの依存関係を指定します。この例では、ここで宣言したクラスたちは、アプリ内のどのクラスにでもInjectできます。この指定は以下の表のようにいろいろと指定することができます。
例えば、FragmentComponentを指定した場合は、FragmentにインジェクションできますがActivityにはインジェクションできません。今回はアプリのどのクラスにでも注入できるように、ApplicationComponentを指定します。
コンポーネント | インジェクションの対象 |
---|---|
ApplicationComponent | Application |
ActivityRetainedComponent | ViewModel |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | View |
ViewWithFragmentComponent | WithFragmentBindingsアノテーションが付いた View |
ServiceComponent | Service |
(3) Binds
アノテーションを付与し、どの実体を生成するか宣言します。メソッドの戻り値には、インターフェースを指定します。メソッドのパラメータで、生成したい実体を指定します。
Provides
を使用してインスタンスを注入する
Binds以外にもインスタンスの生成方法を指定することができます。
外部ライブラリはコンストラクタにインジェクションを付与できません。そのような場合にはProvides
を利用します。
今回のサンプルでは、SampleRepositoryImplがDaoを呼び出すところが該当します。
Room関連をDIするときの実装ですね。(Roomに関する説明はしません。別の記事でする予定です)
上のサンプルコードのDataMudule.java
に追加します。
@Module
@InstallIn(ApplicationComponent.class)
abstract public class DataModule {
@Provides // ・・・ (1)
@Singleton // ・・・ (2)
public static SampleDatabase provideDb(Application context) {
return Room.databaseBuilder(context.getApplicationContext(), SampleDatabase.class, "sample.db")
.addCallback(SampleDatabase.INITIAL_DATA)
.allowMainThreadQueries()
.build();
}
@Provides // ・・・ (3)
@Singleton
public static SampleDao provideSampleDao(SampleDatabase db) {
return db.getSampleDao();
}
@Binds
public abstract SampleRepository bindSampleRepository(SampleRepositoryImpl impl);
}
(1) Provides
アノテーションを付与し、どの実体を生成するか宣言します。
メソッドの戻り値には、生成したインスタンスです。パラメータは、Hiltが管理しているインスタンスを渡すことができます。
(2)このメソッドにはSingleton
アノテーションがついています。これはスコープの設定です。
通常、Hiltはリクエストがあるたびに、毎回、新しいインスタンスを作成します。これをアノテーションを付与することにより制御することができます。今回のサンプルは、Singleton
ですので、アプリケーションで1つのインスタンスの状態を実現します。(毎回、新しいインスタンスはつくりません)。どのクラスの注入しても同じインスタンスになります。
スコープは以下のようなものが用意されています。
Android クラス | 生成されたコンポーネント | スコープ |
---|---|---|
Application | ApplicationComponent | Singleton |
View Model | ActivityRetainedComponent | ActivityRetainedScope |
Activity | ActivityComponent | ActivityScoped |
Fragment | FragmentComponent | FragmentScoped |
View | ViewComponent | ViewScoped |
WithFragmentBindings | ViewWithFragmentComponent | ViewScoped |
Service | ServiceComponent | ServiceScoped |
例えば、今回のサンプルのDataModule.java
のInstantRunをActivityComponent
の変更し、SampleDaoをActivityScoped
に変更すると、Activityが存続する間は同じインスタンスになります。
SampleActivity、SampleUseCase、SampleRespositoryにDaoをInjectした場合、そのDaoはすべて同じインスタンスです。
サンプルアプリの実装に戻ります。
SampleRepositoryImplにDaoを注入して実装を完成させます。
public class SampleRepositoryImpl implements SampleRepository {
public static final String TAG = SampleRepositoryImpl.class.getName();
@Inject // ・・・(1)
SampleDao dao;
@Inject
public SampleRepositoryImpl() {
}
public List<SampleEntity> find() {
Log.d(TAG, "find!");
return dao.find(); // ・・・(2)
}
}
(1)SampleDaoを注入します。スコープはSingleton
なので毎回同じインスタンスが注入されます。
(2)newしていませんが、Hiltによって注入されるのでNullPointerExceptionにはなりません。
その他のコード
上記で説明していないサンプルアプリの、DaoとEntityを載せておきます。
@Entity(tableName = "sample")
public class SampleEntity implements Serializable {
@PrimaryKey
@NonNull
private String code;
private String name;
//setter/getter省略
}
@Dao
public interface SampleDao {
@Insert
long save(SampleEntity dto);
@Query("select * from sample")
List<SampleEntity> find();
}
完成!!
ここまでで、画面のボタンを押すと、ログに検索結果が表示されます。
Dagger2で必要だったものがほとんど不要となり、シンプルに実現できるようになりました。非常に簡単ですね。
まとめ
今回のサンプルを通じてHiltのポイントを整理します。
- Activityに
@AndroidEntryPoint
を付与する - Inject対象のクラスのコンストラクタに
@Inject
を忘れずにつける - インターフェースや他のライブラリをInject対象とするときは、
@Binds
または@Provides
を利用する
以上です。
簡単ですね。次回はHiltを利用しViewModelを使って検索結果を画面に表示させたいと思います。
では、また!
参考
- Android デベロッパーガイド - Hiltを使用した依存関係の注入