DIしてますか?
私はしていませんでした!(というよりよく理解せず使ってました)
今回はHiltという依存関係(DI)ライブラリの使い方とか仕組みみたいなところを学んでいこうと思います。
DIってなんだっけ
プログラムを書こうとすると、どうしても依存関係は避けられないですよね。
class Random {
fun randomArray():MutableList<Double> {
val randomList = MutableList<Double>
for(in in 1..5) {
val a = Math.random()
randomList.add(A)
}
return randomList
}
}
//以下のrandomMath関数はRandomクラスに依存している
fun randomMath() {
val createRandom = Random()
val randomArray = createRandom.randomArray()
println(randomArray) // 例: ランダムな配列をコンソールに出力
}
上記ぐらいであれば、1対1の依存関係なので、たいして複雑じゃない気がしますが、
こんなパターンはどうでしょうか??
// 依存クラス
class ApiDataFetcher {
fun fetchData(): String {
return "APIから取得したデータ"
}
}
class DatabaseDataFetcher {
fun fetchData(): String {
return "データベースから取得したデータ"
}
}
class DataProcessor {
private val dataFetcher = ApiDataFetcher() // 固定された依存
fun processData(): String {
val data = dataFetcher.fetchData()
return "加工後のデータ: $data"
}
}
class DataRenderer {
private val dataProcessor = DataProcessor() // 固定された依存
fun renderData(): String {
val processedData = dataProcessor.processData()
return "表示用データ: $processedData"
}
}
class ReportGenerator {
private val dataRenderer = DataRenderer() // 固定された依存
fun generateReport(): String {
val renderedData = dataRenderer.renderData()
return "レポート: $renderedData"
}
}
fun main() {
val reportGenerator = ReportGenerator()
val report = reportGenerator.generateReport()
println(report)
}
では、これの何が問題なんでしょうか??
依存性が複雑な問題点を考えるために、以下の部分をApiからではなく、Databaseから取得したデータを使うことにしましょうか
class DataProcessor {
private val dataFetcher = ApiDataFetcher() // 固定された依存
class DataProcessor {
private val dataFetcher = DatabaseDataFetcher() // 固定された依存
DIを使わない場合、コードは以下のように書き変わります。
つまり、DataProcessor の内部コードが変わり、
DatabaseDataFetcher が使用されるようになります。
class DataProcessor {
private val dataFetcher = DatabaseDataFetcher() // 依存クラスを変更
fun processData(): String {
val data = dataFetcher.fetchData()
return "加工後のデータ: $data"
}
}
DIを使う場合、コード全体は以下のように書き変わります。
// 依存クラス (インターフェース)
interface DataFetcher {
fun fetchData(): String
}
class ApiDataFetcher : DataFetcher {
override fun fetchData(): String {
return "APIから取得したデータ"
}
}
class DatabaseDataFetcher : DataFetcher {
override fun fetchData(): String {
return "データベースから取得したデータ"
}
}
class DataProcessor(private val dataFetcher: DataFetcher) { // 依存を注入
fun processData(): String {
val data = dataFetcher.fetchData()
return "加工後のデータ: $data"
}
}
class DataRenderer(private val dataProcessor: DataProcessor) { // 依存を注入
fun renderData(): String {
val processedData = dataProcessor.processData()
return "表示用データ: $processedData"
}
}
class ReportGenerator(private val dataRenderer: DataRenderer) { // 依存を注入
fun generateReport(): String {
val renderedData = dataRenderer.renderData()
return "レポート: $renderedData"
}
}
fun main() {
val apiFetcher = ApiDataFetcher()
val databaseFetcher = DatabaseDataFetcher()
val dataProcessor = DataProcessor(apiFetcher) // 依存を注入
val dataRenderer = DataRenderer(dataProcessor) // 依存を注入
val reportGenerator = ReportGenerator(dataRenderer) // 依存を注入
val report = reportGenerator.generateReport()
println(report)
}
先ほどと同じように、Apiからではなく、Databaseから取得したデータを使うことにする場合、以下のDataProcessorに渡す部分だけ変更すれば良いので、DataProcessorの内部コード自体に変更は発生しません。
val dataProcessor = DataProcessor(databaseFetcher) // 依存を注入
つまりは、依存元の変更影響を局所化できる。というのがDIのいい点と考えることができますね。でも、このままだと、以下のようにコントラクタとして手動で依存性を注入してあげないといけません。
val dataProcessor = DataProcessor(apiFetcher) // 依存を注入
val dataRenderer = DataRenderer(dataProcessor) // 依存を注入
val reportGenerator = ReportGenerator(dataRenderer) // 依存を注入
必要な時に自動で依存性を解決してくれるのがDaggerやhiltといったライブラリです
Hiltの設定手順からHiltを学ぶ
プロジェクトの初期設定
Hiltの初期設定は公式サイトに記載されているので、以下を参照してください。
Android Developers Hilt を使用した依存関係挿入
Hiltの仕様
以下の1~4を実施することで、Hiltに各クラス間の依存関係やその依存先のクラスの生成方法を伝えることができ、Hilt側で自動的に依存性の解決ができる。1~4を実施した結果、Hiltはこの図にの感じで依存性を解決していく。
**0.Hiltを用いたコード全量
まず、Hiltを用いた場合のコードを示しておきます
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
// 依存クラス (インターフェース)
interface DataFetcher {
fun fetchData(): String
}
class ApiDataFetcher @Inject constructor() : DataFetcher {
override fun fetchData(): String {
return "APIから取得したデータ"
}
}
class DatabaseDataFetcher @Inject constructor() : DataFetcher {
override fun fetchData(): String {
return "データベースから取得したデータ"
}
}
class DataProcessor @Inject constructor(private val dataFetcher: DataFetcher) { // 依存を注入
fun processData(): String {
val data = dataFetcher.fetchData()
return "加工後のデータ: $data"
}
}
class DataRenderer @Inject constructor(private val dataProcessor: DataProcessor) { // 依存を注入
fun renderData(): String {
val processedData = dataProcessor.processData()
return "表示用データ: $processedData"
}
}
class ReportGenerator @Inject constructor(private val dataRenderer: DataRenderer) { // 依存を注入
fun generateReport(): String {
val renderedData = dataRenderer.renderData()
return "レポート: $renderedData"
}
}
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideApiDataFetcher(): DataFetcher {
return ApiDataFetcher()
}
@Provides
fun provideDatabaseDataFetcher(): DataFetcher {
return DatabaseDataFetcher()
}
}
fun main() {
// Hilt の初期化 (Android アプリケーションの場合、@HiltAndroidApp を使用)
// Hilt.getComponent(SingletonComponent::class.java).inject(this)
// Hilt から ReportGenerator のインスタンスを取得
val reportGenerator = ReportGenerator(DataRenderer(DataProcessor(AppModule.provideApiDataFetcher())))
val report = reportGenerator.generateReport()
println(report)
}
1.アプリケーションクラスに@HiltAndroidAppアノテーションを付与
@HiltAndroidApp
class ExampleApplication : Application() { ... }
- アプリケーションクラスとは、アプリ起動時に作られるインスタンス
- このクラスはアプリケーションのライフサイクル全体で維持・共有される
- @HiltAndroidAppを付与することで、Hiltは管理すべきアプリケーションクラスを知る
2.特定のクラスに@AndroidEntryPointを付与する
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
- 各ライフサイクルに応じて対応するHiltコンテナを生成
上記の例で言うと、Activityのライフサイクルに沿ったコンポーネントを作成- Activity
- Fragment
- View など
3.Hilt バインディングを定義
class ApiDataFetcher @Inject constructor() : DataFetcher {
override fun fetchData(): String {
return "APIから取得したデータ"
}
}
- Hiltコンポーネントに必要な依存関係を伝える。上記の場合は、以下をHiltに伝えている
- ApiDataFetcherクラス自体は何も依存していない(注入が不要)
- ApiDataFetcherクラスのインスタンスが必要な場合、Hiltはこのクラスのコンストラクタを呼び出してインスタンスを生成
4.Hilt モジュール
interface DataFetcher {
fun fetchData(): String
}
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideApiDataFetcher(): DataFetcher {
return ApiDataFetcher()
}
@Provides
fun provideDatabaseDataFetcher(): DataFetcher {
return DatabaseDataFetcher()
}
}
- DataFetcherはインターフェースでありインスタンス化ができないので、Hiltモジュールを使用して、バインディング情報を提供
- @Providesを用いてApiDataFetcher(),DatabaseDataFetcher()のインスタンス生成方法をHiltに提供
終わりに
DIって難しいですね。私も理解度50%?もいってない気がします。
ともかくHiltを使うと、手動でDIするより簡素なコードで描けるようになるんだな〜というところと、全体の流れ(Hiltコンテナが、依存関係のグラフを作って、それを元に注入していく)みたいなところを最低限理解できたので、まあ今回はここまでにしておきまs