Inside the fragment of a tabbed activity:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
serverSetVM = ViewModelProvider(activity!!).get(ServersViewModel::class.java)
serverList = ArrayList(serverSetVM.get())
rv = rv_serverList // findViewById
rv.layoutManager = LinearLayoutManager(context)
rv.adapter = ServerListRevAdapter(context!! ,serverList)
serverSetVM.serverSetLiveData.observe(viewLifecycleOwner, Observer {
Log.v ("MainAct", "Inside OBSERVER")
serverList = ArrayList(serverSetVM.get())
rv.adapter!!.notifyDataSetChanged()
})
}
Also;
val serverSetLiveData = MutableLiveData<HashSet<Server>>() // Inside ViewModel class
observe() function does not seem to work. When the value of ServerSetVM is modified inside the same fragment (by the functions defined in ViewModel class, i.e. add()), recyclerView is not updated. According to Logcat output, Observer lambda is called only after onCreateView().
I confirmed MutableLiveData gets updated but Observer{} lambda is not called. Do I need to correct my notion about ViewModels?
EDIT (SOLUTION):
Use " = " operator to modify the MutableLiveData value so that observer can detect it. Even serverSetLiveData.value=serverSetLiveData.value does the job.
Observer only observe if you call setValue() or postValue() method of MutableLiveData
where you are calling the setValue() or postValue() function for serverSetLiveData .
eg. serverSetLiveData.setValue(serverList) or serverSetLiveData.postValue(serverList). in the code.
Obervre gets triggered only if you call .value =
Related
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 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()
}
}
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)
In Android, I have a ListAdapter that I'm passing callbacks to using Kotlin's lambda notation.
For instance, the ListAdapter has:
var clickCallback = { _: Long -> Unit }
Which gets set in the Fragment calling the ListAdapter with:
mAdapter.clickCallback = {id: Long -> selectItem(id)}
This Fragment function selectItem(id: Long) then gets called inside the ListAdapter with:
clickCallback(item.id)
But what if that passed function selectItem() returned a value, such as a LiveData, and what if I wanted to call that function and observe that LiveData from inside the adapter? I cannot for the life of me figure out how to pass such a function, so that the callback function inside the ListAdapter can be observed.
This doesn't even come close to working, but hopefully it should give you an idea of the sort of solution I'm looking for:
var clickCallback:LiveData<String> = { _: Long -> Unit?? }
....
clickCallback(item.id).observe(holder.ItemView, Observer { answerString ->
doSomethingTo(answerString)
})
Bonus points if you know how to get the viewLifecycleOwner inside a ListAdapter. I'm just guessing it's the holder.ItemView.
Thanks!
John
Creating a callback which returns an object is simple. Just use next syntax for lambda type:
var callback: (([Params]) -> ReturnType)? = null
And the code is:
var clickCallback: ((Long) -> LiveData<String>)? = null
clickCallback?.invoke(item.id)?.observe(holder.ItemView, Observer { answerString ->
doSomethingTo(answerString)
})
If you want callback with default implementation it will be:
var clickCallback: (Long) -> LiveData<String> = { _: Long -> MutableLiveData() }
clickCallback(item.id).observe(holder.ItemView, Observer { answerString ->
doSomethingTo(answerString)
})
Bonus points if you know how to get the viewLifecycleOwner inside a ListAdapter. I'm just guessing it's the holder.ItemView.
It can be achieved by passing viewLifecycleOwner as a constructor parameter:
// adapter definition
class YourAdapter(private val lifecycleOwner: LifecycleOwner) ...
// passing viewLifecycleOwner as constructor parameter
val adapter = YourAdapter(viewLifecycleOwner)
Now you can access lifecycleOwner property inside YourAdapter.
In my fragment I observe dbQuestionsList field:
viewModel.dbQuestionsList.observe(viewLifecycleOwner, Observer { list ->
Log.i("a", "dbQuestionsList inside fragment = $list ")
})
In my fragment I have few buttons and depending on which one is pressed I call method on viewModel passing the string which was set as tag to the button.
viewModel.onFilterChanged(button.tag as String)
My ViewMode:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("")
}
private fun onFilterChanged(filter: String) {
dbQuestionsList = mRepository.getDbQuestionsForCategory(filter)
}
Repository method:
fun getDbQuestionsForCategory(categoryName: String): LiveData<List<DatabaseQuestion>> {
return database.dbQuestionsDao().getDbQuestionsByCategory(categoryName)
}
Dao method:
#Query("SELECT * FROM db_questions_database WHERE category = :categoryId")
fun getDbQuestionsByCategory(categoryId: String): LiveData<List<DatabaseQuestion>>
When I press button, viewModel method is called with argument which should be used to update LiveData by searching through room database, but NOTHING gets updated for no reason. Database is not empty so there is no reason to return null and not trigger observer in main Fragment.
But when I do this in my viewModel:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("travel")
}
where I hardcode parameter, the room will return list and observer in fragment will be triggered, so it works like that but doesn't work when arguments is passed when button is pressed, Please explain because this thing doesn't make sense. I tried with mutable live data, with using .setValue and .postValue but NOTHING works.
The reason you aren't getting updates is because onFilterChanged() is reassigning dbQuestionsList, not updating it. So the variable you observe initially is never actually modified.
I would probably implement this using a Transformation:
val filter = MutableLiveData<String>().apply { value = "" }
val dbQuestionsList = Transformations.switchMap(filter) {
mRepository.getDbQuestionsForCategory(filter)
}
Then in your fragment just set the filter when your button is clicked:
viewModel.filter.value = button.tag as String
Try this:
dbQuestionsList.value = mRepository.getDbQuestionsForCategory(filter)
or
dbQuestionsList.postValue(mRepository.getDbQuestionsForCategory(filter))