Skip to content

Commit

Permalink
Merge pull request #245 from DroidKaigi/takahirom/refactor-navigation…
Browse files Browse the repository at this point in the history
…/2023-07-08

Refactor navigation
  • Loading branch information
takahirom authored Jul 8, 2023
2 parents 085e167 + eb2cd67 commit 8c3fdcf
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<ContributorsViewModel>())
}
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<ContributorsViewModel>())
}
},
)
}

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)
}
}
}
1 change: 1 addition & 0 deletions core/testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,7 +34,7 @@ class KaigiAppRobot @Inject constructor(

fun goToContributor() {
composeTestRule
.onNodeWithText("Go to ContributorsScreen")
.onNodeWithContentDescription(MainScreenTab.Contributor.contentDescription)
.performClick()
waitUntilIdle()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class TimetableScreenRobot @Inject constructor(
composeTestRule.setContent {
KaigiTheme {
TimetableScreen(
onContributorsClick = { },
onTimetableItemClick = { },
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
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
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<MainScreenViewModel>(),
mainNestedNavGraph: NavGraphBuilder.(NavController, PaddingValues) -> Unit,
) {
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = SnackbarHostState()
Expand All @@ -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(),
),
}

Expand All @@ -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(
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import io.github.droidkaigi.confsched2023.designsystem.strings.StringsBindings

sealed class MainStrings : Strings<MainStrings>(Bindings) {
object Timetable : MainStrings()
object Play : MainStrings()
object Contributors : MainStrings()
class Time(val hours: Int, val minutes: Int) : MainStrings()

private object Bindings : StringsBindings<MainStrings>(
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}"
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 8c3fdcf

Please sign in to comment.