発車予定を表示する画面を: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()
}
}