I have a project using Realm.io for storing entities, this used to work fine, but now I have a fragment or activity containing 3 fragments with Lists of Realm objects.
Whenever I Switched to a page and back to the first one (or whatever just returning to a page). I get the java.lang.IllegalStateException: Illegal State: Row/Object is no longer valid to operate on. Was it deleted?
This seems to occur because the objects are no longer valid. Is there a simple way to detach them or something? Allthough it would be nice if they remain managed since I sometimes want to be able to delete them.
The items are queried from database, when there are not sufficient items they will get loaded from the API. Nothing extremely fancy is being used here, three lists with adapters which load the entities. THe difference per list is a string value status, which says if it's an certain status.
I get the error when I load the item from the Adapter after clicking the list item to show the details:
MyEntity myEntity = (MyEntity) adapter.getItem(position);
intent.putExtra("id", myEntity.getId()) <-- this part will crash it.
with exception:
java.lang.IllegalStateException: Illegal State: Row/Object is no longer valid to operate on. Was it deleted?
I guess it's because it's querying the same type of data on three locations (3 tabs). Though I would expect this not to be a problem since they all have their own adapter and list of items. Fetched from their own instances.
This is the code being called by my "Loader" class which handles the from DB and/or Api fetching.
public void loadResult(List result, boolean isFinished) {
//not the best for speed, but at a max of 10 items this is fine to not get duplicates and respect the original order
try {
for (RealmObject ro : result) {
Record r = (Record) ro;
int itemIndex = items.indexOf(r);
if (itemIndex > -1) {
items.set(itemIndex, r);
} else {
items.add(r);
}
}
} catch (IllegalStateException e) {
ErrorClass.log(e);
}
notifyDataSetChanged();
setLoading(!isFinished);
end = result.size() < 10 && isFinished;
}
in short the loader class does this, and it's not a singleton, it's a new instance per Listview (Recycler)
List result = null;
if (sortKey != null) {
result = query.findAllSorted(sortKey, ascending);
} else {
result = query.findAll();
}
if (result.size() < PAGE_SIZE && retry == 0) {
isFinished = false;
retry++;
getPageFromApi(pageNumber);
} else if (retry > 0) {
retry = 0;
}
adapter.loadResult(result, isFinished);
The getPageFromApi will result on this code being called again, and existing entities will be replaced in the list, new items added. So no old removed items should exist in the list when clicking them.
I think this might be very specific but there must be a global reason/solution to my problem.
Stupid me, I wrapped the adding of the new elements in a try catch because of the error before, what was going wrong is pretty simple. In the Loader the items fetched from our API was updating or creating new items. Meaning that those in the list, will be invalid at that point, or at least the pointers to them? Not sure how it works behind the scenes.
What I did to fix it, was loop through all the current items, and check the isValid(), if false the item would be removed. Otherwise I was checking for a new item to be inside the current items List, which would cause the error to occur in the .equals function!
This one thing is something that might be a core error, but I think it's just my error!
Related
I used the lifecycle callback onCreate to fetch data like below
mWeOutViewModel.getPlaceListLiveData()
.observe(this, weOutItemViewModels -> {
AppLogger.i(getCustomTag() + "adding items " + weOutItemViewModels.size());
if (weOutItemViewModels != null && weOutItemViewModels.size() > 0)
mWeOutListAdapter.addToExisting(weOutItemViewModels);
});
As you can see the AppLogger output the initial size which is 0 when the fragment is displayed, then I fetch the data and call postValue (setValue crashes the app and it expected because I fetch data from the internet using a background thread). So I call post value like below :
private void updatePlaces(List<WeOutGroupedViewModels> weOutGroupedViewModels) {
List<WeOutGroupedViewModels> oldList = placeMutableLiveData.getValue();
oldList.addAll(weOutGroupedViewModels);
AppLogger.i(TAG +" updating places "+oldList.size());
placeMutableLiveData.postValue(oldList);
}
As you can see the other AppLogger before postValue, the size of the list is displayed(not empty), but nothing happens until the app crashes and nothing is shown in the logs. I have no ways of debugging since even on debug mode nothing happens. The post value doesn't trigger the observer.
I initialize the mutableLivedata like this :
private final MutableLiveData<List<WeOutGroupedViewModels>> placeMutableLiveData = new MutableLiveData<>();
and access like this :
public LiveData<List<WeOutGroupedViewModels>> getPlaceListLiveData() {
return placeMutableLiveData;
}
Event when I make the livedata public to access directly the livedata, there is no change (just in case someone thinks that's is where the issue comes from)
Instead of placeMutableLiveData.postValue(oldList);
I recommend using
placeMutableLiveData.postValue(Collections.unmodifiableList(new ArrayList<>(newList));
That way, the next time you access this list, you won't be able to mutate it in place, which is a good thing. You're not supposed to mutate the list inside a reactive state holder (MutableLiveData).
So theoretically it should look like this:
private void updatePlaces(List<WeOutGroupedViewModels> weOutGroupedViewModels) {
List<WeOutGroupedViewModels> newList = new ArrayList<>(placeMutableLiveData.getValue());
newList.addAll(weOutGroupedViewModels);
AppLogger.i(TAG +" updating places "+newList.size());
placeMutableLiveData.postValue(Collections.unmodifiableList(newList));
}
#Override
public void onDataChange(#NonNull DataSnapshot snapshot) {
if (snapshot.getValue() == null) {
Log.println(Log.ERROR, TAG, "onDataChange: WAS NULL!!!!");
} else {
//Do my thing.
}
}
Avoiding a null from Database is no big deal, but the fact that the Database is returning a null in the first place is extremely problematic if concurrent transactions are performed(?).
And I believe the issue is in my transaction, I've tried reading some answers in here but they redirect to Firebase documentation that no longer exists(?), and the documentation available is for the Firestore API: https://firebase.google.com/docs/#section-transactions
I believe my issue is the placing of the setValue()'s as all of them are placed inside the null check.
#NonNull
#Override
public Transaction.Result doTransaction(#NonNull MutableData mutableData) {
MutableData childA = mutableData.child(Keys.A);
MutableData childB = mutableData.child(Keys.B);
MutableData childC = mutableData.child(Keys.C);
assert outerKey != null;
MutableData bAReference = childB.child(Keys.Ba).child(dayKey).child(outerKey);
MutableData bBReference = childB.child(Keys.Bb).child(outerKey);
MutableData bCReference = childB.child(Keys.Bc).child(dayKey);
ObjectA anObject = childA.getValue(ObjectA.class);
if (anObject != null) { /**I think the issue is that I am setting values ONLY IF reading of anObject != null*/
// Another reason may be that the null check is performed too late when it should at an earlier stage.
anObject.setA(anObject.getA() + outerDelta); //Change some inner state values.
anObject.setB(newBaObject.getB()); //Change some inner state values.
childA.setValue(anObject); /**Set value inside != null check(??)*/
/**^^^^This (childA) is the one returning null for a second*/
ObjectBc bCObject = Builder.buildSomething(dayKey, bCReference.getValue(ObjectBc.class), outerNewValues); // Builds a new Object with something old and something new
if (!something) {
bAReference.setValue(newBaObject);
bBReference.setValue(newBbObject);
} else {
bAReference.setValue(null); //I dont know if these are returning null, but it doesn't
bBReference.setValue(null); // matter since the one above is returning null anyways
}
if (somethingElse || something) {
creator.accept(childC::child); /**Sequential side effect iterator function for child creation (Don't judge me please! it's supposed to be faster :))*/
}
bCReference.setValue(bCObject); /**Again: sets value inside != null check*/
}
return Transaction.success(mutableData);
}
Another important information is that the transaction is entering the same branch, BUT this branch is forked into 3 main sub-branches, so it is a little bit too divided.
Frank Van Puffellen said in a previous answer (Firebase runTransaction not working - MutableData is null):
If the actual stored value is the same as the assumed current value,
the Firebase server writes the new value you specified.
(A Compare And Swap)
So I think that because I did not specified a new Value AND the Database confirmed equality between "assumed" AND "actual" WAY before my null checker actually got the time to do something meaninful (because its placed too late), new value is set to null for a brief second, Then user sets the correct new value with the help of the cached state on device.
The problem with this hypothesis is that the second (correct) write should not perform since assumed and actual should always match since actual is now null and the code beneath the null check should never get executed, since it will never be not null because no code is placed outside the null check.
And whats worst, lines like anObject.setA(anObject.getA() + outerDelta); which relay on the use of the previous state are writing the correct answer anyways... which means assumed is never null at that point.
The other reason may be that the null is giving for an entire different reason AFTER the transaction has successfully finished, meaning the transaction is performing well...
Of course, it could always be that there is something wrong with the ValueEventListener written as a reactive component (which I have).
firebaser here
Getting null as the current value in your transaction handler initially is the expected behavior. The transaction handler is called with the current guess of the client for the value, which often will be null. If the initial guess is incorrect, your transaction handler will be called again with an updated guess for the current value. See a.o. https://stackoverflow.com/a/57134276/209103
The solution is to return a value if you get null, even if you know that value will never actually be written to the database:
if (anObject != null) {
...
}
else {
mutableData.setValue("This is needed to get the correct flow"); // 👈
}
If you don't want Firebase to fire local events for the transaction values right away, but only once the transaction is confirmed, you can pass a boolean second argument to runTransaction to indicate this:
https://firebase.google.com/docs/reference/android/com/google/firebase/database/DatabaseReference#runTransaction(com.google.firebase.database.Transaction.Handler,%20boolean)
I have the following function:
override fun insertUpdatedItems(items: List<AutomobileEntity>) {
if (!items.isEmpty()) {
items.forEachIndexed { index, automobileEntity ->
if (automobileEntity.id == items[index].id) {
automobileCollection[index] = items[index]
notifyItemInserted(index)
}
}
}
}
I'm using to provide data for a recyclerview, I'm trying to insert updated/edited items that are already in automobileCollection which size always returns 10 items but the items list might differ it can be 1 to 10.
It's supposed to compare items by id but what I'm getting currently with this function is the edited items are just inserted to recyclerview's adapter and not treated as an already existing item.
On the contrary, if I iterate using automobileCollection I get IndexOutOfBoundsException since most of the time the items list is smaller than automobileCollection.
To update a list with items from another one, you can use several ways.
First starting with a direct replacement (which preserves the order, but that's just a detail):
val sourceList = TODO()
val targetList = TODO()
targetList.replaceAll { targetItem ->
sourceList.firstOrNull { targetItem.id == it.id }
?: targetItem
}
Alternatively removing all the items and adding them again:
targetList.removeIf { targetItem ->
sourceList.any { it.id == targetItem.id }
}
targetList.addAll(sourceList)
Using listIterator (note! that's actually also happening under the hood when you call replaceAll... not in the same way, but similar ;-)):
val iterator = targetList.listIterator()
while (iterator.hasNext()) {
iterator.next().apply {
sourceList.firstOrNull { id == it.id }?.also(iterator::set)
}
}
Probably not so readable... For your forEachIndexed I do not really see any use-case. For other problems there definitely are, but I would suggest you try to omit indices (and also forEach) as often as you can. If nothing better comes to your mind, then forEach is also ok, but many times, forEach (and even more so forEachIndexed) isn't the best approach to solve an issue.
final FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter(Service.class, R.layout.browse_service_detail, ServiceHolder.class, mReference){
#Override
protected void populateViewHolder(ServiceHolder serviceHolder, Service service, int position) {
serviceHolder.setServiceName(service.getName());
serviceHolder.setInfo("От " + service.getPrice1());
service.setQuantitySelected(service.getQuantityEnabled());
if (Order.getInstance().getServices() != null) {
for (Service serviceFromSingleton : Order.getInstance().getServices()) {
if (serviceFromSingleton.getName() == serviceHolder.getServiceName().getText().toString()) {
serviceHolder.getServiceName().setSelected(true);
serviceHolder.getServiceName().setTextColor(getResources().getColor(R.color.yellow));
}
}
}
//add item to array
servicesList.add(service);
}
}
};
When I run this activity, it records the visible list objects to an array, but when I scroll down and go back up, it duplicates the first elements again into the array. How to fix it? For an item to be added only once.
I don't think there is any issue in RecyclerAdapter..I think the list only inserting same data multiple times.
why not you check whether the list is empty or not before adding data into it and clear the data if its not empty and then add new.
if(servicesList.isEmpty())
servicesList.add(service);
//else clear and add data
else{
servicesList.clear();
servicesList.add(service);
}
To handle data duplicacy, you can use a Set which will ignore duplicate inserts on scrolling.
servicesList.add(service);
Set<Service> mSet= new HashSet<Service>();
mSet.addAll(servicesList);
servicesList.clear();
servicesList.addAll(mSet);
OR use Set other than ArrayList
little clumsy but will work for you.
This is probably very odd, but I'm using multiple CursorLoaders in Android to do multiple queries and in the onLoadFinished(), I am adding views like TextViews and ListViews to my layout dynamically based on cursor results like if the cursors were not null. I do get accurate results, but since I'm using AsyncTaskLoader (CursorLoader), the cursor results don't come in at the same time and the results are not added in the correct order. I previously used a static layout and added views at indices and did view.setVisiblity(View.GONE) based on the results, but it was just too much and too confusing because I have like 32 views. Plus it seemed weird because I don't think the user wants to see all of those views going away and moving up and down based on AsyncTaskLoader results.
How can I get the views in the correct order I want them in without having a bunch of boolean variables? I looked into LayoutInflater but that requires indices as well, but I'm not sure that will help me. The problem with indices for me is that in cursorloader ID 1:
view.addView(v, 1);
view.addView(v, 2);
might not get executed until the cursorloader with ID 2 finishes with:
view.addView(v, 3);
view.addView(v, 4);
If cursorLoader ID 1 doesn't get executed and ID 2 does, then there is missing space and I have to do a ton of view.setVisibility(View.GONE) if I use static XML views and do not dynamically add them.
In the code I'm doing something like this currently:
#Override
public void onLoadFinished(android.support.v4.content.Loader<Cursor> cursorLoader, Cursor cursor) {
switch (cursorLoader.getId())
{
case 0:
if (cursor != null && cursor.moveToFirst()) {
..
title = new TextView(this);
...
mainLinearLayout.addView(title, 1);
}
break;
case 1:
if (cursor != null && cursor.moveToFirst()) {
..
title2 = new TextView(this);
mainLinearLayout.addView(title2, 2);
break;
default:
...
}
}
I also read somewhere online that it is better to use a service instead of cursorloader if you want to do queries on the background thread and have them finish in a certain order, but I have not heard that advice anywhere else or seen any examples doing queries in services. They all use CursorLoader. Is this advice necessarily true? Sounds a bit sketchy.
By the way, I am using the CursorLoader implementation without a content provider given at CursorLoader usage without ContentProvider
How can I get the views in the correct order I want them in without
having a bunch of boolean variables?
You do need some sort of status control in order to make the views appear in order. I would delegate the view construction/addition to a control class that will have all the information required to make the correct view and in the right order no matter how the loaders finished their jobs.
public class ViewDispatcher {
public SparseArray<Status> mLoadStatuses = new SparseArray<Status>();
public SparseArray<Cursor> mDataCursors = new SparseArray<Cursor>();
// you'll call this method for each of the loaders, in the order they should be. The ids should be incremental
public void registerLoader(int loaderId) {
mLoadStatuses.put(loaderId, Status.INITIAL);
}
// called when one of the loaders finishes its job
public void onLoadComplete(int loaderId, Cursor data) {
mDataCursors.put(loaderId, data);
boolean current = true;
mLoadStatuses.put(loaderId, Status.LOADED);
if (loaderId == firstLoaderId) {
// the first loader it's done and we should start the view creation right away
buildView(loaderId, mainLayout, true);
mLoadStatuses.put(loaderId, data, Status.FULLY_BUILT);
} else {
// implement a priority system, a view construction will be triggered ONLY
// if the previous loader has finished loading data and its view is in place
// I'm assuming that the Loaders have consecutive ids
if (mLoadStatuses.get(loaderId - 1) != null && mLoadStatuses.get(loaderId - 1) == Status.FULLY_BUILT) {
buildView(loaderId, data, mainLayout, true);
mLoadStatuses.put(loaderId, Status.FULLY_BUILT);
} else {
current = false;
}
}
// we'll also need to implement a buddy system. When a loader is done loading and its view
// is created we must check to see if we don't have other loaders after this current one that have finished loading
// but couldn't construct their view because this current loader didn't finished at that moment
// get the next loader
int next = loaderId + 1;
while(current && next < totalNumberOfLoaders && mLoadStatuses.get(next) == Status.LOADED) {
// continue to build views
buildView(next, mDataCursors.get(loaderId), mainLayout, true);
mLoadStatuses.put(next, Status.FULLY_BUILT);
next++;
}
}
// this will build the appropriate view, and optionally attach it
public void buildView(int loaderId, Cursor data, view mainLayout, boolean attach) {
// build the view for this specific Loader
}
}
public enum Status {
INITIAL, LOADED, FULLY_BUILT
}
I hope I'm not missing something obvious as I wrote that without any tests. To use it, you'll first call the registerLoader() method for all loaders in the order you need them to be and in the onLoadComplete() callback of the LoaderCallbacks call ViewDispatcher.onLoadComplete().
I also read somewhere online that it is better to use a service
instead of cursorloader if you want to do queries on the background
thread and have them finish in a certain order, but I have not heard
that advice anywhere else or seen any examples doing queries in
services.
You've probably read about IntentService which can be made to follow a queue through the order of the Intents it receives. But, I don't see how this would help you as it would just add problems. For one you use Cursors as the data holders that you would need to pass back and you need to create views which the IntentService can't do(it will need to make the Activity create them through various communication ways, this is unnecessary work from my point of view).