Why a new ViewModel is created in each Compose Navigation route?
Usually view model is shared for the whole composables scope, and init
shouldn't be called more than once.
But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:
- By passing it directly to
viewModel
call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route. - By proving value for
LocalViewModelStoreOwner
, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
val model = viewModel<Model>()
}
}
}
Jetpack compose navigation with viewModel
collectAsState
triggers recomposition each time you emit a new value to the flow. It means, that JetSurvey0App
will be re-called.
You're trying to navigate using navigator.destination
, but you're creating a new object on each recomposition:
val navigator = Navigator()
val destination by navigator.destination.collectAsState()
You can make your WelcomeViewModel.navigator
public
instead of private
and collect its destination
- as you change the state of this particular object.
Read more about recompositions in Compose Mental Model.
How to navigate on view model field change in Compose Navigation?
I think your view model should know nothing about navigation routes. Simple verificationNeeded
flag will be enough in this case:
var verificationNeeded by mutableStateOf(false)
private set
private val signUpCallback = object : SignUpHandler {
override fun onSuccess(user: User?, signUpResult: SignUpResult?) {
verificationNeeded = true
Log.i(Constants.TAG, "sign up success")
}
override fun onFailure(exception: Exception?) {
Log.i(Constants.TAG, "sign up failure ")
}
}
The best practice is not sharing navController
outside of the view managing the NavHost
, and only pass even handlers. It may be useful when you need to test or preview your screen.
Here's how you can navigate when this flag is changed:
@Composable
fun CreateAccountScreen(
onRequestVerification: () -> Unit,
viewModel: CreateAccountViewModel = hiltViewModel(),
) {
if (viewModel.verificationNeeded) {
LaunchedEffect(Unit) {
onRequestVerification()
}
}
}
in your navigation managing view:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.CreateAccount
) {
composable(Screen.CreateAccount) {
CreateAccountScreen(
onRequestVerification = {
navController.navigate(Screen.VerifyAccountScreen)
}
)
}
}
Sharing viewModel within Jetpack Compose Navigation
You could create a viewModel and pass it trough
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationSystem()
}
}
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
val viewModel: ConversionViewModel = viewModel()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController, viewModel) }
composable("result") { ResultScreen(navController, viewModel) }
}
}
@Composable
fun HomeScreen(navController: NavController, viewModel: ConversionViewModel) {
var temp by remember { mutableStateOf("") }
val fahrenheit = temp.toIntOrNull() ?: 0
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column {
OutlinedTextField(
value = temp,
onValueChange = { temp = it },
label = { Text("Fahrenheit") },
modifier = Modifier.fillMaxWidth(0.85f)
)
Spacer(modifier = Modifier.padding(top = 16.dp))
Button(onClick = {
Log.d("HomeScreen", fahrenheit.toString())
if (fahrenheit !in 1..160) return@Button
viewModel.onCalculate(fahrenheit)
navController.navigate("result")
}) {
Text("Calculate")
}
}
}
}
@Composable
fun ResultScreen(navController: NavController, viewModel: ConversionViewModel) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
Text(
viewModel.celsius.value.toString(),
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.padding(top = 24.dp))
Button(onClick = { navController.navigate("home") }) {
Text(text = "Calculate again")
}
}
}
Cannot create an instance of viewmodel while using Jetpack Compose navigation
If your @HiltViewModel is scoped to the navigation graph use hiltNavGraphViewModel() instead of viewModel() to initialize. For more reference android documentaion
Update
hiltNavGraphViewModel() is now deprecated, use hiltViewModel() instead
Thanks to Narek Hayrapetyan for the reminder
How to save data in a composable function with view model constructor
It seems like I need to save the view model as a state or something?
You don't have to. ViewModel
s are already preserved as part of their owner scope. The same ViewModel
instance will be returned to you if you retrieve the ViewModel
s correctly.
I seem to be missing something.
You are initializing a new instance of your NFTViewModel
every time the navigation composable recomposes (gets called) instead of retrieving the NFTViewModel
from its ViewModelStoreOwner
.
You should retrieve ViewModel
s by calling viewModel()
or if you are using Hilt and @HiltViewModel
then call hiltViewModel()
instead.
No Hilt
val vm: NFTViewModel = viewModel()
Returns an existing
ViewModel
or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided byLocalViewModelStoreOwner
.
The createdViewModel
is associated with the givenviewModelStoreOwner
and will be retained as long as the owner is alive (e.g. if it is an activity, until it is finished or process is killed).
If using Hilt (i.e. your ViewModel
has the @HiltViewModel
annotation)
val vm: NFTViewModel = hiltViewModel()
Returns an existing
@HiltViewModel
-annotatedViewModel
or creates a new one scoped to the current navigation graph present on theNavController
back stack.
If no navigation graph is currently present then the current scope will be used, usually, a fragment or an activity.
The above will preserve your view model state, however you are still resetting the state inside your composable if your composable exits the composition and then re-enters it at a later time, which happens every time you navigate to a different screen (to a different "screen" composable, if it is just a dialog then the previous composable won't leave the composition, because it will be still displayed in the background).
Due to this part of the code
@Composable
fun HomeScreen(viewModel: NFTViewModel) {
val feedState by viewModel.nftResponse.observeAsState()
// this resets to false when HomeScreen leaves and later
// re-enters the composition
val fetched = remember { mutableStateOf(false) }
if (!fetched.value) {
fetched.value = true
viewModel.getFeed()
}
fetched
will always be false when you navigate to (and back to) HomeScreen
and thus getFeed()
will be called.
If you don't want to call getFeed()
when you navigate back to HomeScreen
you have to store the fetched
value somewhere else, probably inside your NFTViewModel
and only reset it to false
when you want that getFeed()
is called again.
Related Topics
Intent - If Activity Is Running, Bring It to Front, Else Start a New One (From Notification)
Session 'App': Error Launching Activity
Android Expandablelistview - Looking for a Tutorial
Add a Search Filter on Recyclerview with Cards
Does Android Support Jdk 6 or 7
Transparent Alertdialog Has Black Background
Why Does Alertdialog.Builder(Context Context) Only Accepts Activity as a Parameter
Mobile Vision API - Concatenate New Detector Object to Continue Frame Processing
How to Obtain All Details of a Contact in Android
How to Prevent Going Back to the Previous Activity
Recyclerview Gridlayoutmanager: How to Auto-Detect Span Count
Android Camera Intent Saving Image Landscape When Taken Portrait
How to Automatically Restart a Service Even If User Force Close It
Remove Application Icon and Title from Honeycomb Action Bar