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 のセットアップはこちらを参照。
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
やらgetContext
、getApplicationContext
をいい感じに DI しないといけないところをアノテーションで定義するだけでいいのはすごいです(Hilt 初心者なので的外れのこと言ってるかも…)。
そもそもなんで Kotlin ではなく、Java の Fragment に DI したかったのかというと、業務の Fragment が大規模過ぎ(ビジネスロジックとかがかなり入り込んでいるため)て、Fragment を Kotlin 化するのが大変なためです。一旦ビジネスロジックを MVVM 化 + Kotlin 化する方針で進めています。そして、Java の Fragment に DI して少しずつ Fragment の Java コードを剥がしているためです。剥がした方は Coroutines などを使っていて、もちろん ViewModel も Kotlin で書いています。Coroutines は Java では使えないのでlifecycle-livedata-ktx
を使ってasLiveData
でLiveData
として公開し、Fragment の Kotlin 化を少しでも楽にしようと色々やっています。そんな背景があってこの記事のような実装を調べていたのでした。
今後は Hilt の理解を深めるためにも、@Provides
と@Binds
の使い分けなども勉強していかねばという気持ちです。