When would one use the findNavController method by itself instead of NavHostFragment? - android

I'm working on a tutorial for working with LiveData in Kotlin here and I've come to a point in the instructions where one of my fragments says to use the NavHostFragment like so:
/**
* Called when the game is finished
*/
private fun gameFinished() {
Toast.makeText(activity, "Game has just finished.", Toast.LENGTH_LONG).show()
val action = GameFragmentDirections.actionGameToScore()
action.score = viewModel.score.value?:0
NavHostFragment.findNavController(this).navigate(action)
viewModel.onGameFinishComplete()
}
But then, I ended up setting up an Observer with a lambda function in it in another fragment, and they say to use findNavController by itself with no arguments in this section:
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score)
viewModel = ViewModelProvider(this, viewModelFactory).get(ScoreViewModel::class.java)
// sets observer
viewModel.score.observe(viewLifecycleOwner, Observer {newScore ->
binding.scoreText.text = newScore.toString()
})
viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer {playAgain ->
if(playAgain) {
findNavController().navigate(ScoreFragmentDirections.actionRestart())
viewModel.onPlayAgainComplete()
}
})
I'm not entirely sure why one would require the call to the static method of the NavHostFragment class with 'this' sent in as a context in the first piece of code, but why one is able to call the method alone in the second one. Does the Observer or the viewLifecycleOwner or the viewModel generate an implied context in this wrapped block or something?

They are both same methods. Fragment's findNavController source code is
fun Fragment.findNavController(): NavController =
NavHostFragment.findNavController(this)

Related

LiveData observer is not removed

I am trying to get LiveData updates in a ViewModel, and make sure that the observer is not leaking, but it is leaking. The typical problem is that the observer is not stored in a variable, but that is not the case here; the lambda is stored in a variable.
private val observer: (List<MusicFile>) -> Unit =
{ musicFiles: List<MusicFile> ->
_uiState.postValue(FragmentMusicListState(musicFiles))
}
init {
musicRepository.musicFiles.observeForever(observer)
}
#VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public override fun onCleared() {
super.onCleared()
musicRepository.musicFiles.removeObserver(observer)
}
The problem is that after onCleared is called, the observer is still attached. I verified this with the following test.
#Test
fun onCleared_RemovesObserver() {
val musicRepository = MusicRepository.getInstance()
val context = InstrumentationRegistry.getInstrumentation().targetContext
musicRepository.loadMusicFiles(context.contentResolver)
val musicFiles = musicRepository.musicFiles
val viewModel = FragmentMusicListViewModel(SavedStateHandle(), musicRepository)
viewModel.onCleared()
assert(!musicFiles.hasObservers())
}
In addition, I have debugged the test on the last line, and musicFile's attributes show the observer is still attached. Attributes mActiveCount and mObservers show the observer is still attached,
How do I actually remove the observer?
LiveData takes an argument of type Observer<T> in observeForever and removeObserver. Observer is what Kotlin considers a functional interface written in Java in the androidx.lifecycle library.
What you are passing in is of type (List<MusicFile>) -> Unit.
This is a high order function and is not the same type as Observer<List<MusicFile>>. They are functionally similar in that they both have one parameter of type List<MusicFile> and both return Unit, so what Kotlin does for the better or for the worse (the worse in this case) is it will "cast" the type for you.
When Kotlin "casts" from high-order function to functional interface it is creating a new object. This happens every single time in your code when either observeForever or removeObserver are called. That's why removeObserver isn't working, because you're actually not passing in the same object despite how the code looks. I've written about this before.
In short, you can fix your problem by changing the type of observer to Observer:
private val observer: Observer<List<MusicFile>> =
Observer { musicFiles: List<MusicFile> ->
// _uiState.postValue(FragmentMusicListState(musicFiles))
}

How to initialize a field in viewModel with suspend method

