App hangs when LiveData is assigned null value - android

I have a LiveData named navigationArgs:
private val _navigationArgs = MutableLiveData<Item>()
val navigationArgs: LiveData<Item>; get() = _navigationArgs
which stores the arguments to be passed to the next fragment. It is attached to an observer, to navigate when the value is changed:
viewModel.navigationArgs.observe(this, Observer{
//navigation code
viewModel.finishedNavigating()
})
in which in finishedNavigating(), value of _navigationArgs is set to null:
fun finishedNavigating(){
_navigationArgs.value = null
}
When finishedNavigating() is included in the observer, the app hangs without even navigating, when the _navigationArgs value is changed.
Why does this happen? I am using Android Studio 4.0 Canary. Thank you.

When you assigning something to _navigationArgs.value, code will be automatically called in Observer, that used in viewModel.navigationArgs.observe (navigationArgs and _navigationArgs are same objects because navigationArgs has getter, that returns _navigationArgs ).
In your case you assigning null to _navigationArgs.value in finishedNavigating(), which calls code in Observer, which calls finishedNavigating() again etc...
You just have recursion here.
You should add recursion exit condition. For example:
viewModel.navigationArgs.observe(this, Observer{
//navigation code
if (it != null) //don't call finishedNavigating, when null passed in to _navigationArgs.value
viewModel.finishedNavigating()
})

mmm, it looks like an endless loop of values sent to the observer, every time you set a value null is sent and then again and again.

Related

How to change visibility of a TextView if a MutableList is empty? (Android/Kotlin)

I'm working on a simple calorie counter app using two fragments and a ViewModel. I'm a beginner and this is a modification of an app I just created for a course (this app is not a homework assignment). It uses ViewModel and has a fragment that collects user input and a fragment that displays the input as a MutableList of MutableLiveData. I would like for the list screen to initially be empty except for a TextView with instructions, and I'd like the instructions to disappear once an entry has been added to the list. My class instructor told me to use an if-else statement in the fragment with the list to achieve this, but it's not working. He didn't tell me exactly where to put it. I tried a bunch of different spots but none of them worked. I don't get errors - just no change to the visibility of the TextView.
Here is the code for the ViewModel with the list:
val entryList: MutableLiveData<MutableList<Entry>>
get() = _entryList
init {
_entry = MutableLiveData<Entry>()
_entryList.value = mutableListOf()
}
fun addEntry(entryInfo: Entry){
_entry.value = entryInfo
_entryList.value?.add(_entry.value!!)
}
}
And this is the code for the observer in the list fragment:
Observer { entryList ->
val entryListView: View = inflater.inflate(R.layout.fragment_entry_list, null, false)
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.VISIBLE
} else {
entryListView.instructions_text_view.visibility = View.GONE
}
entryList.forEach {entry ->
val view: View = inflater.inflate(R.layout.entry_list_item, null, false)
view.date_entry_text_view.text = String.format(getString(R.string.date), entry.date)
view.calories_entry_text_view.text =
view.line_divider
binding.entryList.addView(view)
}
Thanks for any help.
I guess you are expecting your observer to get notified of the event when you are adding entryInfo to your event list (_entryList.value?.add(_entry.value!!).
But this won't happen as you are just adding an element to the same mutable list, and as the list reference hasn't changed, live data won't emit any update.
To solve this, you have two options.
Create a new boolean live data which controls when to show and hide the info text. Set its initial value to false, and update it to true in addEntry() function.
Instead of updating the same mutable list, create of copy of it, add the element and set the entryList.value equal to this new list. This way your observer will be notified of the new list.
Additionally, its generally not a good practice to expose mutable data unless there is no alternative. Here you are exposing a mutable list of Entry and that too in the form of a mutable live data. Ideally, your should be exposing LiveData<List<Entry>>.
This is one possible implementation of all the points that I mentioned:
private val _entryList = MutableLiveData(listOf<Entry>()) // Create a private mutable live data holding an empty entry list, to avoid the initial null value.
val entryList: LiveData<List<Entry>> = _entryList // Expose an immutable version of _entryList
fun addEntry(entryInfo: Entry) {
_entryList.value = entryList.value!! + entryInfo
}
I haven't used the _entry live data here, but you can implement it the same way.
set your viewModel to observe on entry added.
I think you have gotten your visibility toggle in the your if else blocks wrong.
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.GONE // OR View.INVISIBLE
} else {
entryListView.instructions_text_view.visibility = View.VISIBLE
}
Your Observer should get notified of changes to entryList when _entryList has changed. Make sure you are calling addEntry() function to trigger the notification.

