Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -48,7 +48,7 @@ class MainActivity : NordicActivity() {

setContent {
NordicTheme {
NavigationView(MainDestinations + ScannerDestinations)
NavigationView(MainDestination + ScannerDestination)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ParcelUuid, DiscoveredBluetoothDevice>("scanner")
val Scanner = createDestination<ParcelUuid?, DiscoveredBluetoothDevice>("scanner")

private val ScannerDestination = defineDestination(Scanner) {
val vm = hiltViewModel<ScannerViewModel>()
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()
}
}
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavigationDestination> {
return listOf(this, other)
}
}

/**
* A collection of destinations.
*
* @property values List of destinations within a component.
*/
class NavigationDestinations(
val values: List<NavigationDestination>,
) {
constructor(
destination: NavigationDestination,
) : this(listOf(destination))

operator fun plus(other: NavigationDestinations): NavigationDestinations {
return NavigationDestinations(values + other.values)
operator fun plus(other: List<NavigationDestination>): List<NavigationDestination> {
return listOf(this) + other
}
}

Expand All @@ -102,23 +89,4 @@ fun createSimpleDestination(name: String): DestinationId<Unit, Unit> = 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<NavigationDestination>.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)
): NavigationDestination = NavigationDestination(id, content)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package no.nordicsemi.android.common.navigation

/**
* The navigation result.
*/
sealed class NavigationResult<R> {
/** Navigation was cancelled. */
class Cancelled<R> : NavigationResult<R>()
/** The navigation has returned a result. */
data class Success<R>(val value: R) : NavigationResult<R>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -53,7 +56,7 @@ import no.nordicsemi.android.common.navigation.internal.*
*/
@Composable
fun NavigationView(
destinations: NavigationDestinations,
destinations: List<NavigationDestination>,
) {
val navHostController = rememberNavController()

Expand All @@ -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 ->
Expand All @@ -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,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
@file:Suppress("unused")

package no.nordicsemi.android.common.navigation

import android.net.Uri
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 <R> resultFrom(from: DestinationId<*, R>): Flow<R?>
fun <R> resultFrom(from: DestinationId<*, R>): Flow<NavigationResult<R>>

/**
* 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 <A> navigateTo(to: DestinationId<A, *>, args: A? = null)
fun <A> navigateTo(to: DestinationId<A, *>, args: @RawValue A)

/**
* Navigates up to previous destination, or finishes the Activity.
Expand All @@ -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 <R> navigateUpWithResult(from: DestinationId<*, R>, result: R)
fun <R> navigateUpWithResult(from: DestinationId<*, R>, result: @RawValue R)

/**
* Opens the given link in a browser.
Expand All @@ -52,15 +54,20 @@ interface Navigator {
* @param destination The current destination.
*/
fun <A> SavedStateHandle.getStateFlow(destination: DestinationId<A, *>, initial: A?): StateFlow<A?> =
@Suppress("UNCHECKED_CAST")
getStateFlow<A?>(destination.name, initial)
getStateFlow(destination.name, initial)

/**
* Returns the argument for the current destination.
*
* @param destination The current destination.
*/
fun <A> SavedStateHandle.getOrNull(destination: DestinationId<A?, *>): A? =
get(destination.name)

/**
* Returns the argument for the current destination.
*
* @param destination The current destination.
*/
@Suppress("UNCHECKED_CAST")
fun <A> SavedStateHandle.get(destination: DestinationId<A, *>): A? =
@Suppress("DEPRECATION")
get<Bundle?>(destination.name)?.get(destination.name) as? A
fun <A> SavedStateHandle.get(destination: DestinationId<A & Any, *>): A =
get(destination.name) ?: error("Destination '${destination.name}' requires a non-nullable argument")
Original file line number Diff line number Diff line change
@@ -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<A>(
val destination: DestinationId<A, *>,
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<R>(val value: @RawValue R) : NavigationResultState(), Parcelable

/**
* A navigation executor that can be used to navigate to next destination, or back.
Expand All @@ -12,7 +38,7 @@ internal interface NavigationExecutor {
*
* @param target The target target with an optional parameter.
*/
fun navigate(target: NavigationTarget)
fun <A> navigate(target: NavigationTarget<A>)

/**
* Navigate up to the previous destination passing the given result.
Expand All @@ -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)
}
Loading