diff --git a/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt b/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt index 503cd9cbf..4c406a8e2 100644 --- a/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt +++ b/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt @@ -9,16 +9,28 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import io.github.droidkaigi.confsched2023.contributors.ContributorsScreen import io.github.droidkaigi.confsched2023.contributors.ContributorsViewModel +import io.github.droidkaigi.confsched2023.contributors.contributorsScreenRoute import io.github.droidkaigi.confsched2023.designsystem.theme.KaigiTheme -import io.github.droidkaigi.confsched2023.main.MainScreen -import io.github.droidkaigi.confsched2023.sessions.TimetableItemDetailScreen -import io.github.droidkaigi.confsched2023.sessions.TimetableScreen +import io.github.droidkaigi.confsched2023.main.MainNestedGraphStateHolder +import io.github.droidkaigi.confsched2023.main.MainScreenTab +import io.github.droidkaigi.confsched2023.main.MainScreenTab.Contributor +import io.github.droidkaigi.confsched2023.main.MainScreenTab.Timetable +import io.github.droidkaigi.confsched2023.main.mainScreen +import io.github.droidkaigi.confsched2023.main.mainScreenRoute +import io.github.droidkaigi.confsched2023.sessions.navigateTimetableScreen +import io.github.droidkaigi.confsched2023.sessions.navigateToTimetableItemDetailScreen +import io.github.droidkaigi.confsched2023.sessions.nestedSessionScreens +import io.github.droidkaigi.confsched2023.sessions.sessionScreens +import io.github.droidkaigi.confsched2023.sessions.timetableScreenRoute @Composable fun KaigiApp(modifier: Modifier = Modifier) { @@ -35,33 +47,61 @@ fun KaigiApp(modifier: Modifier = Modifier) { modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = "main") { - composable("main") { - MainScreen( - timetableScreen = { - TimetableScreen( - onTimetableItemClick = { - navController.navigate("timetable/${it.id.value}") - }, - onContributorsClick = { - navController.navigate("contributors") - }, - ) - }, - ) - } - composable("contributors") { - ContributorsScreen(hiltViewModel()) - } - composable("timetable/{timetableItemId}") { - TimetableItemDetailScreen( - onNavigationIconClick = { - navController.popBackStack() - }, + KaigiNavHost() + } + } +} + +@Composable +private fun KaigiNavHost( + navController: NavHostController = rememberNavController(), +) { + NavHost(navController = navController, startDestination = mainScreenRoute) { + mainScreen(navController) + sessionScreens( + onNavigationIconClick = { + navController.popBackStack() + }, + ) + } +} + +private fun NavGraphBuilder.mainScreen(navController: NavHostController) { + mainScreen( + mainNestedGraphStateHolder = KaigiAppMainNestedGraphStateHolder(), + mainNestedGraph = { mainNestedNavController, padding -> + nestedSessionScreens( + onTimetableItemClick = { timetableitem -> + navController.navigateToTimetableItemDetailScreen( + timetableitem.id, ) - } + }, + ) + composable(contributorsScreenRoute) { + ContributorsScreen(hiltViewModel()) } + }, + ) +} + +class KaigiAppMainNestedGraphStateHolder : MainNestedGraphStateHolder { + override val startDestination: String = timetableScreenRoute + + override fun routeToTab(route: String): MainScreenTab? { + return when (route) { + timetableScreenRoute -> Timetable + contributorsScreenRoute -> Contributor + else -> null + } + } + + override fun onTabSelected( + mainNestedNavController: NavController, + tab: MainScreenTab, + ) { + when (tab) { + Timetable -> mainNestedNavController.navigateTimetableScreen() + Contributor -> mainNestedNavController.navigate(contributorsScreenRoute) } } } diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index 54f900361..39da6646c 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(project(":core:designsystem")) implementation(project(":core:data")) implementation(project(":app-android")) + implementation(project(":feature:main")) implementation(project(":feature:sessions")) implementation(libs.daggerHiltAndroidTesting) diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/KaigiAppRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/KaigiAppRobot.kt index 301fb0dbd..b0ea7c178 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/KaigiAppRobot.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/KaigiAppRobot.kt @@ -2,9 +2,10 @@ package io.github.droidkaigi.confsched2023.testing.robot import androidx.compose.ui.test.isRoot import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import com.github.takahirom.roborazzi.captureRoboImage +import io.github.droidkaigi.confsched2023.main.MainScreenTab import io.github.droidkaigi.confsched2023.testing.RobotTestRule import kotlinx.coroutines.test.TestDispatcher import javax.inject.Inject @@ -33,7 +34,7 @@ class KaigiAppRobot @Inject constructor( fun goToContributor() { composeTestRule - .onNodeWithText("Go to ContributorsScreen") + .onNodeWithContentDescription(MainScreenTab.Contributor.contentDescription) .performClick() waitUntilIdle() } diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/TimetableScreenRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/TimetableScreenRobot.kt index b1d824d28..8cf0b800a 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/TimetableScreenRobot.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/robot/TimetableScreenRobot.kt @@ -36,7 +36,6 @@ class TimetableScreenRobot @Inject constructor( composeTestRule.setContent { KaigiTheme { TimetableScreen( - onContributorsClick = { }, onTimetableItemClick = { }, ) } diff --git a/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched2023/contributors/ContributorsScreen.kt b/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched2023/contributors/ContributorsScreen.kt index 0d8b8de92..ce1c73b25 100644 --- a/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched2023/contributors/ContributorsScreen.kt +++ b/feature/contributors/src/commonMain/kotlin/io/github/droidkaigi/confsched2023/contributors/ContributorsScreen.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +const val contributorsScreenRoute = "contributors" + @Composable fun ContributorsScreen(viewModel: ContributorsViewModel) { val contributors = Contributors() diff --git a/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/MainScreen.kt b/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/MainScreen.kt index 025e95494..9392f4e47 100644 --- a/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/MainScreen.kt +++ b/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/MainScreen.kt @@ -1,24 +1,23 @@ package io.github.droidkaigi.confsched2023.main -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DateRange -import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.List import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState @@ -26,12 +25,32 @@ import androidx.navigation.compose.rememberNavController import io.github.droidkaigi.confsched2023.main.strings.MainStrings import io.github.droidkaigi.confsched2023.ui.SnackbarMessageEffect +const val mainScreenRoute = "main" const val MainScreenTestTag = "MainScreen" +fun NavGraphBuilder.mainScreen( + mainNestedGraphStateHolder: MainNestedGraphStateHolder, + mainNestedGraph: NavGraphBuilder.(mainNestedNavController: NavController, PaddingValues) -> Unit, +) { + composable(mainScreenRoute) { + MainScreen( + mainNestedGraphStateHolder = mainNestedGraphStateHolder, + mainNestedNavGraph = mainNestedGraph, + ) + } +} + +interface MainNestedGraphStateHolder { + val startDestination: String + fun routeToTab(route: String): MainScreenTab? + fun onTabSelected(mainNestedNavController: NavController, tab: MainScreenTab) +} + @Composable fun MainScreen( - timetableScreen: @Composable () -> Unit, + mainNestedGraphStateHolder: MainNestedGraphStateHolder, viewModel: MainScreenViewModel = hiltViewModel(), + mainNestedNavGraph: NavGraphBuilder.(NavController, PaddingValues) -> Unit, ) { val uiState by viewModel.uiState.collectAsState() val snackbarHostState = SnackbarHostState() @@ -43,24 +62,23 @@ fun MainScreen( MainScreen( uiState = uiState, snackbarHostState = snackbarHostState, - timetableScreen = timetableScreen, + routeToTab = mainNestedGraphStateHolder::routeToTab, + onTabSelected = mainNestedGraphStateHolder::onTabSelected, + mainNestedNavGraph = mainNestedNavGraph, ) } enum class MainScreenTab( val icon: ImageVector, val contentDescription: String, - val route: String, ) { Timetable( icon = Icons.Filled.DateRange, contentDescription = MainStrings.Timetable.asString(), - route = "timetable", ), - Play( - icon = Icons.Filled.PlayArrow, - contentDescription = MainStrings.Play.asString(), - route = "play", + Contributor( + icon = Icons.Filled.List, + contentDescription = MainStrings.Contributors.asString(), ), } @@ -70,20 +88,22 @@ class MainScreenUiState() private fun MainScreen( uiState: MainScreenUiState, snackbarHostState: SnackbarHostState, - timetableScreen: @Composable () -> Unit, + routeToTab: String.() -> MainScreenTab?, + onTabSelected: (NavController, MainScreenTab) -> Unit, + mainNestedNavGraph: NavGraphBuilder.(NavController, PaddingValues) -> Unit, ) { - val bottomBarNavController = rememberNavController() - val navBackStackEntry by bottomBarNavController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route + val mainNestedNavController = rememberNavController() + val navBackStackEntry by mainNestedNavController.currentBackStackEntryAsState() + val currentTab = navBackStackEntry?.destination?.route?.routeToTab() Scaffold( bottomBar = { NavigationBar { MainScreenTab.values().forEach { tab -> - val selected = currentRoute == tab.route + val selected = currentTab == tab NavigationBarItem( selected = selected, onClick = { - bottomBarNavController.navigate(tab.route) + onTabSelected(mainNestedNavController, tab) }, icon = { Icon( @@ -97,15 +117,11 @@ private fun MainScreen( }, contentWindowInsets = WindowInsets(0.dp), ) { padding -> - NavHost(navController = bottomBarNavController, startDestination = "timetable") { - composable(MainScreenTab.Timetable.route) { - Box(Modifier.padding(padding)) { - timetableScreen() - } - } - composable(MainScreenTab.Play.route) { - Text("play") - } + NavHost( + navController = mainNestedNavController, + startDestination = "timetable", + ) { + mainNestedNavGraph(mainNestedNavController, padding) } } } diff --git a/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/strings/MainStrings.kt b/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/strings/MainStrings.kt index a0aec79a8..f9e72c9e7 100644 --- a/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/strings/MainStrings.kt +++ b/feature/main/src/main/java/io/github/droidkaigi/confsched2023/main/strings/MainStrings.kt @@ -6,21 +6,21 @@ import io.github.droidkaigi.confsched2023.designsystem.strings.StringsBindings sealed class MainStrings : Strings(Bindings) { object Timetable : MainStrings() - object Play : MainStrings() + object Contributors : MainStrings() class Time(val hours: Int, val minutes: Int) : MainStrings() private object Bindings : StringsBindings( Lang.Japanese to { item, _ -> when (item) { Timetable -> "タイムテーブル" - Play -> "Play" + Contributors -> "Contributors" is Time -> "${item.hours}時${item.minutes}分" } }, Lang.English to { item, bindings -> when (item) { Timetable -> "Timetable" - Play -> "Play" + Contributors -> "Contributors" is Time -> "${item.hours}:${item.minutes}" } }, diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableItemDetailScreen.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableItemDetailScreen.kt index 17e40b43f..99eefd92a 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableItemDetailScreen.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableItemDetailScreen.kt @@ -6,19 +6,38 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import io.github.droidkaigi.confsched2023.model.TimetableItem +import io.github.droidkaigi.confsched2023.model.TimetableItemId import io.github.droidkaigi.confsched2023.sessions.component.TimetableItemDetailContent import io.github.droidkaigi.confsched2023.sessions.component.TimetableItemDetailFooter import io.github.droidkaigi.confsched2023.sessions.component.TimetableItemDetailHeader import io.github.droidkaigi.confsched2023.sessions.component.TimetableItemDetailSummary import io.github.droidkaigi.confsched2023.sessions.strings.TimetableItemDetailViewModel -sealed class TimetableItemDetailScreenUiState() { - object Loading : TimetableItemDetailScreenUiState() - data class Loaded( - val timetableItem: TimetableItem, - val isBookmarked: Boolean, - ) : TimetableItemDetailScreenUiState() +const val timetableItemDetailScreenRouteItemIdParameterName = "timetableItemId" +const val timetableItemDetailScreenRoute = + "timetableItemDetail/{$timetableItemDetailScreenRouteItemIdParameterName}" + +fun NavGraphBuilder.sessionScreens(onNavigationIconClick: () -> Unit) { + composable(timetableItemDetailScreenRoute) { + TimetableItemDetailScreen( + onNavigationIconClick = onNavigationIconClick, + ) + } +} + +fun NavController.navigateToTimetableItemDetailScreen( + timetableItemId: TimetableItemId, +) { + navigate( + timetableItemDetailScreenRoute.replace( + "{$timetableItemDetailScreenRouteItemIdParameterName}", + timetableItemId.value, + ), + ) } @Composable @@ -34,6 +53,14 @@ fun TimetableItemDetailScreen( ) } +sealed class TimetableItemDetailScreenUiState() { + object Loading : TimetableItemDetailScreenUiState() + data class Loaded( + val timetableItem: TimetableItem, + val isBookmarked: Boolean, + ) : TimetableItemDetailScreenUiState() +} + @Composable private fun TimetableItemDetailScreen( uiState: TimetableItemDetailScreenUiState, diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableScreen.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableScreen.kt index 741acd111..44304909a 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableScreen.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/TimetableScreen.kt @@ -16,6 +16,9 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable import io.github.droidkaigi.confsched2023.model.TimetableItem import io.github.droidkaigi.confsched2023.sessions.component.TimetableTopArea import io.github.droidkaigi.confsched2023.sessions.component.rememberTimetableScreenScrollState @@ -24,11 +27,25 @@ import io.github.droidkaigi.confsched2023.sessions.section.TimetableSheetUiState import io.github.droidkaigi.confsched2023.ui.SnackbarMessageEffect import kotlin.math.roundToInt +const val timetableScreenRoute = "timetable" +fun NavGraphBuilder.nestedSessionScreens( + onTimetableItemClick: (TimetableItem) -> Unit, +) { + composable(timetableScreenRoute) { + TimetableScreen( + onTimetableItemClick = onTimetableItemClick, + ) + } +} + +fun NavController.navigateTimetableScreen() { + navigate(timetableScreenRoute) +} + const val TimetableScreenTestTag = "TimetableScreen" @Composable fun TimetableScreen( - onContributorsClick: () -> Unit, onTimetableItemClick: (TimetableItem) -> Unit, viewModel: TimetableScreenViewModel = hiltViewModel(), ) { @@ -43,7 +60,6 @@ fun TimetableScreen( uiState = uiState, snackbarHostState = snackbarHostState, onTimetableItemClick = onTimetableItemClick, - onContributorsClick = onContributorsClick, onBookmarkClick = viewModel::onBookmarkClick, onTimetableUiChangeClick = viewModel::onUiTypeChange, ) @@ -58,7 +74,6 @@ private fun TimetableScreen( uiState: TimetableScreenUiState, snackbarHostState: SnackbarHostState, onTimetableItemClick: (TimetableItem) -> Unit, - onContributorsClick: () -> Unit, onBookmarkClick: (TimetableItem) -> Unit, onTimetableUiChangeClick: () -> Unit, ) { @@ -92,7 +107,6 @@ private fun TimetableScreen( } }, onTimetableItemClick = onTimetableItemClick, - onContributorsClick = onContributorsClick, uiState = uiState.contentUiState, timetableScreenScrollState = state, onFavoriteClick = onBookmarkClick, diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableList.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableList.kt index 9cd0dd227..fe4812a77 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableList.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableList.kt @@ -1,9 +1,7 @@ package io.github.droidkaigi.confsched2023.sessions.section -import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -20,20 +18,11 @@ fun TimetableList( uiState: TimetableListUiState, onBookmarkClick: (TimetableItem) -> Unit, onTimetableItemClick: (TimetableItem) -> Unit, - onContributorsClick: () -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( modifier = modifier.testTag(TimetableListTestTag), ) { - item { - Text( - text = "Go to ContributorsScreen", - modifier = Modifier.clickable { - onContributorsClick() - }, - ) - } items(uiState.timetable.timetableItems) { session -> val isBookmarked = uiState.timetable.bookmarks.contains(session.id) TimetableListItem( diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt index fef3de347..61852aef8 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt @@ -45,7 +45,6 @@ fun TimetableSheet( uiState: TimetableSheetUiState, timetableScreenScrollState: TimetableScreenScrollState, onTimetableItemClick: (TimetableItem) -> Unit, - onContributorsClick: () -> Unit, onFavoriteClick: (TimetableItem) -> Unit, modifier: Modifier = Modifier, ) { @@ -92,7 +91,6 @@ fun TimetableSheet( TimetableList( uiState = uiState.timetableListUiState, onTimetableItemClick = onTimetableItemClick, - onContributorsClick = onContributorsClick, onBookmarkClick = onFavoriteClick, modifier = Modifier .fillMaxSize()