Back button: fixed destinations
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?) {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
),
|
),
|
||||||
|
|||||||
70
app/src/test/java/org/mydomain/myscan/NavigationTest.kt
Normal file
70
app/src/test/java/org/mydomain/myscan/NavigationTest.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user