ViewModels
Enro allows ViewModels to access the NavigationHandle
for the screen that they are being used by, which allows navigation logic to be managed from within a ViewModel. This is functionality is optional, and some people prefer to leave the navigation logic within the View layer of their applications.
One of the main advantages of using ViewModels for navigation logic is that ViewModels can be tested with Unit tests. This allows you to test the navigation logic of your application without needing to run an emulator or device. For more information on testing, see Testing.
Getting a NavigationHandle inside a ViewModel
To get a NavigationHandle from inside of a ViewModel, use the by navigationHandle<T>()
property delegate. This will return a TypedNavigationHandle<T>
. For more information on what a NavigationHandle is, see Navigation Handles.
@Parcelize
class ExampleNavigationKey(
val exampleArgument: String
) : NavigationKey.SupportsPush
class ExampleViewModel : ViewModel() {
private val navigation by navigationHandle<ExampleNavigationKey>()
}
With a NavigationHandle inside of your ViewModel, you are able to perform all the regular functions that are available on a NavigationHandle.
@Parcelize
class ExampleNavigationKey(
val exampleArgument: String
) : NavigationKey.SupportsPush
class ExampleViewModel : ViewModel() {
private val navigation by navigationHandle<ExampleNavigationKey>()
fun goToNextScreen() {
val argument = navigation.key.exampleArgument
val nextScreenKey = NextScreenKey(nextScreenArgument = argument.hashCode())
navigation.push(nextScreenKey)
}
}
Handle Results
ViewModels are also able to create result channels, and manage results. To create a NavigationResultChannel, use the by registerForNavigationResult<T>()
property delegate. This will return a NavigationResultChannel that will handle results of type T
. For more information of NavigationResultChannels, see Navigation Results.
@Parcelize
class ExampleViewModel : ViewModel() {
private val navigation by navigationHandle<ExampleNavigationKey>()
private val stringResultChannel by registerForNavigationResult<String> { result: String ->
// ...
}
fun onRequestString() {
stringResultChannel.present(RequestStringKey())
}
}
Creating navigation aware ViewModels
To ensure that a NavigationHandle is available for by navigationHandle<T>()
, the ViewModelProvider.Factory
that is used to create the ViewModel must be made aware of the local NavigationHandle. Exactly how this works depends on whether the navigation destination is an Activity/Fragment/Composable.
Composables
By default, all Composable navigation destinations (i.e. Composables annotated with NavigationDestination) already have a ViewModel Factory that is aware of the NavigationHandle. This means that ViewModels created using the standard Composable viewModel<T>()
function should be able to use by navigationHandle<T>()
, as long as a custom ViewModelProvider.Factory is not passed as a parameter to the viewModel<T>()
function.
If you are passing a custom ViewModelProvider.Factory to this function, you will need to bind the NavigationHandle to the factory using withNavigationHandle
.
@Composable
@NavigationDestination(ExampleNavigationKey::class)
fun ViewModelExample() {
// No factory provided:
val firstViewModel = viewModel<FirstViewModel>()
// Custom factory, using `withNavigationHandle()`:
val secondViewModel = viewModel<SecondViewModel>(
factory = CustomViewModelFactory().withNavigationHandle()
)
}
If you use the same custom ViewModelProvider.Factory for all screens in your application, you may want to globally set the ViewModel Factory for all Composable navigation destinations, and ensure that this globally set factory also provides a NavigationHandle.
To do this, you can configure the composeEnvironment
in the createNavigationController
block of your Application class. The composeEnvironment
configuration allows you to provide a common rendering environment for all Composable destinations, and is useful to globally set a theme, or provide screen-specific CompositionLocals.
Here is an example of a composeEnvironment
block which will wrap the LocalViewModelStoreOwner with a special ViewModelStoreOwner that implements HasDefaultViewModelProviderFactory, and provides a custom ViewModelProvider.Factory. This will allow any screen to use the Composable viewModel<T>()
function without needing to specify a custom ViewModelProvider.Factory, and which will allow these ViewModels to access a NavigationHandle.
@NavigationComponent
class ExampleApplication : Application(), NavigationApplication {
override val navigationController = createNavigationController {
composeEnvironment { content ->
ProvideCustomViewModelFactory(content)
}
}
}
@Composable
fun ProvideCustomViewModelFactory(content: @Composable () -> Unit) {
val navigation = navigationHandle()
val viewModelStoreOwner = LocalViewModelStoreOwner.current
val wrappedViewModelStoreOwner = remember(navigation, viewModelStoreOwner) {
WrappedViewModelStore(
wrapped = requireNotNull(viewModelStoreOwner),
factory = CustomViewModelFactory().withNavigationHandle(navigation)
)
}
CompositionLocalProvider(
LocalViewModelStoreOwner provides wrappedViewModelStoreOwner
) {
content()
}
}
class WrappedViewModelStore(
val wrapped: ViewModelStoreOwner,
val factory: ViewModelProvider.Factory
) : ViewModelStoreOwner, HasDefaultViewModelProviderFactory {
override val viewModelStore: ViewModelStore
get() = wrapped.viewModelStore
override val defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = factory
override val defaultViewModelCreationExtras: CreationExtras
get() = when (wrapped) {
is HasDefaultViewModelProviderFactory -> wrapped.defaultViewModelCreationExtras
else -> super.defaultViewModelCreationExtras
}
}
Enro provides a ProvideViewModelFactory
utility method which can be used to achieve the same as the code above.
@NavigationComponent
class ExampleApplication : Application(), NavigationApplication {
override val navigationController = createNavigationController {
composeEnvironment { content ->
ProvideViewModelFactory(
factory = CustomViewModelFactory(),
content = content
)
}
}
}
Hilt
If you are using Hilt, and the Activity hosting your Composable destinations is an AndroidEntryPoint, you should not need to provide a custom factory at all, and the Composable viewModel<T>()
function should use the Hilt ViewModelProvider.Factory.
Activities and Fragments
From an Activity or Fragment, you have two options for making sure that a ViewModel has a NavigationHandle available:
- Use
by enroViewModels<T>()
instead ofby viewModels<T>()
class ExampleActivity : AppCompatActivity() {
private val firstViewModel by enroViewModels<FirstViewModel>()
private val secondViewModel by enroViewModels<SecondViewModel>(
factoryProducer = { CustomViewModelFactory() }
)
}
- Use
withNavigationHandle()
to bind a NavigationHandle to an existing ViewModelProvider.Factory
class ExampleActivity : AppCompatActivity() {
private val navigation by navigationHandle()
private val firstViewModel by viewModels<FirstViewModel>(
factoryProducer = {
CustomViewModelFactory().withNavigationHandle(navigation)
}
)
}