I am currently working on an app that uses Firebase's real-time database and data binding for displaying. To keep it simple, here's a simple version of the problem:
Given a model class:
public class User {
private String name;
private Date date;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Date getDate() { return date; }
public void setDate(Date date) { this.date = date; }
}
And a ViewModel class for the users:
public class UserViewModel {
private User user;
public void setUser(User user) {
this.user = user;
}
public String getName() { return user.getName() }
public void setName(String name) { user.setName(name); }
public String getDateAsString() { // ... }
}
Now, in the activity/fragment I have a RecyclerView rendering a list of users. So within the adapter's onCreateViewHolder() I inflate a layout using DataBindingUtils, create a new ViewHolder and a new UserViewModel instance which accesses the UI. In onBindViewHolder() the UserViewModel gets assigned with the according User instance.
So far, so good: Given a list of users, its items get rendered into the RecyclerView through the UserViewModel.
For the app, I also use Firebase to read and write to the Realtime database. So when I now get a callback that a User entry has been updated, I directly modify the infos in the according instance.
So now to the question: How do I inform the UserViewModel that the data has changed and that it needs to redraw the according views in the UI?
I know one step I need to do is to have UserViewModel extend BaseObservable, mark the methods with #Bindable and add calls to notifyPropertyChanged(int) in the setters of the ViewModel. But this doesn't solve the problem of how to inform the UserViewModel of an update to the model data.
Any help and example code is appreciated! Thx! :)
You don't need to extend UserViewModel with BaseObservable, but you can. I'll show another way how you can achieve this.
Personally, I prefer to create a ObservableField<User> in my UserViewModel, create getters and setters like:
private final ObservableField<User> userField = new ObservableField<User>();
public UserViewModel(User user){
userField.set(user);
}
public ObservableField<User> getUser(){
return userField;
}
pass it to the layout and reference the properties like this:
<variable
name="userViewModel"
type="your.package.UserViewModel" />
<EditText
android:text"#={userViewModel.user.name}" />
Whenever your user changes his Name in your EditText, the changes are also updated in your model. (Using two-way databinding with #={})
Updated to use the ObservableField, thanks for the heads up, #tynn. Correct me, if I'm still wrong.
If I understand you correctly, you can take a look at using RxJava/RxAndroid to subscribe your viewModel to changes in the model class or the firebase instance (they should be observable). So for example, as you want to let the viewModel know that the firebase has a new user and that a user entry has been updated, you can call the viewModels onNext method from that callback, which will notify the viewModel subscribed to it, and run the method you want to run (like fetching the data), then with base observable you can then notify your list.
Related
I have following database relationship:
In words: One Order has 0-n Books, one Order is assigned to one Customer.
In my case, I have bookId. I want to launch some function when I get all associated items (Book, Order and Customer) and when I am assured all of them exists - I need to launch it only one time. I tried to solve it following way:
ViewModel:
private LiveData<Book> book;
private LiveData<Order> order;
private LiveData<Customer> customer;
public MyViewModel(Application app) {
...
book = bookRepository.getBookLiveData(id);
order = Transformations.switchMap(book, b -> orderRepository.getOrder(b.getIdOrder()));
customer = Transformations.switchMap(order, o -> customerRepository.getCustomer(o.getIdCustomer()));
}
However, this solution is uneffective and I believe this can be done some more elegant way with Room/LiveData.
I tried also another approach - creating following object BookOrderCustomer:
public class BookOrderCustomer {
#Embedded
public Book book;
#Embedded
public Order order;
#Embedded
public Customer customer;
}
But this did not work as expected,Dao's query always returned null.
Any idea how to solve this case? Thank you.
There is the MediatorLiveData, it could observe all your streams and merge data. I think this is a best approach.
I'm getting a LiveData> from a database in my view model. But I have to add some Foo-objects to the list, before I can forward them to the view.
I'm using the Room API to get access to a database. I'm using the recommended encapsulation with a Dao, a repository and the view model. The repository just forwards the LiveData from the Dao.
In the view model, I call the method from the repository and store the result in a variable. Because I can't use the observe-method of the LiveData-object, I tried it with the Transformations.map-method. But the map-method isn't called at any time.
public class FooViewModel extends AndroidViewModel {
private LiveData<List<Foo>> fromDatabase;
private MutableLiveData<List<Foo>> forView;
public FooViewModel(/*...*/) {
//...
forView = new MutableLiveData<>();
}
//Returns the LiveData<List> for the view, that should be observed
public LiveData<List<Foo>> getViewList() {
return forView;
}
//Loads the data from the database, modifies it and maps it to the LiveData for the view
public void loadFromDatabase(/*Some conditions for query*/) {
fromDatabase = repository.getData(/*Some conditions*/);
Transformations.map(fromDatabase, (foos) -> {
forView.setValue(fillList(foos));
return forView;
}
}
//Fills the list with some other foos
private static List<Foo> fillList(List<Foo> foos) {
//Fill the list
}
}
And in the view I observe the list in a way like this:
public class FooActivity {
protected void onCreate(/*Some inputs*/) {
viewModel.getViewList().observe(this, (foos) -> /*Display the list*/);
viewModel.loadFromDatabase(/*with some conditions*/);
}
}
And then nothing happens...
I tried also to forward the LiveData got from the repository and observe it. That observation works fine. But not the modified one.
//Loads the data from the database, modifies it and maps it to the LiveData for the view
public void loadFromDatabase(/*Some conditions for query*/) {
fromDatabase = repository.getData(/*Some conditions*/);
Transformations.map(fromDatabase, (foos) -> {
forView.setValue(fillList(foos));
return forView;
}
}
This will never work. fromDatabase is replaced but the transformation is done against the previous fromDatabase instance.
You need to set the query conditions into a MutableLiveData to which you do Transformations.switchMap to return the LiveData<List<T>> with the correct filters applied through the DAO.
Then if you modify the conditions live data, the DAO will re-evaluate the new list with the new conditions.
I am working on a new Android project and am using the new Architecture components but could use some help in some scenarios, as I do not understand the best solution.
I have an object such as below
public class Team {
String name;
String location;
List<User> users;
}
public class User {
String firstName;
String lastName;
}
For the ViewModel should the mutableLiveData be the Team object or each individual property, the same for the users, I will be updating and users and wonder if how I should observe those changes
This question is a follow-up question from: Organize Android Realm data in lists
Due to the data returned by the API we use, it's slightly impossible to do an actual query on the realm database. Instead I'm wrapping my ordered data in a RealmList and adding a #PrimaryKey public String id; to it.
So our realm data looks like:
public class ListPhoto extends RealmObject {
#PrimaryKey public String id;
public RealmList<Photo> list; // Photo contains String/int/boolean
}
which makes easy to write to and read from the Realm DB by simply using the API endpoint as the id.
So a typical query on it looks like:
realm.where(ListPhoto.class).equalTo("id", id).findFirstAsync();
This creates a slightly overhead of listening/subscribing to data because now I need to check listUser.isLoaded() use ListUser to addChangeListener/removeChangeListener and ListUser.list as an actual data on my adapter.
So my question is:
Is there a way I can query this realm to receive a RealmResults<Photo>. That way I could easily use this data in RealmRecyclerViewAdapter and use listeners directly on it.
Edit: to further clarify, I would like something like the following (I know this doesn't compile, it's just a pseudo-code on what I would like to achieve).
realm
.where(ListPhoto.class)
.equalTo("id", id)
.findFirstAsync() // get a results of that photo list
.where(Photo.class)
.getField("list")
.findAllAsync(); // get the field "list" into a `RealmResults<Photo>`
edit final code: considering it's not possible ATM to do it directly on queries, my final solution was to simply have an adapter that checks data and subscribe if needed. Code below:
public abstract class RealmAdapter
<T extends RealmModel,
VH extends RecyclerView.ViewHolder>
extends RealmRecyclerViewAdapter<T, VH>
implements RealmChangeListener<RealmModel> {
public RealmAdapter(Context context, OrderedRealmCollection data, RealmObject realmObject) {
super(context, data, true);
if (data == null) {
realmObject.addChangeListener(this);
}
}
#Override public void onChange(RealmModel element) {
RealmList list = null;
try {
// accessing the `getter` from the generated class
// because it can be list of Photo, User, Album, Comment, etc
// but the field name will always be `list` so the generated will always be realmGet$list
list = (RealmList) element.getClass().getMethod("realmGet$list").invoke(element);
} catch (Exception e) {
e.printStackTrace();
}
if (list != null) {
((RealmObject) element).removeChangeListener(this);
updateData(list);
}
}
}
First you query the ListPhoto, because it's async you have to register a listener for the results. Then in that listener you can query the result to get a RealmResult.
Something like this
final ListPhoto listPhoto = realm.where(ListPhoto.class).equalTo("id", id).findFirstAsync();
listPhoto.addChangeListener(new RealmChangeListener<RealmModel>() {
#Override
public void onChange(RealmModel element) {
RealmResults<Photo> photos = listPhoto.getList().where().findAll();
// do stuff with your photo results here.
// unregister the listener.
listPhoto.removeChangeListeners();
}
});
Note that you can actually query a RealmList. That's why we can call listPhoto.getList().where(). The where() just means "return all".
I cannot test it because I don't have your code. You may need to cast the element with ((ListPhoto) element).
I know you said you're not considering the option of using the synchronous API, but I still think it's worth noting that your problem would be solved like so:
RealmResults<Photo> results = realm.where(ListPhoto.class).equalTo("id", id).findFirst()
.getList().where().findAll();
EDIT: To be completely informative though, I cite the docs:
findFirstAsync
public E findFirstAsync()
Similar to findFirst() but runs asynchronously on a worker thread This method is only available from a Looper thread.
Returns: immediately an empty RealmObject.
Trying to access any field on the returned object before it is loaded
will throw an IllegalStateException.
Use RealmObject.isLoaded() to check if the object is fully loaded
or register a listener RealmObject.addChangeListener(io.realm.RealmChangeListener<E>) to be
notified when the query completes.
If no RealmObject was found after
the query completed, the returned RealmObject will have
RealmObject.isLoaded() set to true and RealmObject.isValid() set to
false.
So technically yes, you need to do the following:
private OrderedRealmCollection<Photo> photos = null;
//...
final ListPhoto listPhoto = realm.where(ListPhoto.class).equalTo("id", id).findFirstAsync();
listPhoto.addChangeListener(new RealmChangeListener<ListPhoto>() {
#Override
public void onChange(ListPhoto element) {
if(element.isValid()) {
realmRecyclerViewAdapter.updateData(element.list);
}
listPhoto.removeChangeListeners();
}
}
I've been trying to add Realm in my Android app. Their docs are pretty well explained & easy to follow. But it fails to explain this one particular area. I'm unable to figure out the practical use for the #Ignore annotation. I know that fields under this annotation are not persisted.
Can someone please share a few use cases. Also I wanted to know the scope of such fields. I mean, if I set an #Ignore field to some value, would that value be available to the other classes in my app for that particular launch session. If yes, then how do we access it? If no (which I guess is the case), then why do we need such a field anyway?
I've searched here and on web but couldn't find the relevant information. If out of my ignorance, I've missed upon some resource, please guide me to it.
Thanks.
Accordingly to the official documentation (see https://realm.io/docs/java/latest/) #Ignore is useful in two cases:
When you use GSON integration and your JSON contains more data than you want to store, but you still would like to parse it, and use right after.
You can't create custom getters and setter in classes extending RealmObject, since they are going to be overridden. But in case you want to have some custom logic anyway, ignored fields can be used as a hack to do that, because Realm doesn't override their getter & setters. Example:
package io.realm.entities;
import io.realm.RealmObject;
import io.realm.annotations.Ignore;
public class StringOnly extends RealmObject {
private String name;
#Ignore
private String kingName;
// custom setter
public void setKingName(String kingName) { setName("King " + kingName); }
// custom getter
public String getKingName() { return getName(); }
// setter and getter for 'name'
}
Ignored fields are accessible only from the object they were set in (same as with regular objects in Java).
UPDATE: As the #The-null-Pointer- pointed out in the comments the second point is out of date. Realm now allows having custom getters and setters in Realm models.
Here's a couple of real-world use cases:
1 - Get user's fullname:
public class User extends RealmObject {
private String first;
private String last;
#Ignore
private String fullName;
public String getFullName() {
return getFirst() + " " + getLast();
}
Get JSON representation of object:
public class User extends RealmObject {
private String first;
private String last;
#Ignore
private JSONObject Json;
public JSONObject getJson() {
try {
JSONObject dict = new JSONObject();
dict.put("first", getFirst());
dict.put("last", getLast());
return dict;
} catch (JSONException e) {
// log the exception
}
return null;
}
I've found it useful to define field names for when I am querying. For example
User.java
public class User extends RealmObject {
#Index
public String name;
#Ignore
public static final String NAME = "name";
}
And then later on I can do something like:
realm.where(User.class).equalTo(User.NAME, "John").findFirst();
This way if the schema changes from say name to id I don't have to hunt down every occurrence of "name".
Please see the the official documentation about #Ignore annotation:
The annotation #Ignore implies that a field should not be persisted to disk. Ignored fields are useful if your input contains more fields than your model, and you don’t wish to have many special cases for handling these unused data fields.