I'm trying to get myself into RxJava so I read some posts about it. I think I understood how it works but I would like to submit you a theorical code to ensure my good understanding of the library.
Let's imagine I have an API enabling to retrieve a list of produceur and for each of them the artists they produce, their albums and their songs.
My model would be the following
public class Produceur {
private Integer id;
private String name;
private String pictureUrl;
}
public class Artist {
private Integer id;
private String name;
private String lastname;
private String sceneName;
private String pictureUrl;
}
public class Album {
private Integer id;
private int year;
private String name;
private String style;
private String pictureUrl;
private Integer artistId;
}
public class Song {
private Integer id;
private String title;
private int duration;
private String pictureUrl;
private Integer albumId;
}
With my Retrofist services
#GET("myUrl/produceurs")
Observable<List<ProduceurResponse>> getProduceurs();
#GET("myUrl/produceurs/{produceurId}")
Observable<List<ArtisteResponse>> getArtistForProduceur(#Path("produceurId") Integer produceurId);
#GET("myUrl/picture/{id}")
Observable<ResponseBody> getPicture(#Path("id") Integer id);
And the response objects
public class ProduceurResponse {
private Integer id;
private String name;
private String pictureUrl;
}
public class ArtisteResponse {
private Integer id;
private String name;
private String lastname;
private String sceneName;
private String pictureUrl;
private List<AlbumResponse> albums;
}
public class AlbumResponse {
private Integer id;
private int year;
private String name;
private String style;
private String pictureUrl;
private Integer artistId;
private List<SongResponse> songs;
}
public class SongResponse {
private Integer id;
private String title;
private int duration;
private String pictureUrl;
private Integer albumId;
}
I would like to retrieve all the produceur and for each the artists and save all the images in the local memory in the same sequence.
I thought about the following code where for each produceur we will retrieve the artists, their albums and song and insert them into our base.
And once we've finished retrieveing the produceurs and artists we can download images (we stored in a list every id with a picture).
getProduceurs().flatMap(produceurs -> Observable.from(produceurs))
.doOnNext(produceur -> insertProduceurInBase(produceur))
.subscribe(produceur -> Observable.from(getArtistForProduceur(produceur.getId())
.flatMap(artists -> Observable.from(artists))
.doOnNext(artist -> insertArtistInBase(artist)))
.subscribe(),
e -> showError(e),
() -> Observable.from(listOfIdsToDownload)
.doOnNext(id -> getPicture(id))
.subscribe(response -> createImage(response),
e -> showError(e),
() -> isFinished()
)
);
Would this code work (I think so but I'm not sure) ? Is this the best way to do it ?
Now, what if my getProduceurs service return me a list of ProduceurResponse containing produceur artists ids and I got a service to retrieve the artist profil and another one to retrieve its albums.
public class ProduceurResponse {
private Integer id;
private String name;
private String pictureUrl;
List<Integer> artistsIds;
}
public class ArtisteProfileResponse {
private Integer id;
private String name;
private String lastname;
private String sceneName;
private String pictureUrl;
}
#GET("myUrl/artist/{artistId}")
Observable<List<ArtisteProfileResponse>> getArtistProfile(#Path("artistId") Integer artistId);
#GET("myUrl/artist/{artistId}/detail")
Observable<List<AlbumResponse>> getArtistAlbums(#Path("artistId") Integer artistId);
I could to use the .zip to make the getArtistProfile() and getArtistAlbums() call simultaneous with something like
getProduceurs().flatMap(produceurs -> Observable.from(produceurs))
.doOnNext(produceur -> insertProduceurInBase(produceur))
.subscribe(produceur -> Observable.from(produceur.getArtistIds())
.zip(
getArtistProfile(),
getArtistAlbums(),
(artistProfil, albumList) -> insertArtistInBase()
)
.subscribe(),
e -> showError(e),
() -> Observable.from(listOfIdsToDownload)
.doOnNext(id -> getPicture(id))
.subscribe(response -> createImage(response),
e -> showError(e),
() -> isFinished()
)
);
But I'm really not sure I'm using the zip the right way. Is zip well used in this piece of code ? Would this work ? Is this the best way to do it ?
EDIT
So I tried to implement something similar to my original idea with the google books api.
I've got a Retrofit interface
public interface IBookService {
#GET("volumes?q=robot+subject:fiction")
Observable<BookSearchResult> getFictionAuthors(#Query("category") String key);
#GET("volumes")
Observable<BookSearchResult> getBooksForAuthor(#Query("q") String author, #Query("category") String key);
}
With
public class BookSearchResult {
public List<BookResult> items;
}
public class BookResult {
public String id;
public String selfLink;
public VolumeInfoResult volumeInfo;
public SaleInfoResult saleInfo;
}
And I try to retrieve the books of fiction with a the string robot (getFictionAuthors) with return me a BookSearchResult containing a list of BookResult. For each of the book results I retrieve all the books of the author with getBooksForAuthor. My code is below
Observable<BookResult> observable = mWebService.getFictionAuthors(API_KEY)
.flatMap(new Func1<BookSearchResult, Observable<BookResult>>() {
// Parse the result and build a CurrentWeather object.
#Override
public Observable<BookResult> call(final BookSearchResult data) {
return Observable.from(data.items);
}
})
.concatMap(new Func1<BookResult, Observable<BookSearchResult>>() {
// Parse the result and build a CurrentWeather object.
#Override
public Observable<BookSearchResult> call(final BookResult data) {
return mWebService.getBooksForAuthor("=inauthor:" + data.volumeInfo.authors.get(0), API_KEY);
}
})
.flatMapIterable(new Func1<BookSearchResult, List<BookResult>>() {
// Parse the result and build a CurrentWeather object.
#Override
public List<BookResult> call(final BookSearchResult data) {
return data.items;
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<BookResult>() {
#Override
public void onNext(final BookResult book) {
Log.e("Book","Book is " + book.volumeInfo.title + " written by " + book.volumeInfo.authors.get(0));
}
#Override
public void onCompleted() {
Log.e("Book","Book list completed");
}
#Override
public void onError(final Throwable error) {
Log.e("Book","Book list error");
}
}
This code is working but there is something weird that I don't understand. In my logs I have at first the return from the getBooksForAuthor request for the first author and then the log from each book of that author. After that I've got the result of the request for the second author and part of the log from his books. Follow the results of the request of the other authors and then the end of the list of books for the second author and the list of books for all the other authors.
To illustrate it, my logs looks like
- > Return from request for Author 1
- > Book 1 from author 1
...
- > Book 10 from author 1
- > Return from request for Author 2
- > Book 1 from author 2
...
- > Book 5 from author 2
- > Return from request for Author 3
- > Return from request for Author 4
...
- > Return from request for Author 10
- > Book 6 from author 2
..
- > Book 10 from author 2
- > Book 1 from author 3
..
- > Book 10 from author 3
...
- > Book 1 from author 10
..
- > Book 10 from author 10
When I expected
- > Return from request for Author 1
- > Book 1 from author 1
...
- > Book 10 from author 1
- > Return from request for Author 2
- > Book 1 from author 2
...
- > Book 10 from author 2
...
- > Return from request for Author 10
- > Book 1 from author 10
...
- > Book 10 from author 10
Does someone have an explanation or understand what I'm missing ?
You should avoid nested subscription (check out the flatmap operator). It's offen an indicator of a code smell.
To avoid nested subscription, you can use the operator flatMap (do not garanti resulting events order) or concatMap (garanti resulting events order).
I notice too that you're using another Observable in the completed callback : you can concat observables in this case.
So the rework of your code using this sort of operators :
getProduceurs().flatMap(produceurs -> Observable.from(produceurs))
.doOnNext(produceur -> insertProduceurInBase(produceur))
// call getArtistForProduceur and emit results in order
.concatMap(produceur -> getArtistForProduceur(produceur.getId()))
// emits items of the list
.flatMapIterable(artists -> artists)
.doOnNext(artist -> insertArtistInBase(artist)))
// don't care about elements. But will wait for the completion of the previous observable
.ignoreElements()
// perform jobs after the previous observable complete
.concatWith(Observable.from(listOfIdsToDownload)
.doOnNext(id -> getPicture(id))
.doOnNext(response -> createImage(response)))
// show an error if an error occur in the downstream
.doOnError(e -> showError(e))
// call isFinished when everything is finished.
.doOnCompleted(() -> isFinished())
.subscribe()
Related
I'm trying to retrieve a value from an Object which sits within an ArrayList
within another Object.
The basic structure is:
+++++
Book Object -> List of Author Objects -> Author Object -> Author first name variable
+++++
And I would like to access the first name of the first Author for a given Book
+++++
I have created a "Book" Class which looks like the following:
public class Book {
private String mTitle;
private List<Author> mAuthors;
public Book(String title, List<Author> authors) {
this.mTitle = title;
this.mAuthors = authors;
}
public String getmTitle() {
return mTitle;
}
public List<Author> getmAuthors() {
return mAuthors;
}
}
This class also contains a list of Author-Objects:
public class Author {
private String mFirstName;
private String mLastName;
public Author(String firstName, String lastName) {
this.mFirstName = firstName;
this.mLastName = lastName;
}
public String getmFirstName() {
return mFirstName;
}
public String getmLastName() {
return mLastName;
}
}
I then create an the list of Author instances in the MainActivity:
ArrayList<Author> authors = new ArrayList<>();
authors.add(new Author("Hans", "Schwabe"));
And use this list when creating the book instance
Book buch = new Book("Säulen der Erde",authors);
When I then try to access the name of the first Author in the list I use the following code:
List<Author> authorArrayList = new ArrayList<Author>();
authorArrayList = buch.getmAuthors();
authorArrayList.get(1).getmFirstName();
And at this point my app keeps crashing.
**
Hence: What would be the right way to retrieve the first name of the
first author from the list?
**
Issue is that you have 1 author and you are trying to retrieve it from index 1. Index should be 0. Indexing in programming is starting from 0.
If you write
authorArrayList.get(0).getmFirstName();
It should work
I have two observables:
Observable <Profile> profileObservable = briteDatabase.createQuery(UserMapper.Table.NAME, sql, String.valueOf(userId)).mapToOneOrDefault(UserMapper.MAPPER, null);
Observable <List<Car>> carObservable = briteDatabase.createQuery(CarMapper.Table.NAME, sql, String.valueOf(userId)).mapToOneOrDefault(CarMapper.MAPPER, null);
public class Profile {
int id;
String name;
ArrayList<Car> car;
...
}
public class Car {
int id;
String brand;
...
}
I need to get list of cars from carObservable and put it to profileObservable (put list of car to Profile), inside method like this:
#Override
public Observable<Profile> getProfile(String userId) {
String sql = String.format("SELECT %s FROM %s WHERE %s=?", TextUtils.join(",", UserMapper.PROJECTION), UserMapper.Table.NAME, UserMapper.Columns.ID);
Observable <Profile> profileObservable = briteDatabase.createQuery(UserMapper.Table.NAME, sql, String.valueOf(userId)).mapToOneOrDefault(UserMapper.MAPPER, null);
Observable <List<Car>> carsObservable = getCars(String userId);
/*
Some important method to concat and return Observable <Profile>. I'm only beginner at RxJava(
*/
}
Did you try the concat operator, I dont know if I understand quite well your use case, but in case you just need to go through the list of car and then profile, just concat one after the other
#Test
public void testContact() {
Observable.concat(Observable.just("Hello"),
Observable.just("reactive"),
Observable.just("world"))
.subscribe(System.out::println);
}
If you want to initiate in the RxJava world take a look here https://github.com/politrons/reactive
Realm not saving (or possibly not returning) String values of related object...
i have 3 models:
public class Customer extends RealmObject {
#Expose
#PrimaryKey
private Long id;
#Expose
private Long historicalId;
#Expose
private String versionUUID;
#Expose
private String nameCompany;
#Expose
private String email;
#Expose
private String phoneNumber;
#Expose
private String notes;
#Expose
private boolean active;
#Expose
private boolean currentVersion;
#Expose
private Date lastUpdated;
#Expose
private Date dateCreated;
public Customer() {
}
and
public class Project extends RealmObject {
#PrimaryKey
private Long id;
private Long historicalId;
private String versionUUID;
private String name;
private String description;
private String addressLineOne;
private String addressLineTwo;
private String addressCity;
private String addressState;
private String addressZip;
private String notes;
private Date lastUpdated;
private Date dateCreated;
private boolean active;
private boolean currentVersion;
private Customer customer;
private String customerVersion;
public Project() {
}
and lastly (added for the comment question)
public class Receipt extends RealmObject {
#PrimaryKey
private String id;
private String name;
private String vendor;
private Double amount;
private String description;
private Date dateCreated;
private Date lastUpdated;
private Date dateSynced;
private byte[] imageByteArray;
private Project project;
private String projectVersion;
private int imgWidht;
private int imgHeight;
public Receipt() {
}
i am saving the data via:
public static void syncAllDataToRealm(Context context){
Globals globals = Globals.getInstance();
Realm realm = Realm.getInstance(context);
realm.beginTransaction();
realm.copyToRealmOrUpdate(globals.getAllCustomers());
realm.copyToRealmOrUpdate(globals.getAllProjects());
realm.commitTransaction();
testRealCommit(context);
}
and i am verifying the data via
private static void testRealCommit(Context context){
Realm realm = Realm.getInstance(context);
RealmQuery<Customer> customerRealmQuery = realm.where(Customer.class);
RealmResults<Customer> customerRealmResults = customerRealmQuery.findAll();
logger.debug(LogUtility.generateMessage(TAG, "===== CUSTOMER ======= "));
for(Customer c: customerRealmResults){
logger.debug(LogUtility.generateMessage(TAG, c.getId() + " - " + c.getNameCompany()));
}
logger.debug(LogUtility.generateMessage(TAG, "===== CUSTOMER GLOBAL======= "));
for(Customer c: Globals.getInstance().getAllCustomers()){
logger.debug(LogUtility.generateMessage(TAG, c.getId() + " - " + c.getNameCompany()));
}
RealmQuery<Project> projectRealmQuery = realm.where(Project.class);
RealmResults<Project> projectRealmResults = projectRealmQuery.findAll();
logger.debug(LogUtility.generateMessage(TAG, "===== PROJECT ======="));
for(Project p: projectRealmResults){
logger.debug(LogUtility.generateMessage(TAG, p.getId() + " - " + p.getName()));
}
}
for some reason:
c.getNameCompany()
returns a null in the above code... if i dont add the project data to realm it works fine....
realm is bein set up in my Application file via:
RealmConfiguration config = new RealmConfiguration.Builder(context)
.name("receiptbucket.realm")
.schemaVersion(2)
.build();
Realm.setDefaultConfiguration(config);
any ideas???
found out something else... if i swap the commit order, adding all customers after adding all projects it works
realm.copyToRealmOrUpdate(globals.getAllProjects());
realm.copyToRealmOrUpdate(globals.getAllCustomers());
short term fix but i would like to know why i have to do it this way for the customer data to stick...
(New Issue)
now when i call copyOrUpdate for the Receipt it wipes all the customer data Projects Customer....
From your last description, I think the problem is the Project list returned by globals.getAllProjects() contains some Customer which has null value for nameCompany .
The reason is your Customer class has an id which is annotated with #PrimaryKey, when realm.copyToRealmOrUpdate(globals.getAllProjects()) called, Realm will create or update related objects recursively. (That is the whole point of update here).
If it finds a customer which has the same id and already saved in the Realm, it will just use all new values from Project.customer to update the one existed in the Realm. If the Procject.customer.nameCompany is null, you will have the problem you described above.
The solution would be make the globals.getAllProjects() return the latest value you want to update, since there is no way for Realm to understand whether the null values are something you want to ignore or update to.
i was able to overcome the last error by querying realm and reattaching the Customer that was getting nulled out to the Project which is part of the Receipt....
so then i started playing.... I was loading realm from Global data... I stopped that and loaded realm right when i got the data from my rest service...
upon doing that everything started just working correctly, lol...
i have a problem getting timestamp(rowversion) from my SQL Azure database.
In my tables there is a column with datatype timestamp. This timestamp isn't similar to datetime, it's more like a rowversion.
I can get all other data in this table with the query from MobileServiceTable, there is no problem.
But this special datatype is a problem.
My class for this table looks like:
public class ArbeitsgangBezeichnung {
#com.google.gson.annotations.SerializedName("id")
private int ID;
#com.google.gson.annotations.SerializedName("ABZ_ArbeitsgangBezeichnungID")
private int ABZ_ArbeitsgangBezeichnungID;
#com.google.gson.annotations.SerializedName("ABZ_Bezeichnung")
private String ABZ_Bezeichnung;
#com.google.gson.annotations.SerializedName("ABZ_RowVersion")
private StringMap<Number> ABZ_RowVersion;
//constructor, getter, setter, etc....
}
If i login in Azure and look at the table, there are my example values and the automatic generated timestamp. The timestamp value looks like "AAAAAAAAB/M=". If i login in sql database and let me show the data, then for timestamp there is only "binarydata" (in pointed brackets) and not that value as it is shown in Azure.
The variable "ABZ_RowVersion" should include this timestamp, but the data in the StringMap doesn't look like the one in Azure. I tried String and Byte as datatype for the StringMap, but it doesn't helped.
I tried byte[] for ABZ_RowVersion, but then i got an exception in the callback method.
Then i tried Object for ABZ_RowVersion, that time i found out, that it is a StringMap, but nothing more.
Does anybody know, how to get the data from timestamp, i need it for comparison.
Thanks already
When you create a timestamp column in a table, it's essentially a varbinary(8) column. In the node SQL driver, it's mapped to a Buffer type (the usual node.js type used for binary data). The object which you see ({"0":0, "1":0, ..., "length":8}) is the way that a buffer is stringified into JSON. That representation doesn't map to the default byte array representation from the Gson serializer in Android (or to the byte[] in the managed code).
To be able to use timestamp columns, the first thing you need to do is to "teach" the serializer how to understand the format of the column returned by the server. You can do that with a JsonDeserializer<byte[]> class:
public class ByteArrayFromNodeBufferGsonSerializer
implements JsonDeserializer<byte[]> {
#Override
public byte[] deserialize(JsonElement element, Type type,
JsonDeserializationContext context) throws JsonParseException {
if (element == null || element.isJsonNull()) {
return null;
} else {
JsonObject jo = element.getAsJsonObject();
int len = jo.get("length").getAsInt();
byte[] result = new byte[len];
for (int i = 0; i < len; i++) {
String key = Integer.toString(i);
result[i] = jo.get(key).getAsByte();
}
return result;
}
}
}
Now you should be able to read data. There's still another problem, though. On insert and update operations, the value of the column is sent by the client, and SQL doesn't let you set them in them. So let's take this class:
public class Test {
#SerializedName("id")
private int mId;
#SerializedName("name")
private String mName;
#SerializedName("version")
private byte[] mVersion;
public int getId() { return mId; }
public void setId(int id) { this.mId = id; }
public String getName() { return mName; }
public void setName(String name) { this.mName = name; }
public byte[] getVersion() { return mVersion; }
public void setVersion(byte[] version) { this.mVersion = version; }
}
On the insert and update operations, the first thing we need to do in the server-side script is to remove that property from the object. And there's another issue: after the insert is done, the runtime doesn't return the rowversion property (i.e., it doesn't update the item variable. So we need to perform a lookup against the DB to retrieve that column as well:
function insert(item, user, request) {
delete item.version;
request.execute({
success: function() {
tables.current.lookup(item.id, {
success: function(inserted) {
request.respond(201, inserted);
}
});
}
});
}
And the same on update:
function update(item, user, request) {
delete item.version;
request.execute({
success: function() {
tables.current.lookup(item.id, {
success: function(updated) {
request.respond(200, updated);
}
});
}
});
}
Now, this definitely is a lot of work - the support for this type of column should be better. I've created a feature request in the UserVoice page at http://mobileservices.uservoice.com/forums/182281-feature-requests/suggestions/4670504-better-support-for-timestamp-columns, so feel free to vote it up to help the team prioritize it.
I can't seem to wrap my head around how to setup my class hierarchy for JSON conversion using GSON.
My JSON looks like:
{
"Users": {
"id": 1,
"name": "Jim",
"location": "Huntsville"
}
}
My User List class looks like:
public class UserList {
public static List<User> Users;
#SuppressWarnings("static-access")
public void setUserList(List<User> userList){
this.Users = userList;
}
public List<User> getUserList(){
return Users;
}
}
and lastly a user class that looks like this:
public class User {
private int id;
private String name;
private String location;
public int getId(){
return id;
}
public String getName(){
return name;
}
public String getLocation(){
return location;
}
public String toString(){
return("User: [id=" + id + "], [name=" + name + "], [location=" + location + "]");
}
}
Anyone mind giving me a shove in the right direction? I'd appreciate it!
EDIT:
Forgot to show my parsing code.. (Just reading a sample JSON file from SDCard)
BufferedReader br = new BufferedReader(new FileReader(Environment.getExternalStorageDirectory() + "/user.json"));
UserList userList = gson.fromJson(br, UserList.class);
are you sure your example JSON is correct?
It does not seem to be a list of things, just one user is defined.
Furthermore, your getter and setters for Users, should be following the get/set pattern and be called
public List<User> getUsers()
public void setUsers(List<User> users)
Also, you can follow the Java convention of small case and instruct Gson to use a different casing.
Assuming that you only have one entry of Users in your JSON. This would let you parse the snippit you provided, if you change the Users property into User not a list.
#SerializedName("Users")
private User user;
So if you want a list of users you should find that in the json, this should let you parse it as a list, !note that you need to have objects, which are enclosed, like:
{"users" : [{id:"one"}, ...]}
As pointed out in the comments.