発車予定を表示する画面を:feature:depart:homeモジュールに作成します。
uiモジュールの作成と同様に、トップディレクトリで:feature:depart:homeを作成し、javaディレクトリ名の変更を行います。
各feature画面は画面の表示データがありますのでViewModelを使用します。
Hiltと連携してViewModelを使用するためhilt-navigation-composeを導入します。
※navigation-composeは必要ありません。
バージョンカタログファイルにhilt-navigation-composeの定義を追記します。
[versions]
:
hilt-navigation-compose = "1.3.0" # https://mvnrepository.com/artifact/androidx.hilt/hilt-navigation-compose
:
lifecycleRuntimeKtx = "2.9.4"
lifecycle = "2.9.4"
:
[libraries]
:
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
:
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
:
追記後、『Sync Now』で内容をプロジェクトに反映させます。
:core:uiモジュールで作成したLibraryConfigurePlugin.ktを使用してもいいのですが、画面が複数ありますので、画面用のビルドプラグインを作成することにします。
/build-logic/src/main/kotlin内に画面ライブラリモジュールのbuild.gradle.ktsで使用するビルドプラグインを作成します。
ファイル名をFeatureConfigurePlugin.ktにしています。
package jp.co.progress_llc.buildlogic import com.android.build.api.dsl.LibraryExtension import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies class FeatureConfigurePlugin: Plugin<Project> { override fun apply(project: Project) = with(project) { with(pluginManager) { apply("com.android.library") apply("org.jetbrains.kotlin.android") apply("com.google.devtools.ksp") apply("com.google.dagger.hilt.android") apply("org.jetbrains.kotlin.plugin.compose") } extensions.configure<LibraryExtension> { configureCommonExtension(this) defaultConfig.consumerProguardFiles("consumer-rules.pro") buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } buildFeatures.compose = true } dependencies { add("implementation", project.libs().findLibrary("androidx-core-ktx").get()) add("implementation", project.libs().findLibrary("androidx-appcompat").get()) add("implementation", project.libs().findLibrary("material").get()) add("implementation", project.libs().findLibrary("hilt-android").get()) add("implementation", project.libs().findLibrary("hilt-navigation-compose").get()) add("implementation", project.libs().findLibrary("androidx-lifecycle-runtime-ktx").get()) add("implementation", project.libs().findLibrary("androidx-lifecycle-viewmodel-compose").get()) add("implementation", platform(project.libs().findLibrary("compose-bom").get())) add("implementation", project.libs().findLibrary("compose-ui").get()) add("implementation", project.libs().findLibrary("compose-material").get()) add("implementation", project.libs().findLibrary("androidx-ui-tooling-preview").get()) add("ksp", project.libs().findLibrary("hilt-compiler").get()) add("debugImplementation", platform(project.libs().findLibrary("compose-bom").get())) add("debugImplementation", project.libs().findLibrary("androidx-ui-tooling").get()) } } }
作成したビルドプラグインをモジュールのbuild.gradle.ktsで使用できるようにbuild-logic直下のbuild.gradle.ktsに追記します。
: gradlePlugin { : plugins { register("FeatureConfigurePlugin") { id = "build.logic.feature.configure" implementationClass = "jp.co.progress_llc.buildlogic.FeatureConfigurePlugin" } } } dependencies { :
追記後、『Sync Now』で内容をプロジェクトに反映させます。
:feature:depart:homeモジュールのbuild.gradle.ktsに作成したビルドプラグインを適用します。
plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) id("build.logic.feature.configure") } android { namespace = "jp.co.progress_llc.portal.feature.depart.home" compileSdk { version = release(36) } defaultConfig { minSdk = 28 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) }
適用後、『Sync Now』で内容をプロジェクトに反映させます。
UI層としてpresentationディレクトリを作成します。
発車予定画面として、暫定的にViewModelのないDepartHomeScreen.ktを作成します。
package jp.co.progress_llc.portal.feature.depart.home.presentation import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun DepartHomeScreen( ) { var expanded by remember { mutableStateOf(false) } Column( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( text = "発車予定", style = MaterialTheme.typography.headlineMedium ) ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = !expanded } ) { OutlinedTextField( value = "", onValueChange = {}, readOnly = true, label = { Text("経路を選択") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, modifier = Modifier .fillMaxWidth() ) ExposedDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { // 新規経路オプション DropdownMenuItem( text = { Text( text = "編集/新規経路...", color = MaterialTheme.colorScheme.primary ) }, onClick = { expanded = false } ) } } } } @Preview(showBackground = true) @Composable fun DepartHomeScreenPreview() { DepartHomeScreen() }
ViwModelのインターフェースDepartHomeUiLogic.ktを作成します。
package jp.co.progress_llc.portal.feature.depart.presentation import jp.co.progress_llc.portal.core.data.Route import kotlinx.coroutines.flow.StateFlow interface DepartUiLogic { val routes: StateFlow<List<Route>> }
本番用のViewModelとしてDepartViewModel.ktを作成します。
package jp.co.progress_llc.portal.feature.depart.presentation import javax.inject.Inject import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import jp.co.progress_llc.portal.feature.depart.repository.DepartRepositoryImpl import jp.co.progress_llc.portal.core.data.Route @HiltViewModel class DepartViewModel @Inject constructor( private val departRepositoryImpl: DepartRepositoryImpl ) : ViewModel(), DepartUiLogic { private val _routes = MutableStateFlow<List<Route>>(emptyList()) override val routes: StateFlow<List<Route>> = _routes.asStateFlow() init { loadRoutes() } private fun loadRoutes() { viewModelScope.launch { try { val routeList = departRepositoryImpl.getAllRoute() _routes.value = routeList } catch (e: Exception) { _routes.value = emptyList() } } } }
プレビュー用のViewModelが本番パッケージに含まれないようするため、debugソースセットを新規に作成します。
debugソースセット内に、プレビュー用のViewModelとしてFakeDepartViewModel.ktを作成します。
package jp.co.progress_llc.portal.feature.depart.presentation import jp.co.progress_llc.portal.core.data.Route import kotlinx.coroutines.flow.MutableStateFlow class FakeDepartViewModel : DepartUiLogic { override val routes = MutableStateFlow<List<Route>>(emptyList()) }
発車予定画面として、DepartHomeScreen.ktを作成します。
package jp.co.progress_llc.portal.feature.depart.presentation import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import jp.co.progress_llc.portal.core.data.Route @OptIn(ExperimentalMaterial3Api::class) @Composable fun DepartScreen( ui: DepartUiLogic ) { val routes by ui.routes.collectAsState() var expanded by remember { mutableStateOf(false) } var selectedRoute by remember { mutableStateOf<Route?>(null) } Column( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( text = "発車予定", style = MaterialTheme.typography.headlineMedium ) ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = !expanded } ) { OutlinedTextField( value = "", onValueChange = {}, readOnly = true, label = { Text("経路を選択") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, modifier = Modifier.fillMaxWidth() ) ExposedDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { DropdownMenuItem( text = { Text( text = "編集/新規経路...", color = MaterialTheme.colorScheme.primary ) }, onClick = { } ) } } } } @Preview(showBackground = true) @Composable fun DepartScreenPreview() { DepartScreen(ui = FakeDepartViewModel()) }
テーブルのDAO内には、テーブルを更新するインターフェースが含まれています。
:feature:departモジュールでは、テーブルを更新しませんので、インターフェースを隠蔽した方が安全です。
リポジトリ層を作成して実現します。
repositoryディレクトリ(新規に作成します)内にDepartRepositoryImpl.ktを作成します。
:feature:departモジュール内だけで使用し、ロジックらしきものがないのでインターフェース定義は作成しません。
package jp.co.progress_llc.portal.feature.depart.repository import javax.inject.Singleton import javax.inject.Inject import jp.co.progress_llc.portal.core.data.Route import jp.co.progress_llc.portal.core.data.repository.RouteRepository /** * 発車予定画面専用のRouteリポジトリ実装 * 発車予定画面固有のビジネスロジックを提供 */ @Singleton class DepartRepositoryImpl @Inject constructor( private val routeRepository: RouteRepository, ) { /** * すべてのRouteを取得 */ suspend fun getAllRoute(): List<Route> { return routeRepository.getAllRoute() } }