Back button: fixed destinations

This commit is contained in:
Pierre-Yves Nicolas
2025-08-25 08:24:32 +02:00
parent 7983b19812
commit 27046cb1b7
5 changed files with 125 additions and 24 deletions

View File

@@ -64,15 +64,15 @@ class MainActivity : ComponentActivity() {
val cameraPermission = rememberCameraPermissionState() val cameraPermission = rememberCameraPermissionState()
MyScanTheme { MyScanTheme {
val navigation = Navigation( val navigation = Navigation(
toHomeScreen = { viewModel.navigateTo(Screen.Home) }, toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) },
toCameraScreen = { viewModel.navigateTo(Screen.Camera) }, toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) },
toDocumentScreen = { viewModel.navigateTo(Screen.Document()) }, toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) },
toAboutScreen = { viewModel.navigateTo(Screen.About) }, toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) },
toLibrariesScreen = { viewModel.navigateTo(Screen.Libraries) }, toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) },
back = { viewModel.navigateBack() } back = { viewModel.navigateBack() }
) )
when (val screen = currentScreen) { when (val screen = currentScreen) {
is Screen.Home -> { is Screen.Main.Home -> {
val recentDocs by viewModel.recentDocuments.collectAsStateWithLifecycle() val recentDocs by viewModel.recentDocuments.collectAsStateWithLifecycle()
HomeScreen( HomeScreen(
cameraPermission = cameraPermission, cameraPermission = cameraPermission,
@@ -83,17 +83,17 @@ class MainActivity : ComponentActivity() {
onOpenPdf = { file -> openPdf(file.toUri()) } onOpenPdf = { file -> openPdf(file.toUri()) }
) )
} }
is Screen.Camera -> { is Screen.Main.Camera -> {
CameraScreen( CameraScreen(
viewModel, viewModel,
navigation, navigation,
liveAnalysisState, liveAnalysisState,
onImageAnalyzed = { image -> viewModel.liveAnalysis(image) }, onImageAnalyzed = { image -> viewModel.liveAnalysis(image) },
onFinalizePressed = { viewModel.navigateTo(Screen.Document()) }, onFinalizePressed = navigation.toDocumentScreen,
cameraPermission = cameraPermission cameraPermission = cameraPermission
) )
} }
is Screen.Document -> { is Screen.Main.Document -> {
DocumentScreen ( DocumentScreen (
document = document, document = document,
initialPage = screen.initialPage, initialPage = screen.initialPage,
@@ -109,14 +109,14 @@ class MainActivity : ComponentActivity() {
), ),
onStartNew = { onStartNew = {
viewModel.startNewDocument() viewModel.startNewDocument()
viewModel.navigateTo(Screen.Home) }, viewModel.navigateTo(Screen.Main.Home) },
onDeleteImage = { id -> viewModel.deletePage(id) } onDeleteImage = { id -> viewModel.deletePage(id) }
) )
} }
is Screen.About -> { is Screen.Overlay.About -> {
AboutScreen(onBack = navigation.back, onViewLibraries = navigation.toLibrariesScreen) AboutScreen(onBack = navigation.back, onViewLibraries = navigation.toLibrariesScreen)
} }
is Screen.Libraries -> { is Screen.Overlay.Libraries -> {
LibrariesScreen(onBack = navigation.back) LibrariesScreen(onBack = navigation.back)
} }
} }

View File

