I'm trying to understand notification types in Realm from the Notifications section in the official docs, and when I'm using RealmObject addChangeListener in multiple managed object all of them are called when only one object is changing.
This is my code
Person first = realm.where(Person.class).equalTo("id", 0).findFirst();
first.addChangeListener(new RealmChangeListener<Person>() {
#Override
public void onChange(Person person) {
Log.e(LOG_TAG, "First element is changing: " + person);
}
});
Person second = realm.where(Person.class).equalTo("id", 1).findFirst();
second.addChangeListener(new RealmChangeListener<Person>() {
#Override
public void onChange(Person person) {
Log.e(LOG_TAG, "Second person is changing: " + person);
}
});
When I trigger an update in any of these Person objects (for example in first) both of the listeners are being called.
This what official docs say:
Listeners can also be attached to RealmObject instances as well as RealmResults instances. This allows you to react to changes to your objects and query results.
And
Lastly, typed-based change listeners will get notified when their referenced types change.
From what I understand the seen behaviour agrees with the second definition but I need to use the first behaviour, that's, I want to be notified when the object corresponding to that listener is changed.
So, if first Person is updated, only its corresponding listener get notified, not all Person listeners.
Right now it is happening because our change detection is not granular enough. It will trigger change listeners for all objects of the same type, not just those that changed.
Getting the changelisteners to only notify if the exact object has changed is being tracked here https://github.com/realm/realm-java/issues/989.
Use findFirstAsync() which returns a realmModel being empty and invalid. This is how you are able to get updates with its addChangeListener()
Specific change listener is now supported.
official release!
Demo
Related
I have a list of messages.
Each message has a unique GUID.
My setup is working for normal usage: user clicks on conversation, list opens with all the messages belonging to that conversation, ordered by most recent first.
ConversationFragment
#Override
public void onViewCreated(
#NonNull View view,
#Nullable Bundle savedInstanceState
) {
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
viewModel
.getMessageList(lifecycleOwner, conversationId) // conversationId is a global variable
.observe(lifecycleOwner, messagePagingData -> adapter.submitData(
lifecycleOwner.getLifecycle(),
messagePagingData
));
super.onViewCreated(view, savedInstanceState);
}
ConversationViewModel
final PagingConfig pagingConfig = new PagingConfig(10, 10, false, 20);
private final ConversationRepository conversationRepository;
public ConversationViewModel(#NonNull Application application) {
super(application);
conversationRepository = new ConversationRepository(application);
}
public LiveData<PagingData<ItemMessage>> getMessageList(
#NonNull LifecycleOwner lifecycleOwner,
#NonNull String conversationId
) {
return PagingLiveData.cachedIn(
PagingLiveData.getLiveData(new Pager<>(pagingConfig, () -> conversationRepository.getMessageList(conversationId))),
lifecycleOwner.getLifecycle()
);
}
ConversationRepository
private final MessageDao messageDao;
public ConversationRepository(#NonNull Context context) {
AppDatabase database = AppDatabase.getDatabase(context);
messageDao = database.messageDao();
}
public PagingSource<Integer, ItemMessage> getMessageList(#NonNull String conversationId) {
return messageDao.getMessageList(conversationId);
}
MessageDao
#Query(
"SELECT * FROM Message " +
"WHERE Message.conversationId = :conversationId " +
"ORDER BY Message.time DESC"
)
public abstract PagingSource<Integer, ItemMessage> getMessageList(String conversationId);
Now my goal is to be able to open the conversation already scrolled at a specific message.
I also do not want to load the entire conversation and then scroll to the message, some conversations can be very long and I do not want to put the user on an auto scroll that can take ages to reach the specific message.
Ideally the way I envision this being done correct is to pass the message id to be in view, load a chunk of X messages surrounding before and after that message id and then after it is already presented to the user in the RecyclerView it will load more if the user goes up or down.
This is not meant to use network requests, the entire conversation is available in the database already so it will only use the information that is already in the database.
I've tried understanding the examples that use ItemKeyedDataSource or PageKeyedDataSource, but I cannot go anywhere because every single time those examples are in Kotlin only and require Retrofit to work, which I do not use. As it is these examples are completely useless for anyone like me that is in Java and not using Retrofit.
How can this be achieved?
Please provide an answer in Java, not just Kotlin only (kotlin is OK as long as it's in java as well) and please do not suggest new libraries.
As far as I could find the official documentation does not provide any sort of clue on how to solve this one for a Paging + Room integration. In fact, it doesn't provide any solution whatsoever to scroll to an item in a PagingDataAdapter, period.
The only thing that worked for me so far was to run two queries every single time I wish to accomplish this: one to find the item position in the result query list and the other to actually load said list with the initialKey set in the Pager constructor with the value of the item position we queried previously.
And if you're feeling a bit confused, this does not end here, because even the explanation for what is initialKey and how to use it is just not documented. No, seriously: What does the initialKey parameter do in the Pager constructor
So there's two guessing games here: one to find a proper way to lookup the item index from a result list and another to set it up properly in the final query.
I hope the Paging 3 documentation gets improved soon to cover these very basic issues.
In the end this is an example of how I managed to get this problem kind of working for me, even though I have no idea if this is the proper way to do it because, again, their documentation is absolutely lacking in this department.
Create two identical queries for the list results you desire
One of those queries only returns a full list of the results based on a key you'll use to uniquely identify an item. In my case it is messageId.
Load the query in 2 and individually iterate the results list using a for... loop until you find the item you want to know its position in the list. That position is given by the iterator you use in your loop block.
Pass the item position from 3 as initialKey parameter into your Pager builder of the final query
The first chunk of data you'll receive now will contain the item you want
If you want you can now scroll to that item in your RecyclerView, but you'll have to query it from the current list of items loaded in the adapter. See about using the .snapshot() in the PagingAdapter
That's it, now I can finally load an item at a certain position using Paging 3 + Room, with absolutely no idea of whether this is the proper way to do it thanks to the completely absent documentation for this.
I'm trying to figure out the best way to set up a RealmObject with a RealmResult as one of its fields.
For example, let's say I have two RealmObjects, Goal and Achievement. The Goal object contains fields that define a query of Achievement's the user wants to track (e.g. date range the achievement was created, type of achievement, etc) and has custom methods to extract statistics from those Achievements.
What is the best way for Goal to contain this RealmResult of Achievements? Here are some ways I've thought of doing this:
Have a persisted RealmList field in Goal and update it anytime a field is changed that would change the resulting query. But how would this RealmList get updated if a new Achievement gets added to the realm?
Use #Ignore annotation on a RealmResult<Achievement> field within Goal. Anywhere in Goal where mResult is used, first check if null and requery if needed. This seems like I will be doing a lot of unneccessary querying if I'm using something like a RecyclerView that refetches the object in getItem().
Have a wrapper class that contains a Goal object and the RealmResult<Achievement> as fields. Add a listener to Goal so that anytime a relevant field changes the RealmResult can be requeried.
I'm leaning towards the last one as the cleanest way to keep a valid RealmResult. Am I missing an easier way to accomplish this?
Okay so I'm trying to implement a wrapper class (which I think is similar to the DAO abstraction #EpicPandaForce was mentioning, but I'm not super familiar with that)
public class GoalWrapper {
private RealmResults<Achievements> mResults;
private Goal mGoal;
private Realm mRealm;
public GoalWrapper(Realm realm, Goal goal) {
mRealm = realm;
mGoal = goal;
// TODO: does this need to be removed somewhere? What happens when GoalWrapper gets GC'd?
goal.addChangeListener(new RealmChangeListener<RealmModel>() {
#Override
public void onChange(RealmModel element) {
// rerun the query
findResultForGoal();
}
});
findResultForGoal();
}
/**
* Run a query for the given goal and calculate the result
*/
private void findResultForGoal() {
mResults = mRealm.where(Achievement.class)
.greaterThanOrEqualTo("date", mGoal.getStartDate())
.lessThanOrEqualTo("date", mGoal.getEndDate())
.equalTo("type", mGoal.getAchievementType())
.findAll();
calculateStats();
}
private void calculateStats() {
// Get relevant stats from mResult...
}
}
I haven't tested this code yet but I plan to have a RecyclerView.Adapter with an ArrayList of GoalWrapper objects.
My one concern is that I never remove the listener on mGoal. Do I even need to remove it? What happens in the case that the ArrayList gets GC'ed? I would think that the Goal field and resulting listeners attached to it all get GC'ed as well.
I am new to Firebase and need some help with a query to retrieve data from a table. I am currently able to access and retrieve the data that I need from firebase, however, the timing is the problem I am having an issue with.
From everything I've seen, the firebase database requires me to add event listeners to the Query or DatabaseReference objects. I am trying to download the contents of a node called "questions" before a method to display the question contents is called, however, I cannot control the timing of the firing of the event which downloads the data, and as a result my display method is always called before the firebase event fires.
How can I execute a query when I want, and be sure it will be completed before a certain section of my code executes? I am used to traditional RDBs where you execute a query and get its results and then move forward with your logic. The need to use an event handler with firebase is what I am having a hard time with. I have even tried moving the definition of the firebase reference object and the event handler into onCreate() and moved the code that calls my display method into onStart() without any success - same problem. The data I am trying to get does not change so I only need to download it once at the beginning to have available for the display method.
Here is an image of my "questions" node which is a child of the root.
image of the child "questions" node on my firebase DB
Here is my code:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get Firebase DB reference
firebase = FirebaseDatabase.getInstance();
fdbRef = firebase.getReference("questions");
// [START Question_event_listener]
fdbRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get Questions object and use the values to update the UI
objQuestions = dataSnapshot.getValue();
Log.w("Firebase:", "In Firebase ValueEventListener");
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting Questions failed, log a message
Log.w("Firebase Error:", "onCancelled:", databaseError.toException());
Toast.makeText(ReviewActivity.this, "Failed to load question!", Toast.LENGTH_SHORT).show();
}
});
//. . . remaining onCreate logic removed for simplicity
} //end of onCreate
#Override
public void onStart() {
super.onStart();
// I moved this logic from onCreate to onStart but did not help...
// Firebase retrieve must execute before I call any of these
if (list_type == MainActivity.LIST_UNREVIEWED_DOCS)
displayNewReviewForm();
else if (list_type == MainActivity.LIST_REVIEWS)
displayCompletedReview();
else // (list_type == MainActivity.LIST_DRAFTS)
displayDraftReview();
}
Other alternatives if I can't get this resolved may be to move this retrieve logic to the prior Activity in my sequence and pass the retrieved data as an extra to this activity - but that seems really silly to have to do such a thing. I would think I should be able to get data from a DB when I need it... not when it feels like giving it to me.
I appreciate any help getting me past this issue.
Your code is downloading the snapshot data containing all the data at the first go only, and with Firebase, you cannot download data timely, you can only do it through different references.
What I would suggest you to do is, to have a DatabaseReference of q01, q02 respectively and then call data as in when required.
If your Keys "q01", "q02" are static, which they are looking at the scenario. I would suggest you to have their DatabaseReferences:
question_one = firebase.getReference("q01");
question_two = firebase.getReference("q02");
question_three = firebase.getReference("q03");
//Once you have the reference, you can call their ValueListeners respectively
question_one.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get Questions object and use the values to update the UI
objQuestions = dataSnapshot.getValue();
Log.w("Firebase:", "In Firebase ValueEventListener");
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting Questions failed, log a message
Log.w("Firebase Error:", "onCancelled:", databaseError.toException());
Toast.makeText(ReviewActivity.this, "Failed to load question!", Toast.LENGTH_SHORT).show();
}
});
After looking at this a bit more, I came up with 2 possible solutions to the problem I had.
The first one I sort of mentioned already in my original question post, however it's not ideal in my opinion. It basically involves relocating the firebase retrieve logic to the prior Android Activity and passing the retrieved data to the Activity I need it in as an Extra. In my case the data is a HashMap so I would need to use the serialize versions of the methods to pass the serialized content to the desired Activity.
The best solution, is much simpler. I basically relocated the logic that I had in the onStart() function (which is calling my custom display methods) and moved it inside of the Firebase Event Listener's onDataChange() method, right after the call to dataSnapshot.getValue(). This ensures that I get the data before I call my display methods. This seems to be working well now.
Goal(s):
1: Effortless updating for dynamic items.
Example:
I have a List<T> returned from an API, I use that list in my RecyclerView.Adapter. User swipes to refresh and a new list is returned from the API containing some new items and some updated old items. Now the older list needs to remove duplicate items.
Note: assume all items have an updated attribute that might change if a user interacts with it.
2: Immediate user feedback (this might tie in with goal #1).
Example:
To insert a new item into the RecyclerView.Adapter it needs to be created in an API first. Implementation creates object in the RecyclerView.Adapter and in the API simultaneously. When the new object is returned from the API the immediate object that was previously injected right away into the RecyclerView.Adapter "syncs" with the API response. This way the user sees immediate feedback.
Code Example:
I don't really have anything in mind for Goal #1 BUT for Goal 2 I was thinking something like this maybe inside my ViewHolder? (I have heard that updating / syncing models in Viewholders is not a good practice in general because viewholders recycle):
// JAVA 7
private void createNewObjectToBeInsertedIntoRecyclerView(String data) {
// Pass callback to API and at the same time insert object into adapter
mAdapter.addNewObject(data);
mPresenter.createObject(new SyncRequestCallback() {
#Override
public void onSuccessFromAPI(ModelObject model) {
mAdapter.updateObject(model);
}
});
}
// JAVA 8
private void createNewObjectToBeInsertedIntoRecyclerView(String data) {
// Pass callback to API and at the same time insert object into adapter
mAdapter.addNewObject(data);
mPresenter.createObject((sync) -> { mAdapter.updateObject(model); });
}
This is just off the top of my head and it is definitely bug prone.
How Can I Achieve This?:
Looking for a robust solution here, but something that doesn't involve content providers (if possible).
You should not do anything like that in the ViewHolder, just bind the data you got from the API to the UI.
What you should do is operate on the Adapter
when the new List<T> returns from the API, just make the old list in the adapter to point to this new one (oldList = newList) and call mAdapter.notifyDataSetChanged()
You can do like point 1) but that way updates the whole Adapter. If you know where in the Adapter you have inserted that item (and I assume you know), just call mAdapter.notifyItemInserted(position) or alternatively, if you have already created it the Adapter, call mAdapter.notifyItemChanged(position)
I guess everybody knows the listener interface pattern. I have a class with some favorite items the user can add or remove.
In this class I have:
public static OnFavsChangedListener onFavsChangedListener;
public interface OnFavsChangedListener {
abstract void onFavsChanged();
}
public static void setOnFavsChangedListener(
OnFavsChangedListener listener) {
onFavsChangedListener = listener;
}
public void addToFavorites () {
if (onFavsChangedListener != null) {
onFavsChangedListener.onFavsChanged()
}
}
This works great when I only need to use ONE of those listeners, when I use setOnFavsChangedListener from different Activities, they will overwrite the last one.
My idea was to use an ArrayList of listeners and call them all when my Favs change, but it's hard to not let the ArrayList grow too big (Activities might add a listener on every orientation change / onCreate). I could use a HashMap of IDs and listeners, let every Activity remove it's listener onDestroy, but that seems clumsy.
TL;DR: Whats an elegant way for several Activities to be informed when my Favs change?
This may be overkill for your use case, but for notifying classes about events I use an event bus (specifically https://github.com/greenrobot/EventBus).
This allows you to simply post a message and every class that has registered to receive that type of message gets it. Very simple to use, and pretty small as far as libraries go.