skip to content
私的歌詞倉庫

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

/ 7 min read

Updated:
Table of Contents

はじめに

皆さん、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 のセットアップはこちらを参照。

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

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

@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を使ったコードもありました。しっかり公式を見ようと心に誓いました。

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

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

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を使うようにしています。

@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の使い分けなども勉強していかねばという気持ちです。

参考サイト