How to handle invalidated RealmList in RecyclerView - android-recyclerview

My Android app uses a RecyclerView to display a managed RealmList contained in a RealmObject. If the RealmObject is deleted I get the following error:
java.lang.IllegalStateException: Access to invalidated List object
The error occurs in the this section of the adapter (which extends RecyclerView.Adapter):
#Override
public int getItemCount() {
if (mEventList != null) {
return mEventList.size(); //Error occurs here
} else {
return 0;
}
}
EventListAdapter(RealmList<TodoItem> todoItems, int colorGroup, OnStartDragListener dragStartListener) {
this.mEventList = todoItems;
this.colorGroup = colorGroup;
this.mDragStartListener = dragStartListener;
}
It occurs in conjunction with the following code to delete the RealmObject containing the RealmList. The error occurs before the Activity is closed and info is returned to its parent Activity
case R.id.navigation_delete:
deleteGroup();
sendIntentInfoBack(Constants.RESULT_CHANGED, createIntentBundle());
return true;
private void deleteGroup() {
mRealm.executeTransaction(realm -> {
RealmList<TodoItem> eventsToDelete = mCurrentTodoList.getTodos();
eventsToDelete.deleteAllFromRealm();
RealmResults<TodoList> noteToDelete = mRealm.where(TodoList.class).equalTo(TodoList.PK, mCurrentTodoList.getPK()).findAll();
noteToDelete.deleteAllFromRealm();
});
}
The user must be able to delete the RealmObject containing the RealmList.
My Questions:
How do I handle the RealmList being invalidated? I've tried assigning the RecyclerView data source to be null, then doing notifyDataSetChanged() on the adapter before deleting the RealmObject, but get the same error.
Do I disable the RecyclerView? If so, how?
Do I create an empty or throw-away RealmList, then assign a new adapter to the RecyclerView with this as the data source, then delete the RealmObject?
Do I close the Activity containing the RecyclerView and just handle deleting the RealmObject in the parent Activity?
Thanks for your consideration and time.

Related

Databinding is initialized in improper order by automatically generated executeBindings

Preface
I have a simple app with a viewmodel, a custom UI control, and a TextView. Databinding is setup like this:
LiveData -> control.value -> control.preValue -> TextView
When LiveData is changed, databinding notifies control.value of the new value. control.value setter has a line which also gives the new value to control.preValue. Both properties have calls to their respective databinding listeners to notify databinding that the values have changed and that the UI should be updated. The text value of TextView is bound to control.preValue, so when the listener is notified, the TextView is updated.
This works well at runtime, however there is a problem at initialization.
The Problem
When the UI is first constructed, the LiveData value is not correctly propagated to the TextView. This is because the listeners have not yet been created by the android databinding library, so when control.preValue is set by control.value's setter, the listener is still null.
Diving deeper into executeBindings we can see the cause of the problem.
executeBindings is a function which is part of the *BindingImpl file automatically generated by the databinding library based on the Binding Adapters I have defined. It is responsible for initializing databinding, e.g. creating listeners, registering livedatas, and setting initial values to the UI.
executeBindings starts like this. It initializes variables for all the databound values.
#Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.Integer viewmodelBpmGetValue = null;
androidx.lifecycle.MutableLiveData<java.lang.Integer> viewmodelBpm = null;
int bpmPickerPreValue = 0;
androidx.lifecycle.MutableLiveData<java.lang.Boolean> viewmodelPlaying = null;
java.lang.String integerToStringBpmPickerPreValue = null;
int androidxDatabindingViewDataBindingSafeUnboxViewmodelBpmGetValue = 0;
com.okos.metronome.MetViewModel viewmodel = mViewmodel;
Next, it gets the value of control.preValue property and stores it in the earlier created variable. This is already the core of the problem. At this point control.preValue is still at the default value that is defined in the control's definition class, not the LiveData value which will be assigned to it a bit later.
if ((dirtyFlags & 0x18L) != 0) {
// read bpmPicker.preValue
bpmPickerPreValue = bpmPicker.getPreValue();
// read Integer.toString(bpmPicker.preValue)
integerToStringBpmPickerPreValue = java.lang.Integer.toString(bpmPickerPreValue);
}
Next we get the LiveData value from the viewmodel and register it with databinding
if ((dirtyFlags & 0x15L) != 0) {
if (viewmodel != null) {
// read viewmodel.bpm
viewmodelBpm = viewmodel.getBpm();
}
updateLiveDataRegistration(0, viewmodelBpm);
if (viewmodelBpm != null) {
// read viewmodel.bpm.getValue()
viewmodelBpmGetValue = viewmodelBpm.getValue();
}
// read androidx.databinding.ViewDataBinding.safeUnbox(viewmodel.bpm.getValue())
androidxDatabindingViewDataBindingSafeUnboxViewmodelBpmGetValue = androidx.databinding.ViewDataBinding.safeUnbox(viewmodelBpmGetValue);
}
Here it sets control.value to the value of the LiveData in the first if block. This line will trigger the control.value setter, which will set control.preValue, and those setters will both try to call their respective onChange listeners but they will be null because executeBindings hasn't created them yet. They are created in the 2nd if block.
if ((dirtyFlags & 0x15L) != 0) {
// api target 1
this.bpmPicker.setValue(androidxDatabindingViewDataBindingSafeUnboxViewmodelBpmGetValue);
}
if ((dirtyFlags & 0x10L) != 0) {
// api target 1
com.okos.metronome.view.DialPickerBindingAdapter.setPreValueListener(this.bpmPicker, (com.okos.metronome.view.PrePickerBase.OnValueChangeListener)null, bpmPickerpreValueAttrChanged);
com.okos.metronome.view.DialPickerBindingAdapter.setValueListener(this.bpmPicker, (com.okos.metronome.view.PrePickerBase.OnValueChangeListener)null, bpmPickervalueAttrChanged);
}
Finally, the value of the TextView is set, but it is set to the original value of preValue which we cached in a variable in the very first if block. **Not the new value which has been updated to preValue from the LiveData since then.
if ((dirtyFlags & 0x18L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvBpmDisplay, integerToStringBpmPickerPreValue);
}
This seems like an oversight in the databinding library, and I wonder if anyone has any ideas of getting around this? The fix seems pretty simple to just move the first if block in executeBindings down so integerToStringBpmPickerPreValue is set after the value has been set from LiveData, but because executeBindings is automatically generated, I can't do that. There are some ways of changing the order of execution in executeBindings, like which order the bindings are defined in the xaml, but none of that affects the parts I want to change.