Changing value of a LiveData Calendar do not trigger Observer

In the fragement I display a Date from a Calendar object. This date is got from the viewModel of the fragement and it's a live date :
private val _theDate = MutableLiveData<Calendar>()
val theDate: LiveData<Calendar>
get() = _theDate
In the viewModel also I have a function that add 1 day to _theDate
fun goToDayAfter() {
_theDate.value!!.add(Calendar.DATE, 1)
}
This function is called after clicking on a button of the same fragment, and it does not trigger the observer :
viewModel.theDate.observe(viewLifecycleOwner, androidx.lifecycle.Observer { newDate ->
displayDate(newDate)
})
For more details and after debugging, I believe that _theDate is well changed but the observer is not triggered, If I change to another fragement and come back the new _theDate is changed.
Also if I change the method goToDayAfter() to :
fun goToDayAfter() {
val tmp = _theDate.value!!
tmp.add(Calendar.DATE, 1)
_theDate.value = tmp
}
It works!
Why cahnging _theDate directly do not trigger the observer ? is it because it s an object and not a premitive ? Is there any better solution that to pass by a tmp variable ?
For the update to trigger, you have to set the value of the LiveData, like you have discovered, i.e. by using:
_theDate.value = ...
Othwerwise, if you mutate (i.e. change) the value that the LiveData points to (_theDate.value!!.add(Calendar.DATE, 1), it has no way of knowing that the value was changed! That's why no update is triggered.
Prefer immutable objects
I recommend using immutable objects and re-creating them every time you want to change them. With immutable objects it's more difficult to run into this kind of bugs. An added bonus is that the code that observes _theDate cannot change it.
So instead of Calendar, I would use java.time.LocalDateTime. You can use it if you add a dependency to desugar_jdk_libs in your build.gradle file:
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
in which case your code would like like this:
fun goToDayAfter() {
_theDate.value = _theValue!!.plusDays(1)
}

Best approach to waiting on pending live data using architecture components

I'm working on an application that fetches data from a graphql server via apollo-android.
I do a single fetch on my aws rds database. I do this fetch right at the onCreate() of my CalendarFragment.
The thing is, at onViewCreated(), I want to set my textview to one of the fields that is fetched, first and last name. So, I run my getBarberFullName method which returns the String value of mBarberFullName. I'm trying to follow the UI controller displays while the view model handles all the logic approach. getBarberFullName resides within my ViewModel.
public String getBarberFullName() {
if (appointmentsAreNull()) return mBarberFullName.getValue();
AppointmentModel am = mMasterAppointments.getValue().get(0);
String fullName = am.bFirstName;
fullName = fullName.concat(" " + am.bLastName);
// Get the logged in barber's full name and set it as mBarberFullName.
mBarberFullName.setValue(fullName);
return mBarberFullName.getValue();
}
where mMasterAppointments is a MutableLiveData<List<AppointmentModel>>. In my onViewCreated() callback, I run
String barberName = mBarberViewModel.getBarberFullName();
mTxtv_barberName.setText(barberName);
However, mMasterAppointments is always null so it just returns the default value of mBarberFullName which is a String.
However, if I were to run the following code, in the same onViewCreated(), I get the desired result where the textview is updated with the desired barber's full name.
mBarberViewModel.getAllAppointments().observe(getViewLifecycleOwner(), am -> {
if (am.isEmpty()) {
Log.d(TAG, "No barber.");
return;
}
String barberGreeting;
barberGreeting = am.get(0).bFirstName;
barberGreeting = barberGreeting.concat(" " + am.get(0).bLastName);
mTxtv_barberName.setText(barberGreeting);
});
getAllAppointments returns an observer to mMasterAppointments located in my ViewModel.
Although getAllAppointments and getBarberFullName are called within onViewCreated(), one is able to access the pending values of mMasterAppointments while the other is not. Why?
I don't want to do the logic in my Fragments onViewCreated callback, so how can I wait on the pending mMasterApointmentData in my ViewModel's getBarberFullName()? Are there tools within LiveData and ViewModel that would aid me in this situation?
Use LiveData's Transformations class
when you need to perform calculations, display only a subset of the
data, or change the rendition of the data.
First add a new String LiveData for BarberFullName in the viewmdoel, and give it the value of transforming (mapping) the source LiveData mMasterAppointments into the desired String:
val fullBarberName: LiveData<String> = Transformations.map(mMasterAppointments) { am ->
" ${am[0].bFirstName} ${am.get(0).bLastName}"
}
Now you can observe this String LiveData in your fragment, the way you in did your second snippet.
Note that the code I provided is in Kotlin, I use it nowadays. I hope you get it.

Android MVVM: databinding value is not set from MediatorLiveData in particular situation

When setting a value to MediatorLiveData that reacts to a source added in the constructor of a viewModel or activity onCreate observer in the ViewModel , like this for example:
showingMethodLiveData.addSource(stateChangeLiveData) {
when (it) {
ConfigurationState.CURRENT -> showingMethodLiveData.value = commMethod[it]
ConfigurationState.PENDING -> showingMethodLiveData.value = commMethod[it]
}
}
The value isn't set to the observing view, although the set method is called.
I can work around this by either adding the source in onStart (which creates other problems of registering observer more than once), or using postValue instead of setValue.
The debug of setValue method leads me to following code, where there is an interesting comment that tells the story, the method returns without setting the value to the binded view.
in androidx.databinding package of lifecycle dependency:
class ViewDataBinding:
private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
if (mInLiveDataRegisterObserver) {
// We're in LiveData registration, which always results in a field change
// that we can ignore. The value will be read immediately after anyway, so
// there is no need to be dirty.
return;
}
boolean result = onFieldChange(mLocalFieldId, object, fieldId);
if (result) {
requestRebind();
}
}
The value is not set afterwards either, but only when the mediatorlivedata is invoked again by change in it's source.
Why this situation occurs?
Thank you for the help
PS
I think it may be an android library bug
The use of Mediatorlivedata is to compare two values and then provide a result.
If you want to change the value of a variable, you can simply use MutableLiveData and to assign a new value, write variableName.value = newValue
Should be even easier to achieve like this:
val showingMethodLiveData = Transformations.map(stateChangeLiveData) { commMethod[it] }

