はじめに
私は業務でServer-side Kotlinの開発に携わっているのですが、Androidアプリ開発に関してはかなり初心者です。
ふと、Jetpack Composeでアプリを作ってみようと思い立ち、その過程で勉強したDIライブラリのHiltの使い方についてまとめました。
開発環境
- PC: M1 Mac mini / macOS 12.5
- Android Studio: Dolphin|2021.3.1
サンプルアプリの概要
機能要件
- Joe SchmoeのAPIをコールして、アバターを生成
- アバターをローカルDBに保存
- 保存されているデータの一覧を表示
Joe Schmoe APIを使用したのは無料で利用できるAPIを探していてたまたま見つけたからです。
勉強のためとは言え、無料で利用させていただき、感謝いたします。
ライブラリ
- DI: Hilt
- その他: 画像表示にCoil、DatabaseにRoomを使用しています。
build.gradleの設定についてはAndoridの公式ページを参照してください。
設計
基本的にはクリーンアーキテクチャを採用しています。
Android開発ではMVVMとRepositoryパターンがよく用いられると思いますが、複数のレイヤーに対してDIするやり方を学ぶために敢えてViewModelからUseCaseを呼び出すようにしています。
ディレクトリ構成
app
├── di
│ └── modules
├── domains
│ ├── entities
│ └── repositories
├── infrastructures
│ ├── dao
│ └── repositories
├── presentations
│ ├── navigations
│ ├── screens
│ ├── theme
│ ├── viewmodels
│ └── MainActivity.kt
├── usecases
│ ├── inputports
│ └── interactors
└── MainApplication.kt
拡張子が付いてるもの以外はディレクトリを指してます。
Hiltの使い方
準備
MainApplicationクラスの作成
Jetpack Composeプロジェクトを開始しただけではまだ作成されていないので、MainApplicationクラスを追加します。
特に詳細の実装は不要です。
@HiltAndroidApp
class MainApplication: Application()
MainActivityの修正
既存のMainActivityに@AndroidEntryPoint
を付けるだけです。
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
AndroidManifest.xmlの修正
<application>
タグに作成したMainApplicationのパスを設定します。
<application
~
~
android:name=".MainApplication">
<activity
android:name=".presentations.MainActivity"
~
~
</application>
実装
ViewにViewModelをインジェクト
Jetpack ComposeではViewModelを使用することは推奨されていない1ようですが、HiltはViewModelをライフサイクルに合わせて制御してくれるので安全に使用できると思っています(詳細は公式ページを参照)。
ここでは、ViewModelにUseCaseをインジェクトしています。
ViewModelは@HiltViewModel
アノテーションを付けると特に何も記述しないで自動でバインディングしてくれます。
@HiltViewModel
class AvatarCreationScreenViewModel @Inject constructor(
private val saveAvatarUseCase: SaveAvatarUseCase,
): ViewModel() {
// View Model Logic
}
一方で、ViewではViewModelをhiltViewModel()
から取得します。
@Composable
fun AvatarCreationScreen(
viewModel: AvatarCreationScreenViewModel = hiltViewModel(),
navigateToList: () -> Unit
) {
// View
}
ViewModelは裏でインジェクトされるため、NavigationGraphではViewModelを敢えて渡さなくて良いのが楽です。
@Composable
fun NavigationGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "avatar_creation"
) {
composable("avatar_creation") {
AvatarCreationScreen(navigateToList = {
navController.navigate("avatar_list")
})
}
composable("avatar_list") {
AvatarListScreen(backToCreation = {
navController.popBackStack()
})
}
}
}
UseCaseとReppositoryをインジェクト
ViewModel以外をインジェクトする場合は、di/module
パスにModuleクラスを定義する必要があります(公式ページ)。
ネットを探すと書き方に揺らぎがありますが、公式ではprovideXXXX()
に統一されてるのでprovideXXXX()
を使えば良いです。
ざっと要点をまとめると
-
di/module
パスに定義すると自動で読み込まれる -
@InstallIn
でコンポーネントのスコープを設定する- ここでは、UseCaseやRepositoryはViewのライフサイクルには依存してほしくないので、
SingletonComponent
にしています
- ここでは、UseCaseやRepositoryはViewのライフサイクルには依存してほしくないので、
-
@Provides
でバインドする対象を定義する - コンテキストを渡す場合は
@ApplicationContext
を利用するとApplicationのコンテキストがインジェクトされます- Roomを使うために必要だったからです
-
@ActivityContext
など、コンポーネントのスコープに合わせる必要があります
@Module
@InstallIn(SingletonComponent::class)
object ApplicationModule {
@Provides
fun provideAvatarRepository(
@ApplicationContext context: Context
): AvatarRepository = AvatarRepositoryImpl(
context = context
)
@Provides
fun provideListAvatarUseCase(
avatarRepository: AvatarRepository
): ListAvatarUseCase = ListAvatarUseCaseImpl(
repository = avatarRepository
)
@Provides
fun provideSaveAvatarUseCase(
avatarRepository: AvatarRepository
): SaveAvatarUseCase = SaveAvatarUseCaseImpl(
repository = avatarRepository
)
}
最後に
ソースコードはこちらに置いてます。雑な設計なので参考程度に。