When using Firebase to store and retrieve objects (POJOs) created by the user (for example: posts or comments), it becomes necessary to pass these objects around the application. But what is the suggested way to keep track of the associated DatabaseReference, location or unique key in the database for this object?
Example scenario
A simple to do list app allows the user to freely add, edit and remove items in their list. So when the user creates an item, something similar to the below would happen:
private Item storeItem(String title) {
String key = mDatabase.child("items").push().getKey(); // Where do we keep this key?
Item item = new Item(title);
mDatabase.child("items").child(key).setValue(item);
return item;
}
Where Item is this Java object:
public class Item {
private String title;
private String description;
public Item() {}
public Item(String title) {
this.title = title;
}
// ...
}
Behind the scenes, this item is added to a RecyclerView, either by inserting the returned Item to the adapter or when a ChildEventListener attached to the "items" reference is fired.
The user then wishes to rename this new item or add text to the description field, so tapping on it in the RecyclerView starts a separate Activity which receives the passed Item and uses getters/setters to make changes.
Now, we'll need to save these changes to the database, which we can do by calling setValue() again, as above. However, we didn't store the key variable from storeItem() so we don't actually know where this item is currently stored in the database.
So, where can we keep track of the created item's key for later use to save changes back to the database?
Possible solutions
There are a number of different paths we could take here, but I'm looking for some guidance on the suggested method, as the Firebase documentation doesn't mention this hurdle. I've outlined some examples that I can think of:
Store the key inside the object. We could add another field to the Item object to store the database key. So within the previous storeItem() method, the key variable is added to the Item constructor and stored in the database as a field.
Create a wrapper object. We could wrap the Item object in a container that has methods such as getItem() and getKey() or getDatabaseReference() and then pass this around the app instead of the Item itself.
Use the DataSnapshot instead. Once the item is created, wait until an attached listener receives it, then use and pass around the retrieved DataSnapshot, which has methods for getKey() and getRef().
Retrieve the object every time it is needed. Instead of passing Item around the app, we could retrieve it from the database every time it is needed, by using the key or DatabaseReference.
Wrapping up
Looking back on this huge question, it seems I might have overcomplicated it a little, but I wanted to be thorough in my explanation. I'm also hoping that it's not purely opinion-based and there currently is some standard way to achieve this.
So I guess my question is: is there a standard method to handle and make changes to Java objects stored in Firebase?
Most developers I see struggling with this end up storing the key inside the Java objects too. To prevent it being duplicated in the JSON, you can annotate it in the Java class:
public class Item {
private String title;
private String description;
#Exclude
public String key;
public Item() {}
public Item(String title) {
this.title = title;
}
// ...
}
See: Is there a way to store Key in class which I cast from Firebase object?
My personal preference in such cases is to keep the DataSnapshot around. The main disadvantage I see in that is that the information on the object-type of the snapshot is spreading out over my code since this exists in multiple places:
snapshot.getValue(Item.class);
I've been lobbying to generify the DataSnapshot class so that it'd become DataSnapshot<Item>, which would solve that problem. I think that is currently being considered in the Firestore SDK for JavaScript/TypeScript.
But lacking such a solution for the Android SDK for the Realtime Database, you're probably better off with the first approach: storing the key inside the Java objects.
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 stored an array of customized class like "Patient" in a Document Field. It stored properly but now when I am trying to download it to android studio it seems like this in the log statement:-
[{"appointmentnumber"=26, "name"="Neeraj Aggarwal",
"phonenumber"="+917988081391"}, {appointmentnumber=27, name=Aaskah
Sharma, phonenumber=+917988081391}, {appointmentnumber=28, name=Mohit
sskm, phonenumber=+917988081391}]
It seems like an Array but I am not being able to apply any property of array on this. Like when I tried to get this array's first element like ArrayName[0] then it shows error like:
java.lang.ClassCastException: java.util.HashMap cannot be cast to com.example.reception.Patient
(reception my project name and Patient as you know is customized class name)
Also I don't know how to get the value of a specific element in the array stored as a fieldvalue. Like we can extract a specific value of map by documentsnapshot.get("Fieldname.Keyname"), is there any method like this to get the element on a specific position in an array.
Firebase solved my problem. Their answer is like this:-
Firebase Support
10:56 AM (29 minutes ago)
to me
Hello Neeraj,
I'm Ian, here to answer any questions you may have when it comes to using Firestore for Android.
If you use the snapshot.get() method on a field containing an array of objects, the Android SDK will return that value as a Java List of HashMap objects. There's currently no way to override this behavior, so you may need to manually get each key-value pair and set the value to a Patient object.
With that said, it is possible to convert the entirety of the document snapshot into a Java object in one go, without needing to use a method like snapshot.get(). Assuming your Doctor class looks something like this:
public class Doctor {
private List<Patient> patients;
//other variables
//Constructor must be empty
public Doctor() {}
public List<Patient> getPatients() {
return patients;
}
public void setPatients(List<Patient> patients) {
this.patients = patients;
}
You can convert your document snapshot into the Doctor object with just one line of code:
Doctor doctor = snapshot.toObject(Doctor.class);
From there, you can get your Java List of patients, and then perform your computation on the patient data as intended.
So all the trick was just to use toObject method.
My app gets it data from a REST API.
Depending on the endpoint I get list of complete or partial Account.
When the API responds I call realm.copyToRealmOrUpdate(accounts).
My problem is that if a complete account is already stored in realm and the same partial account but where relation_tx, relation_rx, message_last_rx, message_last_tx are null is in the accounts list, the complete account is overriden completely.
I only want realm to update fields which are not null.
Any suggestions?
Any way I can override the Account.update() method?
#RealmClass
public class Account extends RealmObject {
public String timestamp;
#PrimaryKey
public long id;
public String name;
public String email;
public Relation relation_tx;
public Relation relation_rx;
public Message message_last_rx;
public Message message_last_tx;
}
I think you'll have to manually compare. Don't call realm.copyToRealmOrUpdate(accounts) with the content from the server.
Instead, name your server response serverAccounts. For every account in it: get the account (localAccount) with this id from Realm. If not found (null): add the serverAccount to the DB. If found: update your four fields manually on the localAccount and save the localAccount to the DB.
To optimize the above, you might want to do a single Realm query with a list of ids (from the serverAccounts) and save your changes in one batch to Realm as well.
I ran across a problem where I am not really sure how to solve it. The project I am working on currently has a model which partly consists of backend stored data and data from the local database.
So what I am trying to Archive is something like that:
Article : [Bunch of Information] & [boolean Subscribed]
The subscribed field is device bound and should not reflect any data on the backend. My question is if it is possible to implement in Room some kind of createIfNotExit() Method that handles the following cases:
Article not present locally: store a copy and set Subscribed to
false
Article present: update all the Information and Keep the
Subscribe-Flag untouched
My idea is to split the model into a separate Subscription-Model holding a reference to the Article. This way I could implement it simply via #Update(OnConfict=Update) etc...
Is there a way to implement a simple #Query method in the DAO that performs what I want?
Sorry if this is a really basic question but I couldn't find any material about best practices handling this case.
Thank you in advance!
For example, your entity is:
#Entity(tableName = "articles")
public final class Article {
#PrimaryKey
public long serverId;
public String title;
public String url;
public boolean isSubscribed;
}
You may write this method in DAO:
#Query("INSERT OR REPLACE INTO articles (serverId, title, url, isSubscribed) VALUES (:id, :title, :url,
COALESCE((SELECT isSubscribed FROM articles WHERE id = :id), 0));")
void insertOrUpdateArticle(long id, String title, String url);
Another option - write this logic in your repository and use two simple operations: select and update
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.