Dagger Hiltを使ってJavaで書かれたFragmentにViewModelをDIする

2022-09-12

はじめに

皆さん、Dagger Hilt使ってますか?私は存在自体は知っていたのですが、業務で MVVM 化 + Kotlin 化をするときに、Context周りの DI を楽にしたいなと思い、ようやく勉強を始めました。Dagger Hilt を調べるとよくフル Kotlin のサンプルが多いような気がします。ですが、私が実際に業務に携わっているコードは、ほとんど Java なのであまり参考にならないこともあります。例えば、Kotlin だと ktx が使えますが Java では使えなかったり…

そんな中、Java で書かれた Fragment に対してどうやって ViewModel を DI するか調べたので記事として公開しておきます。あと、ApplicationContextを UseCase 層に DI する方法についても書いています。

実装

今回サンプルに使ったコードは、このリポジトリで公開しています。今回使用した Hilt のバージョンは以下の通りです。また既に Hilt のセットアップも終わっていて、Application クラスや Activity や Fragment にもアノテーションを設定しているとします。Hilt のセットアップはこちらを参照。

app/build.gradle
dependencies {
    implementation 'com.google.dagger:hilt-android:2.42'
    kapt 'com.google.dagger:hilt-compiler:2.42'
}

早速ですが、Java の Fragment に ViewModel を DI する方法は以下の通りです。

SampleFragment1.java
@AndroidEntryPoint
public class SampleFragment1 extends Fragment {

    SampleViewModel sampleViewModel;
    // (色々省略)
   @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        sampleViewModel = new ViewModelProvider(this).get(SampleViewModel.class);
    }
    // (色々省略)
}

最初は Hilt を分かってなさすぎて、Fragment で ViewModel に@Injectをつけて DI しようとしていました…何かを見てこうやっていたのですが、何を見たのか…Google 公式の Hilt の説明では、きちんとViewModelProviderを使ったコードもありました。しっかり公式を見ようと心に誓いました。

SampleFragment1.java
@AndroidEntryPoint
public class SampleFragment1 extends Fragment {

    // よくわからないコード
    @Inject SampleViewModel sampleViewModel;
    // (色々省略)
   @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }
    // (色々省略)
}

次に UseCase 層にApplicationContextを DI する方法です。今回は、@Providesではなく、@Bindsを使う方法でやっています(正直まだ自分の中で使い分けが定まっていません…)。

SampleUseCase.kt
interface SampleUseCase {
    fun isGranted(): Boolean
}

class SampleUseCaseImpl @Inject constructor(
    // あらかじめ@ApplicationContextを準備してくれてるのでそれを使う
    @ApplicationContext private val applicationContext: Context
): SampleUseCase {

    @RequiresApi(33)
    override fun isGranted(): Boolean {
        return (ContextCompat.checkSelfPermission(
                applicationContext,
                Manifest.permission.POST_NOTIFICATIONS,
            ) == PackageManager.PERMISSION_GRANTED
        )
    }
}

// 今回はViewModelでだけ使いたかったのでViewModelComponentにします
// けど他の画面でも使うことを考えるとSingtonComponentでも良い気がしています
@Module
@InstallIn(ViewModelComponent::class)
abstract class SampleModule {

    @Binds
    @ViewModelScoped
    abstract fun bindSampleUseCase(
        sampleUseCaseImpl: SampleUseCaseImpl
    ): SampleUseCase
}

ApplicationContextの場合は、既に Hilt 側で@ApplicationContext準備されているのでこれを使います。アプリのプッシュ通知の権限許可の処理を行いたいのでActivityContextではなく、ApplicationContextを使うようにしています。

SampleViewModel.kt
@HiltViewModel
class SampleViewModel @Inject constructor(
    private val useCase: SampleUseCase
    ) : ViewModel() {

    fun isGranted(): Boolean {
        return useCase.isGranted()
    }

}

ViewModel では、インターフェイスで定義したSampleUseCaseを DI します。

最後に

Hilt では、最初の取っ掛かりが、かなり難しいですが慣れると作業になってきます(多分)。これは Rx や Combine と同じだなと思いました。未だに Rx や Combine のオペレータはよく使うのしか覚えてないし、Hot と Cold の記憶も薄らいで来たし…そいうところも含めて同じだなとなりました。ただ慣れると楽だ〜とはなります。Context の DI とかは本当かなり楽だなと実感しました。普通だとthisやらgetContextgetApplicationContextをいい感じに DI しないといけないところをアノテーションで定義するだけでいいのはすごいです(Hilt 初心者なので的外れのこと言ってるかも…)。

そもそもなんで Kotlin ではなく、Java の Fragment に DI したかったのかというと、業務の Fragment が大規模過ぎ(ビジネスロジックとかがかなり入り込んでいるため)て、Fragment を Kotlin 化するのが大変なためです。一旦ビジネスロジックを MVVM 化 + Kotlin 化する方針で進めています。そして、Java の Fragment に DI して少しずつ Fragment の Java コードを剥がしているためです。剥がした方は Coroutines などを使っていて、もちろん ViewModel も Kotlin で書いています。Coroutines は Java では使えないのでlifecycle-livedata-ktxを使ってasLiveDataLiveDataとして公開し、Fragment の Kotlin 化を少しでも楽にしようと色々やっています。そんな背景があってこの記事のような実装を調べていたのでした。

今後は Hilt の理解を深めるためにも、@Provides@Bindsの使い分けなども勉強していかねばという気持ちです。

参考サイト

Tatsumi0000

Written by Tatsumi0000 モバイル開発が好きなエンジニアのブログです. GitHub

Copyright © 2023, Tatsumi0000 All Rights Reserved.