4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AndroidのHiltをJavaで実装してみる

Last updated at Posted at 2020-09-19

はじめに

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 {
}

AndroidManifest.xml
<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を注入します。

MainActivity.java

@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です。
まずは、ログを出力するだけの実装です。

SampleUseCase.java

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クラスに実装を追加します。

SampleUseCase.java
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の実装です。インターフェースと実体クラスは以下のようになります。

SampleRepository.java
public interface SampleRepository {
    List<SampleEntity> find();
}
SampleRepositoryImpl.java
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アノテーションを付けて依存関係を指定します。

DataModule.java
@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に追加します。

DataModule.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を注入して実装を完成させます。

SampleRepositoryImpl.java
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を載せておきます。

SampleEntity.java
@Entity(tableName = "sample")
public class SampleEntity implements Serializable {

    @PrimaryKey
    @NonNull
    private String code;
    private String name;

    //setter/getter省略
}
SampleDao.java
@Dao
public interface SampleDao {

    @Insert
    long save(SampleEntity dto);

    @Query("select * from sample")
    List<SampleEntity> find();
}

完成!!

ここまでで、画面のボタンを押すと、ログに検索結果が表示されます。
Dagger2で必要だったものがほとんど不要となり、シンプルに実現できるようになりました。非常に簡単ですね。

まとめ

今回のサンプルを通じてHiltのポイントを整理します。

  1. Activityに@AndroidEntryPointを付与する
  2. Inject対象のクラスのコンストラクタに@Injectを忘れずにつける
  3. インターフェースや他のライブラリをInject対象とするときは、@Bindsまたは@Providesを利用する
    以上です。
    簡単ですね。次回はHiltを利用しViewModelを使って検索結果を画面に表示させたいと思います。
    では、また!

参考

4
3
0

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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?