diff --git a/app/src/main/java/no/nordicsemi/android/common/test/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/common/test/MainActivity.kt index 71008672d..621e3503a 100644 --- a/app/src/main/java/no/nordicsemi/android/common/test/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/common/test/MainActivity.kt @@ -35,8 +35,8 @@ import android.os.Bundle import androidx.activity.compose.setContent import dagger.hilt.android.AndroidEntryPoint import no.nordicsemi.android.common.navigation.NavigationView -import no.nordicsemi.android.common.test.main.MainDestinations -import no.nordicsemi.android.common.test.scanner.ScannerDestinations +import no.nordicsemi.android.common.test.main.MainDestination +import no.nordicsemi.android.common.test.scanner.ScannerDestination import no.nordicsemi.android.common.theme.NordicActivity import no.nordicsemi.android.common.theme.NordicTheme @@ -48,7 +48,7 @@ class MainActivity : NordicActivity() { setContent { NordicTheme { - NavigationView(MainDestinations + ScannerDestinations) + NavigationView(MainDestination + ScannerDestination) } } } diff --git a/app/src/main/java/no/nordicsemi/android/common/test/main/Main.kt b/app/src/main/java/no/nordicsemi/android/common/test/main/Main.kt index a07b080d4..228c8f389 100644 --- a/app/src/main/java/no/nordicsemi/android/common/test/main/Main.kt +++ b/app/src/main/java/no/nordicsemi/android/common/test/main/Main.kt @@ -24,11 +24,9 @@ val Main = createSimpleDestination("main") * * Optionally, this can define a local Router for routing navigation within the module. */ -private val MainDestination = defineDestination(Main) { - MainScreen() - } - -val MainDestinations = MainDestination.asDestinations() +val MainDestination = defineDestination(Main) { + MainScreen() +} @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/no/nordicsemi/android/common/test/main/page/BasicViewsPage.kt b/app/src/main/java/no/nordicsemi/android/common/test/main/page/BasicViewsPage.kt index c0c3e70a8..c242ba0be 100644 --- a/app/src/main/java/no/nordicsemi/android/common/test/main/page/BasicViewsPage.kt +++ b/app/src/main/java/no/nordicsemi/android/common/test/main/page/BasicViewsPage.kt @@ -21,9 +21,8 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* +import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.common.test.R import no.nordicsemi.android.common.test.scanner.Scanner @@ -58,7 +57,8 @@ class BasicPageViewModel @Inject constructor( init { navigator.resultFrom(Scanner) // Filter out results of cancelled navigation. - .mapNotNull { it } + .mapNotNull { it as? NavigationResult.Success } + .map { it.value } // Save the result in SavedStateHandle. .onEach { savedStateHandle[DEVICE_KEY] = it } // And finally, launch the flow in the ViewModelScope. diff --git a/app/src/main/java/no/nordicsemi/android/common/test/scanner/Scanner.kt b/app/src/main/java/no/nordicsemi/android/common/test/scanner/Scanner.kt index 14bbe36d6..f275c8bb3 100644 --- a/app/src/main/java/no/nordicsemi/android/common/test/scanner/Scanner.kt +++ b/app/src/main/java/no/nordicsemi/android/common/test/scanner/Scanner.kt @@ -1,44 +1,28 @@ package no.nordicsemi.android.common.test.scanner import android.os.ParcelUuid -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import no.nordicsemi.android.common.navigation.* +import no.nordicsemi.android.common.navigation.createDestination +import no.nordicsemi.android.common.navigation.defineDestination +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel import no.nordicsemi.android.common.ui.scanner.DeviceSelected import no.nordicsemi.android.common.ui.scanner.ScannerScreen -import no.nordicsemi.android.common.ui.scanner.ScannerScreenResult import no.nordicsemi.android.common.ui.scanner.ScanningCancelled import no.nordicsemi.android.common.ui.scanner.model.DiscoveredBluetoothDevice -import javax.inject.Inject -val Scanner = createDestination("scanner") +val Scanner = createDestination("scanner") -private val ScannerDestination = defineDestination(Scanner) { - val vm = hiltViewModel() - val uuid by vm.uuid.collectAsState() +val ScannerDestination = defineDestination(Scanner) { + val vm: SimpleNavigationViewModel = hiltViewModel() + val uuid = vm.nullableParameterOf(Scanner) - ScannerScreen(uuid = uuid) { - vm.onEvent(it) - } -} - -val ScannerDestinations = ScannerDestination.asDestinations() - -@HiltViewModel -private class ScannerViewModel @Inject constructor( - private val navigator: Navigator, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - val uuid = savedStateHandle.getStateFlow(Scanner, null) - - fun onEvent(event: ScannerScreenResult) { - when (event) { - is DeviceSelected -> navigator.navigateUpWithResult(Scanner, event.device) - ScanningCancelled -> navigator.navigateUp() + ScannerScreen( + uuid = uuid, + onResult = { result -> + when (result) { + is DeviceSelected -> vm.navigateUpWithResult(Scanner, result.device) + is ScanningCancelled -> vm.navigateUp() + } } - } + ) } \ No newline at end of file diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationDestination.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationDestination.kt index de749241f..73817d194 100644 --- a/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationDestination.kt +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationDestination.kt @@ -59,25 +59,12 @@ data class NavigationDestination( val content: @Composable () -> Unit ) { - operator fun plus(other: NavigationDestination): NavigationDestinations { - return listOf(this, other).asDestinations() + operator fun plus(other: NavigationDestination): List { + return listOf(this, other) } -} -/** - * A collection of destinations. - * - * @property values List of destinations within a component. - */ -class NavigationDestinations( - val values: List, -) { - constructor( - destination: NavigationDestination, - ) : this(listOf(destination)) - - operator fun plus(other: NavigationDestinations): NavigationDestinations { - return NavigationDestinations(values + other.values) + operator fun plus(other: List): List { + return listOf(this) + other } } @@ -102,23 +89,4 @@ fun createSimpleDestination(name: String): DestinationId = Destinati fun defineDestination( id: DestinationId<*, *>, content: @Composable () -> Unit -): NavigationDestination = NavigationDestination(id, content) - -/** - * Helper method for creating a [NavigationDestinations]. - * - * This destination can be routed using global router specified - * in the [NavigationView]. - */ -fun List.asDestinations() = - NavigationDestinations(this) - -/** - * Helper method for creating a [NavigationDestinations] - * from a single destination. - * - * This destination can be routed using global router specified - * in the [NavigationView]. - */ -fun NavigationDestination.asDestinations() = - NavigationDestinations(this) \ No newline at end of file +): NavigationDestination = NavigationDestination(id, content) \ No newline at end of file diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationResult.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationResult.kt new file mode 100644 index 000000000..8c260cd10 --- /dev/null +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationResult.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.common.navigation + +/** + * The navigation result. + */ +sealed class NavigationResult { + /** Navigation was cancelled. */ + class Cancelled : NavigationResult() + /** The navigation has returned a result. */ + data class Success(val value: R) : NavigationResult() +} \ No newline at end of file diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationView.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationView.kt index ec60821f8..41888784d 100644 --- a/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationView.kt +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/NavigationView.kt @@ -42,7 +42,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import no.nordicsemi.android.common.navigation.internal.* +import no.nordicsemi.android.common.navigation.internal.Cancelled +import no.nordicsemi.android.common.navigation.internal.NavigationViewModel +import no.nordicsemi.android.common.navigation.internal.NavigationViewModel.Event +import no.nordicsemi.android.common.navigation.internal.navigate /** * A navigation view allows navigating between different destinations. @@ -53,7 +56,7 @@ import no.nordicsemi.android.common.navigation.internal.* */ @Composable fun NavigationView( - destinations: NavigationDestinations, + destinations: List, ) { val navHostController = rememberNavController() @@ -63,8 +66,8 @@ fun NavigationView( val event by navigation.events.collectAsState() event?.let { e -> when (e) { - is NavigateTo -> navHostController.navigate(e.route, e.args) - is NavigateUp -> { + is Event.NavigateTo -> navHostController.navigate(e.route, e.args) + is Event.NavigateUp -> { val activity = LocalContext.current as Activity // Navigate up to the previous destination, passing the result. navHostController.currentBackStackEntry?.destination?.route?.let { route -> @@ -84,9 +87,9 @@ fun NavigationView( NavHost( navController = navHostController, - startDestination = destinations.values.first().id.name, + startDestination = destinations.first().id.name, ) { - destinations.values.forEach { destination -> + destinations.forEach { destination -> composable( route = destination.id.name, ) { diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/Navigator.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/Navigator.kt index aae8e2609..00ee5708d 100644 --- a/navigation/src/main/java/no/nordicsemi/android/common/navigation/Navigator.kt +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/Navigator.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package no.nordicsemi.android.common.navigation import android.net.Uri @@ -5,25 +7,25 @@ import android.os.Bundle import androidx.lifecycle.SavedStateHandle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.RawValue interface Navigator { /** * Creates a flow that will emit the results of the navigation from the given destination. * - * Null is emitted when the navigation was cancelled. - * * @param from The origin destination to listen for results from. */ - fun resultFrom(from: DestinationId<*, R>): Flow + fun resultFrom(from: DestinationId<*, R>): Flow> /** * Requests navigation to the given destination. An optional parameter can be passed. * * @param to The destination to navigate to. - * @param args An optional argument to pass to the destination. + * @param args An optional argument to pass to the destination. The argument will be saved + * in [SavedStateHandle], therefore it must be savable to a [Bundle]. */ - fun navigateTo(to: DestinationId, args: A? = null) + fun navigateTo(to: DestinationId, args: @RawValue A) /** * Navigates up to previous destination, or finishes the Activity. @@ -35,10 +37,10 @@ interface Navigator { * * @param from The destination from which navigating up. * @param result The result, which will be passed to the previous destination. - * The returned type will be saved in [SavedStateHandle], therefore it must be + * The returned object will be saved in [SavedStateHandle], therefore it must be * savable to a [Bundle]. */ - fun navigateUpWithResult(from: DestinationId<*, R>, result: R) + fun navigateUpWithResult(from: DestinationId<*, R>, result: @RawValue R) /** * Opens the given link in a browser. @@ -52,15 +54,20 @@ interface Navigator { * @param destination The current destination. */ fun SavedStateHandle.getStateFlow(destination: DestinationId, initial: A?): StateFlow = - @Suppress("UNCHECKED_CAST") - getStateFlow(destination.name, initial) + getStateFlow(destination.name, initial) + +/** + * Returns the argument for the current destination. + * + * @param destination The current destination. + */ +fun SavedStateHandle.getOrNull(destination: DestinationId): A? = + get(destination.name) /** * Returns the argument for the current destination. * * @param destination The current destination. */ -@Suppress("UNCHECKED_CAST") -fun SavedStateHandle.get(destination: DestinationId): A? = - @Suppress("DEPRECATION") - get(destination.name)?.get(destination.name) as? A \ No newline at end of file +fun SavedStateHandle.get(destination: DestinationId): A = + get(destination.name) ?: error("Destination '${destination.name}' requires a non-nullable argument") \ No newline at end of file diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationExecutor.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationExecutor.kt index e8f4f01ad..b50e58dbd 100644 --- a/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationExecutor.kt +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationExecutor.kt @@ -1,7 +1,33 @@ package no.nordicsemi.android.common.navigation.internal import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.bundleOf import androidx.lifecycle.SavedStateHandle +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import no.nordicsemi.android.common.navigation.DestinationId + +/** + * Navigation target. This class wraps the destination and the parameter. + * + * @property destination The destination id. + * @property args Optional + */ +internal data class NavigationTarget( + val destination: DestinationId, + val args: @RawValue A +) { + fun toBundle() = bundleOf(destination.name to args) +} + +internal sealed class NavigationResultState +@Parcelize +internal object Initial : NavigationResultState(), Parcelable +@Parcelize +internal object Cancelled : NavigationResultState(), Parcelable +@Parcelize +internal data class Success(val value: @RawValue R) : NavigationResultState(), Parcelable /** * A navigation executor that can be used to navigate to next destination, or back. @@ -12,7 +38,7 @@ internal interface NavigationExecutor { * * @param target The target target with an optional parameter. */ - fun navigate(target: NavigationTarget) + fun navigate(target: NavigationTarget) /** * Navigate up to the previous destination passing the given result. @@ -21,5 +47,5 @@ internal interface NavigationExecutor { * The returned type will be saved in [SavedStateHandle], therefore it must be * savable to a [Bundle]. */ - fun navigateUpWithResult(result: NavigationResult) + fun navigateUpWithResult(result: NavigationResultState) } \ No newline at end of file diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationManager.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationManager.kt index 8bf4630d5..354736857 100644 --- a/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationManager.kt +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationManager.kt @@ -3,35 +3,17 @@ package no.nordicsemi.android.common.navigation.internal import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Parcelable import android.util.Log -import androidx.core.os.bundleOf import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.transform -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue import no.nordicsemi.android.common.navigation.DestinationId +import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator import javax.inject.Inject -internal data class NavigationTarget( - val destination: DestinationId<*, *>, - val args: @RawValue Any? -) { - fun toBundle() = args?.let { bundleOf(destination.name to args) } -} - -internal sealed class NavigationResult -@Parcelize -internal object Initial : NavigationResult(), Parcelable -@Parcelize -internal object Cancelled : NavigationResult(), Parcelable -@Parcelize -internal class Success(val value: @RawValue Any) : NavigationResult(), Parcelable - /** * A navigation manager that can be used to navigate to next destination, or back. * @@ -47,28 +29,28 @@ internal class NavigationManager @Inject constructor( internal var executor: NavigationExecutor? = null internal var savedStateHandle: SavedStateHandle? = null - override fun resultFrom(from: DestinationId<*, R>): Flow = + override fun resultFrom(from: DestinationId<*, R>): Flow> = @Suppress("UNCHECKED_CAST") savedStateHandle?.run { - getStateFlow(from.name, Initial) + getStateFlow(from.name, Initial) .transform { result -> when (result) { // Ignore the initial value. is Initial -> {} // Return success result. - is Success -> emit(result.value as R) + is Success<*> -> emit(NavigationResult.Success(result.value as R)) // Return null when cancelled. - is Cancelled -> emit(null) + is Cancelled -> emit(NavigationResult.Cancelled()) } } } ?: throw IllegalStateException("SavedStateHandle is not set") - override fun navigateTo(to: DestinationId, args: A?) { + override fun navigateTo(to: DestinationId, args: A) { executor?.navigate(NavigationTarget(to, args)) } override fun navigateUpWithResult(from: DestinationId<*, R>, result: R) { - executor?.navigateUpWithResult(Success(result as Any)) + executor?.navigateUpWithResult(Success(result)) } override fun navigateUp() { diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationViewModel.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationViewModel.kt index 290dd67ea..0249ae6ff 100644 --- a/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationViewModel.kt +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/internal/NavigationViewModel.kt @@ -9,15 +9,17 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import javax.inject.Inject -internal sealed class NavigationEvent -internal data class NavigateTo(val route: String, val args: Bundle?) : NavigationEvent() -internal data class NavigateUp(val result: Any?) : NavigationEvent() - @HiltViewModel internal class NavigationViewModel @Inject constructor( private val navigationManager: NavigationManager, ): ViewModel(), NavigationExecutor { - private val _events = MutableStateFlow(null) + /** The navigation events class. */ + sealed class Event { + data class NavigateTo(val route: String, val args: Bundle?) : Event() + data class NavigateUp(val result: Any?) : Event() + } + + private val _events = MutableStateFlow(null) val events = _events.asStateFlow() init { @@ -43,12 +45,12 @@ internal class NavigationViewModel @Inject constructor( _events.update { null } } - override fun navigate(target: NavigationTarget) { - _events.update { NavigateTo(target.destination.name, target.toBundle()) } + override fun navigate(target: NavigationTarget) { + _events.update { Event.NavigateTo(target.destination.name, target.toBundle()) } } - override fun navigateUpWithResult(result: NavigationResult) { - _events.update { NavigateUp(result) } + override fun navigateUpWithResult(result: NavigationResultState) { + _events.update { Event.NavigateUp(result) } } override fun onCleared() { diff --git a/navigation/src/main/java/no/nordicsemi/android/common/navigation/viewmodel/SimpleNavigationViewModel.kt b/navigation/src/main/java/no/nordicsemi/android/common/navigation/viewmodel/SimpleNavigationViewModel.kt new file mode 100644 index 000000000..3002264c0 --- /dev/null +++ b/navigation/src/main/java/no/nordicsemi/android/common/navigation/viewmodel/SimpleNavigationViewModel.kt @@ -0,0 +1,45 @@ +package no.nordicsemi.android.common.navigation.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.common.navigation.DestinationId +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.get +import no.nordicsemi.android.common.navigation.getOrNull +import javax.inject.Inject + +@Suppress("unused") +@HiltViewModel +open class SimpleNavigationViewModel @Inject constructor( + navigator: Navigator, + private val savedStateHandle: SavedStateHandle, +): ViewModel(), Navigator by navigator { + + /** + * Returns the parameter of the current destination, or null, if hasn't been set. + */ + fun nullableParameterOf(destinationId: DestinationId): A? { + return savedStateHandle.getOrNull(destinationId) + } + + /** + * Returns the parameter of the current destination, or null, if hasn't been set. + */ + fun parameterOf(destinationId: DestinationId): A { + return savedStateHandle.get(destinationId) + } + + /** + * Returns the parameter of the current destination, or null, if hasn't been set. + */ + protected fun DestinationId.getParameterOrNull(): A? = + nullableParameterOf(this) + + /** + * Returns the parameter of the current destination. + */ + protected fun DestinationId.getParameter(): A = + parameterOf(this) + +} \ No newline at end of file