How to initialize a field in view model if I need to call the suspend function to get the value?
I a have suspend function that returns value from a database.
suspend fun fetchProduct(): Product
When I create the view model I have to get product in this field
private val selectedProduct: Product
I tried doing it this way but it doesn't work because I'm calling this method outside of the coroutines
private val selectedProduct: Product = repository.fetchProduct()
You can't initialize a field in the way you described. suspend function must be called from a coroutine or another suspend function. To launch a coroutine there are a couple of builders for that: CoroutineScope.launch, CoroutineScope.async, runBlocking. The latter is not recommended to use in production code. There are also a couple of builders - liveData, flow - which can be used to initialize the field. For your case I would recommend to use a LiveData or Flow to observe the field initialization. The sample code, which uses the liveData builder function to call a suspend function:
val selectedProduct: LiveData<Product> = liveData {
val product = repository.fetchProduct()
emit(product)
}
And if you want to do something in UI after this field is initialized you need to observe it. In Activity or Fragment it will look something like the following:
// Create the observer which updates the UI.
val productObserver = Observer<Product> { product ->
// Update the UI, in this case, a TextView.
productNameTextView.text = product.name
}
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
viewModel.selectedProduct.observe(this, productObserver)
For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 or higher.
Since fetchProduct() is a suspend function, you have to invoke it inside a coroutine scope.
For you case I would suggest the following options:
Define selectedProduct as nullable and initialize it inside your ViewModel as null:
class AnyViewModel : ViewModel {
private val selectedProduct: Product? = null
init {
viewModelScope.launch {
selectedProduct = repository.fetchProduct()
}
}
}
Define selectedProduct as a lateinit var and do the same as above;
Personally I prefer the first cause I feel I have more control over the fact that the variable is defined or not.
You need to run the function inside a coroutine scope to get the value.
if you're in a ViewModel() class you can safely use the viewModelScope
private lateinit var selectedProduct:Product
fun initialize(){
viewModelScope.launch {
selectedProduct = repository.fetchProduct()
}
}

Android compose Navigation and ViewModel constructor calls

Currently I'm using compose with navigation and viewmodels.
The code of my NavHost is the following
composable(MyRoute.name + "/{param}") { backStackEntry ->
val param = backStackEntry.arguments?.getString("id") ?: ""
val viewModel = hiltViewModel<MyViewModel>()
MyComposable(
viewModel = viewModel
)
}
The issue I'm facing is that viewModel.init is called an infinite number of times (I guess it is recomposition), but the viewModel is supposed to have only one instance that outlives the lifecycle of the composables.
Use a LaunchedEffect to run your network call.
See this for reference.

Change view via ViewModel

I'm new to MVVM. I'm trying to figure out easiest way to change view from ViewModel. In fragment part I have navigation to next fragment
fun nextFragment(){
findNavController().navigate(R.id.action_memory_to_memoryEnd)
}
But I cannot call it from ViewModel. AFAIK it is not even possible and it destroys the conception of ViewModel.
I wanted to call fun nextFragment() when this condition in ViewModel is True
if (listOfCheckedButtonsId.size >= 18){
Memory.endGame()
}
Is there any simple way to change Views depending on values in ViewModel?
Thanks to Gorky's respond I figured out how to do that.
In Fragment I created observer
sharedViewModel.changeView.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
if (hasFinished) nextFragment()
})
I created changeView variable in ViewModel. When
var changeView = MutableLiveData<Boolean>()
change to true, observer call function.
source:
https://developer.android.com/codelabs/kotlin-android-training-live-data#6

Why Transformation.switchMap(anyLiveData) isn't fire when i change the value of "anyLiveData"

I will hope that when i call to "addPlantToGarden()" passing respect "plantId" parameter then fire the "observers" "Transformations.switchMap(plantName)" but that doesn't happen, what is the error?
private val plantName: MutableLiveData<String> = MutableLiveData()
val plant: LiveData<Plant> = Transformations.switchMap(plantName){plantId ->
plantRepository.getPlant(plantId)
}
val isPlanted: LiveData<Boolean> = Transformations.switchMap(plantName){plantId ->
gardenPlantingRepository.isPlanted(plantId)
}
fun addPlantToGarden(plantId: String) {
plantName.value = plantId
}
These are a few things to consider:
1. Check your Repository
Make sure your plantRepository.getPlant(plantId) returns LiveData. Since methods from Repository are executed in background, I prefer encapsulate the function using this:
liveData {
// some async process (e.g. HTTP Request)
emit(/*your value*/)
}
Reference: https://developer.android.com/topic/libraries/architecture/coroutines#livedata
2. Check your Observer
Are you observing on a correct view lifecycle owner? If your ViewModel is inside a Fragment, make sure to do this:
viewModel.plant.observe(viewLifecycleOwner, Observer{
// action
})
instead of:
viewModel.plant.observe(this, Observer{
// action
})
And make sure to observe first before trying to change your plantName value.
3. Start with a simple case
I have no idea how you changed your plantName value. But try from a simple hardcoded/mock value first, for example:
plantName.value = "1"
then trace it through your Repository, then down to your Observer. Hopefully this will help you.

Categories

Resources