How to correctly update value of IntegerProperty in the view?

I am building simple application that will track a certain tabletop game of 2 players.
I have a view called MatchView
class MatchView : View() {
// data
private var currentRound = SimpleIntegerProperty(0)
private var currentTurn = SimpleIntegerProperty(0)
override val root = borderpane {
center = label(currentRound.stringBinding{ "Round %d".format(it) })
// other layout-related stuff
subscribe<EndTurnEvent> {
endPlayerTurn()
}
}
private fun endPlayerTurn() {
// increment turn
currentTurn.plus(1)
currentRound.plus(currentTurn.value % 2)
}
}
that is subscribed to EndTurnEvent - event emitted by one of the fragments used by view.
The called method is supposed to increment value of currentTurn and if needed currentRound (round increments after second player ends their turn)
However neither the value of currentRound nor the one of currentTurn are getting increased when i call .plus() method.
I have tried editting values differently :
private fun endPlayerTurn() {
// increment turn
currentTurn.value = currentTurn.value + 1
currentRound.value = currentTurn.value % 2
}
But this throws me java.lang.IllegalStateException: Not on FX application thread
I am aware that putting properties into views is anti-pattern, but since I just want to keep track of 2 properties, I thought I could put them directly into View
Platform.runLater(() -> {
// Update GUI from another Thread here
});

Will the same value trigger the event in LiveDate?

The Code A and Image A is from the artical LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case).
The author told me "Trigger the event by setting a new Event as a new value", I think it should be "Trigger the event by setting a new Event as any value", right?
For example,
Step 1: The user clicks the button in master Activity with the code userClicksOnButton("StartDetails") , the Details Activity will start.
Step 2: The user presses back, coming back to the master activity
Step 3: The user clicks the button in master Activity with the code userClicksOnButton("StartDetails") again, the Details Activity will start again.
Is it right?
Code A
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
Image A
You're having two different constraints on the data.
The LiveData emits every update to active observers, but the event itself is stateful. Thus a new event is required as a new LiveData value. The itemId could be any value, but the comment doesn't refer to it.
Trigger the event by setting a new Event as [a new] value

WebDriverEventListener take screenshot onException()

