Mokelab Blog

#69 Jetpack ComposeでToDoアプリを作る - HiltとViewModel

今回はHiltとViewModelをアプリに導入してみます。

ViewModel

ViewModelはもともとAndroid Architecture Componentのひとつとして発表されました。

という特徴をもちます。Configuration Changeでもインスタンスが保持されるという特徴は便利なので、今回も使ってみます。

Hilt

Hiltは最近公式でおすすめされている依存性注入(DI)ライブラリです。Daggerが元になっています。 先ほど紹介したViewModelに対し、依存するオブジェクトを注入するのに使うととても便利なのでこちらもセットで使ってみます。

ライブラリの追加

まずはViewModelを追加します。

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0"
}

次にHiltです。ルートのbuild.gradleにclasspathを追加します。

buildscript {
  ...
  dependencies {
    ...
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.36'
  }
}

app/build.gradleでは、まずプラグインを追加します。(追記)kaptも追加します。。。

plugins {
  ...
  id 'dagger.hilt.android.plugin'
  id 'kotlin-kapt'
}

そしてdependenciesでHiltを追加します。Navigation Composeも使っているのでhilt-navigation-composeも追加します。

dependencies {
  ...
  implementation "com.google.dagger:hilt-android:2.36"
  // kspだと執筆時点では動作しませんでした。。。
  kapt "com.google.dagger:hilt-android-compiler:2.36"
  implementation 'androidx.hilt:hilt-navigation-compose:1.0.0-alpha03'
}

Hiltの準備

HiltにDIしてもらうための準備をいくつかやります。Viewシステムのときとやることは同じです。

まず、Applicationクラスを作ります。

package com.mokelab.compose.todo

@HiltAndroidApp
class ToDoApplication : Application()

そしてAndroidManifest.xmlにも登録しておきます。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.mokelab.compose.todo">

  <application
    ...
    android:name=".ToDoApplication">
    <activity>
      <intent-filter></intent-filter>
    </activity>
  </application>
</manifest>

次にMainActivityに@AndroidEntryPointアノテーションをつけます。

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
  ...
}

最後にモジュールを作ります。Room部分のオブジェクトの作り方だけ教えておきましょう。

@Module
@InstallIn(SingletonComponent::class)
object MainModule {
  @Provides
  @Singleton
  fun provideToDoDatabase(@ApplicationContext context: Context): ToDoDatabase {
    return Room.databaseBuilder(
        context,
        ToDoDatabase::class.java,
        "todo.db"
    ).build()
  }

  @Provides
  @Singleton
  fun provideToDoDAO(db: ToDoDatabase): ToDoDAO {
    return db.todoDAO()
  }
}

各画面用のViewModelを作る

Hiltの準備ができたら各画面用のViewModelを作ります。中身は必要になったときに実装していきましょう。

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel()
@HiltViewModel
class CreateToDoViewModel @Inject constructor() : ViewModel()
@HiltViewModel
class ToDoDetailViewModel @Inject constructor() : ViewModel()
@HiltViewModel
class EditToDoViewModel @Inject constructor() : ViewModel()

クラスに@HiltViewModelを、コンストラクタに@Injectをつけます。

パラメータにViewModelを追加する

各画面のComposable関数の引数にViewModelを追加します。

@Composable
fun MainScreen(
  navController: NavController,
  viewModel: MainViewModel,
) { ... }
@Composable
fun CreateToDoScreen(
  navController: NavController,
  viewModel: CreateToDoViewModel,
) { ... }
@Composable
fun ToDoDetailScreen(
  navController: NavController,
  viewModel: ToDoDetailViewModel,
  todoId: Int
) { ... }
@Composable
fun EditToDoScreen(
  navController: NavController,
  viewModel: EditToDoViewModel,
  todoId: Int,
) { ... }

ViewModelインスタンスを取得する

hiltViewModel()を使って、NavHostの箇所でViewModelインスタンスを取得し、各画面のComposableに渡します。

NavHost(navController = navController, startDestination = "main") {
  composable("main") {
    val viewModel = hiltViewModel<MainViewModel>()
    MainScreen(navController = navController, viewModel = viewModel)
  }
  composable("create") {
    val viewModel = hiltViewModel<CreateToDoViewModel>()
    CreateToDoScreen(navController = navController, viewModel = viewModel)
  }
  composable(
    "detail/{todoId}",
    arguments = listOf(navArgument("todoId") { type = NavType.IntType })
  ) { backStackEntry ->
    val viewModel = hiltViewModel<ToDoDetailViewModel>()
    val todoId = backStackEntry.arguments?.getInt("todoId") ?: 0
    ToDoDetailScreen(
      navController = navController,
      viewModel = viewModel,
      todoId = todoId
    )
  }
  composable(
    "edit/{todoId}",
    arguments = listOf(navArgument("todoId") { type = NavType.IntType })
  ) { backStackEntry ->
    val viewModel = hiltViewModel<EditToDoViewModel>()
    val todoId = backStackEntry.arguments?.getInt("todoId") ?: 0
    EditToDoScreen(
      navController = navController,
      viewModel = viewModel,
      todoId = todoId,
    )
  }
}

これで、HiltにDIしてもらったViewModelを各画面に渡せるようになりました。

ここまで作業したものはこちらにあります。

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。