I have a composable function for the home screen, whenever I navigate to a new composable and come back to this function everything is re created. Do I have to move the view model somewhere higher? home screen
@Composable
fun HomeScreen(viewModel: NFTViewModel = viewModel()) {
val feedState by viewModel.nftResponse.observeAsState()
if (feedState?.status == Result.Status.SUCCESS) {
val nfts = feedState?.data?.content
LazyColumn {
itemsIndexed(nfts!!) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
.clickable { },
elevation = 8.dp
) {
Column(
modifier = Modifier.padding(15.dp)
) {
Image(
painter = rememberAsyncImagePainter(
item.firebaseUri
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.height(250.dp)
)
Text(
buildAnnotatedString {
append("Contract Address: ")
withStyle(
style = SpanStyle(fontWeight = FontWeight.W900, color = Color(0xFF4552B8))
) {
append(item.contractAddress)
}
}
)
Text(
buildAnnotatedString {
append("Chain: ")
withStyle(style = SpanStyle(fontWeight = FontWeight.W900)) {
append(item.chain.displayName)
}
append("")
}
)
Text(
buildAnnotatedString {
append("Price: ")
withStyle(style = SpanStyle(fontWeight = FontWeight.W900)) {
append(item.price)
}
append("")
}
)
}
}
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.white))
.wrapContentSize(Alignment.Center)
) {
Text(
text = "Feed",
fontWeight = FontWeight.Bold,
color = Color.DarkGray,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
fontSize = 25.sp
)
}
}
}
Navigation
@Composable
fun Navigation(navController: NavHostController) {
NavHost(navController, startDestination = NavigationItem.Home.route) {
composable(NavigationItem.Home.route) {
val vm: NFTViewModel = viewModel()
HomeScreen(vm)
}
composable(NavigationItem.Add.route) {
AddScreen()
}
composable(NavigationItem.Wallets.route) {
WalletsScreen()
}
composable(NavigationItem.Popular.route) {
PopularScreen()
}
composable(NavigationItem.Profile.route) {
ProfileScreen()
}
}
}
It seems like I need to save the view model as a state or something? I seem to be missing something. Basically I dont want to trigger getFeed() everytime the composable function is called, where do I save the data in the compose function, in the view model?
EDIT now calling getFeed from init in the view model
class NFTViewModel : ViewModel() {
private val nftRepository = NFTRepository()
private var successResult: Result<NftResponse>? = null
private var _nftResponse = MutableLiveData<Result<NftResponse>>()
val nftResponse: LiveData<Result<NftResponse>>
get() = _nftResponse
init {
successResult?.let {
_nftResponse.postValue(successResult!!)
} ?: kotlin.run {
getFeed() //this is always called still successResult is always null
}
}
private fun getFeed() {
viewModelScope.launch {
_nftResponse.postValue(Result(Result.Status.LOADING, null, null))
val data = nftRepository.getFeed()
if (data.status == Result.Status.SUCCESS) {
successResult = data
_nftResponse.postValue(Result(Result.Status.SUCCESS, data.data, data.message))
} else {
_nftResponse.postValue(Result(Result.Status.ERROR, null, data.message))
}
}
}
override fun onCleared() {
Timber.tag(Constants.TIMBER).d("onCleared")
super.onCleared()
}
}
However it still seems like a new view model is being created.
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.