guys. Today I have done my custom realization for WebDriverEventListener. I need only onException() method which will create screenshot. But I got problem because I am using fluent wait.
new FluentWait<>(webDriver)
.withTimeout(Duration.ofSeconds(10))
.pollingEvery(Duration.ofMillis(500))
.ignoring(NoSuchElementException.class)
.until(someCondition)
So, finally, I have got screen for each ignoring(NoSuchElementException.class) - 20 screenshots for 1 fail ))). Had somebody the such problem or had someone resolve it?
when you use .ignoring(NoSuchElementException.class) you don't avoid that the exception is raised, you are just ignoring that exception. What is happening is that the exception is being raised by your FluentWait, but it is ignored (when you declare .ignoring(NoSuchElementException.class)).
You have three options here:
Capture the screen at the end of your test if the test failed [preferred].
Have a Try-Catch wherever you are using your FluentWait or any other Selenium code.
Use reflection to avoid capture when the event is raised from the method that implements the FluentWait.
This is an idea after what we have discussed:
private void ExceptionThrown(object sender, WebDriverExceptionEventArgs e)
{
if (e.ThrownException is NoSuchElementException)
{
// Get the stack trace from the current exception
StackTrace stackTrace = new StackTrace(e.ThrownException, true);
// Get the method stack frame index.
int stackTraceIndex = stackTrace.FrameCount - 1;
// Get the method name that caused the exception
string methodName = stackTrace.GetFrame(stackTraceIndex).GetMethod().Name;
if(methodName != "MyFindElement")
{
TakeSceenshot();
}
}
else
{
TakeSceenshot();
}
}
// This is an extension method of the DriverHelper interface
public IWebElement MyFindElement(this IWebDriver driver, By by, int timeOut = 0)
{
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeOut));
wait.IgnoreExceptionTypes(typeof(NoSuchElementException));
// I wait until the element exist
IWebElement result = wait.Until(drv => drv.FindElement(by) != null);
// it means that the element doesn't exist, so we throw the exception
if(result == null)
{
MyPersonalException(by);
}
}
// The parameter will help up to generate more accurate log
public void MyPersonalException(By by)
{
throw new NoSuchElementException(by.ToString());
}
This probably require changes in EventFiringWebDriver, because this class is without WebDriverWait instance and events for them. If you want avoid it, create bool variable in your EventFiringWebDriver extended class and check this value in your OnException like:
protected void OnException(WebDriverExceptionEventArgs e) {
if (IsWaitHandler)
return;
Your actions...
}
but this is not perfect solution.

Unsubscribe from IObservableElementEnumerable.EnumerableChanged doesn't work?

Parts of our UI uses IObservableElementEnumerable.EnumerableChanged in order to update if the user e.g. deletes a domain object from a folder.
When the UI is disposed, we unsubscribe from the event... or so we thought. It turns out that the unsubscribe doesn't have any effect, and our event handler is still called. This caused a number of odd bugs, but also leads to memory leaks.
The only time unsubscription works, is if we store the IObservableElementEnumerable reference instead of calling IObservableElementEnumerableFactory.GetEnumerable(obj) again. But this, in turn, is likely to keep a live reference to the folder object, which will break if the folder itself is deleted by the user.
This is particularly puzzling as the GetEnumerable() documentation clearly states: "It is expected that subsequent calls with the same domain object will yield the same instance of IObservableElementEnumerable." Is this not to be interpreted as a guarantee?
Should there be any reason for unsubscription not working?
The following code replicates the issue on Petrel 2011 (add to a simple plugin with a menu extension, or get the full solution here (DropBox)):
using System;
using System.Linq;
using System.Windows.Forms;
using Slb.Ocean.Core;
using Slb.Ocean.Petrel;
using Slb.Ocean.Petrel.Basics;
using Slb.Ocean.Petrel.UI;
namespace ObservableElementEnumerable
{
public class OEEForm : Form
{
private Droid _droid;
private bool _disposed;
public OEEForm()
{
IInput input = PetrelProject.Inputs;
IIdentifiable selected = input.GetSelected<object>().FirstOrDefault() as IIdentifiable;
if (selected == null)
{
PetrelLogger.InfoOutputWindow("Select a folder first");
return;
}
_droid = selected.Droid;
GetEnumerable().EnumerableChanged += enumerable_EnumerableChanged;
PetrelLogger.InfoOutputWindow("Enumerable subscribed");
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing && !_disposed)
{
GetEnumerable().EnumerableChanged -= enumerable_EnumerableChanged;
PetrelLogger.InfoOutputWindow("Enumerable unsubscribed (?)");
_droid = null;
_disposed = true;
}
}
IObservableElementEnumerable GetEnumerable()
{
if (_disposed)
throw new ObjectDisposedException("OEEForm");
object obj = DataManager.Resolve(_droid);
IObservableElementEnumerableFactory factory = CoreSystem.GetService<IObservableElementEnumerableFactory>(obj);
IObservableElementEnumerable enumerable = factory.GetEnumerable(obj);
return enumerable;
}
void enumerable_EnumerableChanged(object sender, ElementEnumerableChangeEventArgs e)
{
PetrelLogger.InfoOutputWindow("Enumerable changed");
if (_disposed)
PetrelLogger.InfoOutputWindow("... but I am disposed and unsubscribed!");
}
}
public static class Menu1
{
public static void OEEBegin1_ToolClick(object sender, System.EventArgs e)
{
OEEForm f = new OEEForm();
f.Show();
}
}
}
To replicate:
Run Petrel with the plugin
Load a project with a folder with objects
Select the folder
Activate the plugin menu item
With the popup open, delete an object in the folder
Close the Form popping up
Delete an object in the folder
The message log should clearly show that the event handler is still called after the form is disposed.
You already keep a reference to the underlying enumerable by connecting the event. Events are references as well. Just keep a reference to the enumerable and unsubscribe from the same instance as the one you subscribe to.
To deal with the issue of objects that are deleted by the user you need to listen to the delete event.