@@ -73,9 +73,9 @@ class MainViewModel(
val liveAnalysisState: StateFlow<LiveAnalysisState> = _liveAnalysisState.asStateFlow() val liveAnalysisState: StateFlow<LiveAnalysisState> = _liveAnalysisState.asStateFlow()
private var lastSuccessfulLiveAnalysisState: LiveAnalysisState? = null private var lastSuccessfulLiveAnalysisState: LiveAnalysisState? = null
private val _screenStack = MutableStateFlow<List<Screen>>(listOf(Screen.Home)) private val _navigationState = MutableStateFlow(NavigationState.initial())
val currentScreen: StateFlow<Screen> = _screenStack.map { it.last() } val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
.stateIn(viewModelScope, SharingStarted.Eagerly, _screenStack.value.last()) .stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
private val _pageIds = MutableStateFlow(imageRepository.imageIds()) private val _pageIds = MutableStateFlow(imageRepository.imageIds())
val documentUiModel: StateFlow<DocumentUiModel> = val documentUiModel: StateFlow<DocumentUiModel> =
@@ -159,12 +159,12 @@ class MainViewModel(
} }
} }
fun navigateTo(screen: Screen) { fun navigateTo(destination: Screen) {
_screenStack.update { it + screen } _navigationState.update { it.navigateTo(destination) }
} }
fun navigateBack() { fun navigateBack() {
_screenStack.update { stack -> if (stack.size > 1) stack.dropLast(1) else stack } _navigationState.update { stack -> stack.navigateBack() }
} }
fun onImageCaptured(imageProxy: ImageProxy?) { fun onImageCaptured(imageProxy: ImageProxy?) {

View File

@@ -15,11 +15,15 @@
package org.mydomain.myscan package org.mydomain.myscan
sealed class Screen { sealed class Screen {
object Home : Screen() sealed class Main : Screen() {
object Camera : Screen() object Home : Main()
data class Document(val initialPage: Int = 0) : Screen() object Camera : Main()
object About : Screen() data class Document(val initialPage: Int = 0) : Main()
object Libraries : Screen() }
sealed class Overlay : Screen() {
object About : Overlay()
object Libraries : Overlay()
}
} }
data class Navigation( data class Navigation(
@@ -30,3 +34,30 @@ data class Navigation(
val toLibrariesScreen: () -> Unit, val toLibrariesScreen: () -> Unit,
val back: () -> Unit, val back: () -> Unit,
) )
@ConsistentCopyVisibility
data class NavigationState private constructor(val stack: List<Screen>) {
companion object {
fun initial() = NavigationState(listOf(Screen.Main.Home))
}
val current: Screen get() = stack.last()
fun navigateTo(destination: Screen): NavigationState {
return if (destination is Screen.Overlay) {
copy(stack = stack + destination)
} else {
copy(stack = listOf(destination))
}
}
fun navigateBack(): NavigationState {
return when (current) {
is Screen.Main.Home -> this // Back handled by system
is Screen.Main.Camera -> copy(stack = listOf(Screen.Main.Home))
is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera))
is Screen.Overlay -> copy(stack = stack.dropLast(1))
}
}
}

View File

@@ -154,7 +154,7 @@ fun CameraScreen(
pageListState = pageListState =
CommonPageListState( CommonPageListState(
document = document, document = document,
onPageClick = { index -> viewModel.navigateTo(Screen.Document(index)) }, onPageClick = { index -> viewModel.navigateTo(Screen.Main.Document(index)) },
listState = listState, listState = listState,
onLastItemPosition = { offset -> thumbnailCoords.value = offset }, onLastItemPosition = { offset -> thumbnailCoords.value = offset },
), ),

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mydomain.myscan.Screen.Main.Camera
import org.mydomain.myscan.Screen.Main.Document
import org.mydomain.myscan.Screen.Main.Home
import org.mydomain.myscan.Screen.Overlay.About
import org.mydomain.myscan.Screen.Overlay.Libraries
class NavigationTest {
@Test
fun empty_ScreenStack() {
val empty = NavigationState.initial()
assertThat(empty.current).isEqualTo(Home)
assertThat(empty.navigateBack()).isEqualTo(empty)
}
@Test
fun navigate_between_fixed_screens() {
val atHome = NavigationState.initial()
val atCamera = atHome.navigateTo(Camera)
val atDocument = atHome.navigateTo(Document())
assertThat(atHome.current).isEqualTo(Home)
assertThat(atCamera.current).isEqualTo(Camera)
assertThat(atDocument.current).isEqualTo(Document())
assertThat(atCamera.navigateTo(Document())).isEqualTo(atDocument)
assertThat(atDocument.navigateTo(Home)).isEqualTo(atHome)
assertThat(atDocument.navigateTo(Camera)).isEqualTo(atCamera)
assertThat(atHome.navigateBack()).isEqualTo(atHome)
assertThat(atCamera.navigateBack()).isEqualTo(atHome)
assertThat(atDocument.navigateBack()).isEqualTo(atCamera)
}
@Test
fun navigate_to_secondary_screens() {
val atHome = NavigationState.initial()
val atCamera = atHome.navigateTo(Camera)
val atAboutAfterHome = atHome.navigateTo(About)
assertThat(atAboutAfterHome.current).isEqualTo(About)
assertThat(atAboutAfterHome.navigateBack()).isEqualTo(atHome)
val atAboutAfterCamera = atCamera.navigateTo(About)
assertThat(atAboutAfterCamera.current).isEqualTo(About)
assertThat(atAboutAfterCamera.navigateBack()).isEqualTo(atCamera)
val atLibrariesAfterCameraAbout = atAboutAfterCamera.navigateTo(Libraries)
assertThat(atLibrariesAfterCameraAbout.current).isEqualTo(Libraries)
assertThat(atLibrariesAfterCameraAbout.navigateBack()).isEqualTo(atAboutAfterCamera)
}
}