Kotlin "Smart cast is impossible, because the property could have been changed by this time"

Why Android Studio show error when I use No.2 script.
I found no different between 1 and 2.
class Adapter {
var nameList : ArrayList<String>? = null
}
class Program {
private fun send() {
val list: ArrayList<String> = ArrayList()
val adapter = Adapter()
// Case 1
var otherList = adapter.nameList
if (otherList != null) {
list.addAll(otherList) // <--- no error
}
// Case 2
if (adapter.nameList!=null) {
list.addAll(adapter.nameList) // <--- Error here
// Smart cast to 'kotlin.collections.ArrayList<String> /* = java.util.ArrayList<String> */' is impossible, because 'adapter.nameList' is a mutable property that could have been changed by this time
}
}
}
Please explain this case
The IDE should give you a warning, explaining that after the null check, it's possible that adapter.nameList was changed by another thread, and that when you call list.addAll(adapter.nameList), adapter.nameList could actually be null by that point (again, because a different thread could have changed the value. This would be a race condition).
You have a few solutions:
Make nameList a val, which makes its reference final. Since it's final, it's guaranteed another thread can't change it. This probably doesn't fit your use case.
class Adapter {
val nameList : ArrayList<String>? = null
}
Create a local copy of name list before you do the check. Because it's a local copy, the compiler knows that another thread can't access it, and thus it can't be changed. The local copy could be defined with either a var or a val in this case, but I recommend val.
val nameList = adapter.nameList
if (nameList != null) {
list.addAll(nameList)
}
Use one of the utility functions that Kotlin provides for just such a case as this. The let function copies the reference it's called on as a parameter using an inline function. This means that it effectively compiles down to be the same as #2, but it's a bit more terse. I prefer this solution.
adapter.nameList?.let { list.addAll(it) }
Your adapter.nameList is mutable property so please convert it to immutable.
Use this
val nameList : ArrayList<String>? = null
Instead of this
var nameList : ArrayList<String>? = null
Or you can also solve this problem by assert of non null Assert
list.addAll(adapter.nameList!!)
Note :- !! is evaluated at runtime, it's just an operator.
The expression (x!!)
throws a KotlinNullPointerException if x == null,
otherwise, it returns x cast to the corresponding non-nullable type (for example, it returns it as a String when called on a variable with type String?).
adapter.nameList is a mutable property that could have been changed`
The reason for this check and error message is threads. What you have is called a race-condition. In many similar cases it is possible for another thread to change the value of adapter.namelist between the nullity check and the list.addAll call. Clearly this can not happen in your case as the adapter is not leaked from the send function, but I guess the compiler isn't smart enough to know that.
In contrast there is no race condition in case 1 as the namelist is only accessed once.
Also this can not happen if namelist is val rather than var - since the compiler then knows it can not change - so it can not change from non-null to null.

Categories

Resources