I'm trying to learn about Android Architecture Components and RXJava/RXAndroid and converting a poorly written old project.
I want my DB to be the single source of truth so I have a MovieRepository class which makes the call to the API, inserts it into a DB, and then fetches the movies from the DB and sends it to the ViewModel.
My MovieRepository class -
public Completable getMovies(String sortPref) {
Timber.d("test repo getmovies");
if (!sortPref.equals("favorite")) {
Single<MoviesResponse> movieFlowable = movieService.getMovies(sortPref, BuildConfig.OPEN_TMDB_API_KEY);
return movieFlowable.subscribeOn(Schedulers.io())
.flatMapCompletable(moviesResponse -> {
for (MovieInfo movie : moviesResponse.getMovies()) {
movie.setPosterUrl("http://image.tmdb.org/t/p/w185/" + movie.getPosterPath());
movie.setSortSetting(sortPref);
movie.setVoterRating(movie.getRating() + "/10");
}
return insertMovies(moviesResponse.getMovies()).andThen(getMoviesFromDB(sortPref));
});
} else {
return getMoviesFromDB(sortPref);
}
}
public LiveData<List<MovieInfo>> getMoviesLiveData() {
return movies;
}
private Completable getMoviesFromDB(String sortPref) {
Timber.d("test get movies from db");
return Completable.fromAction(() -> movies = movieDatabase.getMovieDao().getMoviesBySortSetting(sortPref))
.subscribeOn(Schedulers.io());
}
private Completable insertMovies(List<MovieInfo> movies) {
Timber.d("test insert movies in db");
return Completable.fromAction(() -> movieDatabase.getMovieDao().insertAll(movies)).subscribeOn(Schedulers.io());
}
My MovieFragmentViewModel class has the method loadMovies() which subscribes to the cold Completable.
public LiveData<List<MovieInfo>> getMovieInfo() {
if (movieInfo == null) {
movieInfo = new MutableLiveData<>();
}
return movieInfo;
}
public void loadMovies() {
// Get the Preference settings Popular is default setting
String sortPref = Utility.getPreferredSortSetting(application);
repository.getMovies(sortPref)
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(() -> movieInfo.setValue(repository.getMoviesLiveData().getValue()))
.subscribe();
}
I'm observing my ViewModel in the Fragment like so -
viewModel = ViewModelProviders.of(getActivity()).get(MovieFragmentViewModel.class);
viewModel.loadMovies();
viewModel.getMovieInfo().observe(this, movieInfos -> {
this.movies.clear();
if (movieInfos != null) {
this.movies.addAll(movieInfos);
mAdapter.notifyDataSetChanged();
}
});
According to the docs, the viewmodel observe method should be called when the LiveData changes. But I'm not seeing any images being loaded in the GridView.
The doOnComplete method in my ViewModel is setting an empty list of movies but it should call this Action after the Completable is complete, so I should have a list of movies from the DB. I can see that movies are being saved in the DB, but not getting them in my ViewModel.
This is my github branch -
https://github.com/anklinuxboy/PopularMovies2/tree/ankit/room
Related
I'm having a trouble with firebase queries in Studio.
I'm trying to abstract a simple query to get a list of object stored in a collection (in my case "users")
I want to create a function stored in a Class that can be called by every fragment into the project.
But I don' t find any method to do that, is to repeate the same instruction the only way to do that?
Here is an example
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
I write these lines of code every time I need them, but I want to create a method that return a List as in this example:
public static List<User> getUsers(FirebaseFirestore db) {
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
} else {
//error
}
return usersList;
}
Data is loaded from Firestore (and most modern cloud APIs) asynchronously, because it may take some time. Instead of blocking the app during that time, the main code continues to execute. Then when the data is available, your addOnCompleteListener callback is executed with that data.
The easiest way to see this is by adding some well-placed logging to your code:
public static List<User> getUsers(FirebaseFirestore db) {
Log.i(LOGIN, "Starting getUsers");
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
Log.i(LOGIN, "Got data");
})
Log.i(LOGIN, "Returning from getUsers");
}
When you run this code, you get the following output:
Starting getUsers
Returning from getUsers
Got data
This is probably not the order you expected, but it completely explains why the code that calls getUsers never sees the data: by the time your return usersList runs, the data hasn't loaded yet and usersList.add(document.toObject(User.class)) has never been called.
The solution is always the same: any code that needs the data from the database, must either be inside the completion callback, be called from there, or be synchronized by some other means.
A simple example is to create a custom callback function:
public interface GetUsersCallback {
void onCallback(List<User> users);
}
You then pass that to getUsers, which can then call it once it's gotten and processed the results from the database:
public static void getUsers(FirebaseFirestore db, GetUsersCallback callback) {
// 👆
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
callback(usersList); // 👈
}
}
And you can then use it like this:
getUsers(new GetUsersCallback() {
#Override
public void onCallback(List<User> users) {
Log.i(LOGIN, "Found "+users.size()+" users");
}
});
Asynchronous loading of data is incredibly common when dealing with cloud APIs, but it's also quite confusing when you first encounter it. I recommend reading some of these answers to learn more about it:
How to check a certain data already exists in firestore or not
How to return a DocumentSnapShot as a result of a method?
Why does my function that calls an API return an empty or null value?
Firebase Firestore get data from collection
I'm working on a trading app. When the user select some products I need to show for each product if the market is open and its latest price. The user can select for example 2 products and what I have to do is to show the data only when I have all the info for the 2 products. The data can change at any time (i.e. the market for one of the products got closed). This is my code:
data class ProductInfo(
val productCode: String,
val isMarketOpen: Boolean,
val latestPrice: BigDecimal,
)
// This observable could emit at any time due to user interaction with the UI
private fun productsCodeObservable(): Observable<List<String>> = Observable.just(listOf("ProductA", "ProductB"))
// Markets have different working hours
private fun isMarketOpenObservable(productCode: String): Observable<Boolean> {
return Observable.interval(2, TimeUnit.SECONDS)
.map {
// TODO: Use API to determine if the market is open for productCode
it.toInt() % 2 == 0
}
}
// The product price fluctuates so this emits every X seconds
private fun latestPriceObservable(productCode: String): Observable<BigDecimal> {
return Observable.interval(2, TimeUnit.SECONDS)
.map { productPrice -> productPrice.toBigDecimal() }
}
#Test
fun `test`() {
val countDownLatch = CountDownLatch(1)
productsCodeObservable()
.switchMap { Observable.fromIterable(it) }
.flatMap { productCode ->
Observable.combineLatest(
isMarketOpenObservable(productCode),
latestPriceObservable(productCode)
) { isMarketOpen, latestPrice ->
ProductInfo(productCode, isMarketOpen, latestPrice)
}
}
.toList()
.doOnSuccess { productsInfo ->
println(productsInfo)
}
.subscribe()
countDownLatch.await()
}
I don't know what the problem is because the test method never prints anything. I don't know much about RxJava but my understanding is that toList is not working because the source observables never complete. Any idea about how I can collect the data for the product codes and emit a list when any of the data changes? :)
If you want to receive new product info list every time any of these products has changed:
productsCodeObservable()
.switchMap { list ->
val productInfoObservables = list.map { productCode ->
Observable.combineLatest(
isMarketOpenObservable(productCode),
latestPriceObservable(productCode)
) { isMarketOpen, latestPrice ->
ProductInfo(productCode, isMarketOpen, latestPrice)
}
}
Observable.combineLatest(productInfoObservables) { it.toList() as List<ProductInfo> }
}
.doOnNext { productsInfoList ->
println(productsInfoList)
}
.subscribe()
Use the RxJava’s Emitter interface and implement its methods:
public interface Emitter<T> {
void onNext(T value);
void onError(Throwable error);
void onComplete();
}
I think you need an ObservableEmitter, Please take a look at the following page:
https://www.raywenderlich.com/2071847-reactive-programming-with-rxandroid-in-kotlin-an-introduction
I am new to doing asynchronous programming in Android Java. I am wondering if there is a way to run another Callback after an initial Callback function has completed. Right now, I think they are running in parallel even though the second relies on the first.
First Callback:
// GETTING USER
private interface FirestoreUserCallback {
void onCallback (User myUser);
}
private void getUser(final FirestoreUserCallback firestoreCallback) {
Task<DocumentSnapshot> task = fStore.collection("users").document(fAuth.getCurrentUser().getUid()).get();
task.addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
user = documentSnapshot.toObject(User.class);
firestoreCallback.onCallback(user);
Log.d(TAG, "user created");
}
});
task.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.d(TAG, "user creation failed");
}
});
}
Second Callback:
// GETTING ALL DOCUMENTS
private interface FirestoreDocumentCallback {
void onCallback (List<TableEntries> myEntries);
}
private void getDocuments (final FirestoreDocumentCallback firestoreDocumentCallback) {
fStore.collection("result")
.document(Integer.toString(user.getCompanyNumber())) // need to use User object returned from the first Callback
.collection("SAM").get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
List<TableEntries> results = new ArrayList<>();
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
// add objects to results ArrayList ...
Log.d(TAG, document.getId() + " => " + document.getData());
}
firestoreDocumentCallback.onCallback(results);
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
}
onCreate:
getUser(new FirestoreUserCallback () {
#Override
public void onCallback(User myUser) {
user = myUser;
}
});
getDocuments(new FirestoreDocumentCallback() {
#Override
public void onCallback(List<TableEntries> myEntries) {
entries = myEntries;
}
});
getDocuments() relies on the user variable being given its value from the first Callback. I'm receiving this error:
java.lang.NullPointerException: Attempt to invoke virtual method 'double java.lang.Double.doubleValue()' on a null object reference
Callbacks are looking fine. You just need to check if your value is null or not before accessing it. Just add a null check
if(doubleValue!=null)
Using RxJava. First, we fetch the user and then fetch the documents. Rx-Java has an operator flatmap. flatmap is used to execute the sequential tasks, where the second task is dependent on the data from the first task.
final CompositeDisposable disposable = new CompositeDisposable();
//function to fetch user data
Single<User> getUser(){
return API.getUserData(...);
}
//function to fetch ducuments
Sinlge<UserDetail> getDocuments(int userId){
return API.getUserDetail(userId, ...);
}
//Subscribe
disposable.add(getUser()
.flatmap(user-> return getDocuments(...))
.subscribeOn(Scheduler.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableSingleObservable(){
#Override
public void onSuccess(UserDetail userDetail){
Log.v("Api result", "Successful";
//Do some work
}
#Override
public void onError(Throwable e)
Log.v("Api result", "Error Returned");
}
}));
If either of the API call fails, onError() is called. If first API fails, second API call is not executed and onError() is called.
The simplest solution for your use-case is to pass both queries to Tasks.whenAllSuccess() method, as explained in my answer from the following post:
Firestore - Merging two queries locally
So once the task is complete, you can use the elements from both queries. Another solution might be to use Android Jetpack with LiveData along with ViewModel, as the Android team recommends.
In Google NetworkBoundResource class I am not able to understand the sequence in which addSource() and removeSource() of MediatorLiveData is used.
For Example-- In this constructor I am not able to understand
a) why result.removeSource(dbSource); is used just after result.addSource(dbSource, data);. I know addSource() is used for start listning to LiveData and removeSource() is used for stop listining to LiveData. But why we started listening to LiveData if we immediate stop it.
protected NetworkBoundResource() {
result.setValue(Resource.loading(null));
// Always load the data from DB intially so that we have
LiveData<T> dbSource = loadFromDb();
// Fetch the data from the network and add it to the resource
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch()) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource, newData -> {
if(null != newData)
result.setValue(Resource.success(newData)) ;
});
}
});
}
or can't we use the above constructor like this
protected NetworkBoundResource() {
result.setValue(Resource.loading(null));
// Always load the data from DB intially so that we have
LiveData<T> dbSource = loadFromDb();
// Fetch the data from network and add it to the resource
result.addSource(dbSource, data -> {
if (shouldFetch()) {
fetchFromNetwork(dbSource);
result.removeSource(dbSource);
} else {
if(null != data)
result.setValue(Resource.success(data)) ;
}
});
}
Yup, Yes can use this as well.
// Fetch the data from network and add it to the resource
result.addSource(dbSource, data -> {
if (shouldFetch()) {
fetchFromNetwork(dbSource);
result.removeSource(dbSource);
} else {
if(null != data)
result.setValue(Resource.success(data)) ;
}
});
I am messing around some with the google awareness api and now my understanding of RxJava is limiting me.
What I want to achieve in the end:
I want to get a Weather and a Location from the Api, and merge them into one object that I can pass on to my view for update.
However, I'm not sure how I achieve the returning of an Observable from the api callback here since it has void return type, and how to achieve merging of the weather and location object from api.getWeather and api.getLocation
public void requestUserCurrentInfo() {
Subscription userInfo = getWeatherLocation().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(userinfo ->
Log.d(TAG,userinfo.something()));
}
public Observable<UserInfo> getWeatherLocation () {
try {
Awareness.SnapshotApi.getWeather(client)
.setResultCallback(weather -> {
if (!weather.getStatus().isSuccess()) {
Log.d(TAG, "Could not get weather");
return;
}
//How do I do here?
return weather.getWeather();
});
Awareness.SnapshotApi.getLocation(mGoogleApiClient)
.setResultCallback(retrievedLocation -> {
if(!retrievedLocation.getStatus().isSuccess()) return;
Log.d("FRAG", retrievedLocation.getLocation().getLatitude() + "");
});
} catch (SecurityException exception) {
throw new SecurityException("No permission " + exception);
}
}
For my other things in my Project, I get some stuff through a REST api following the repository pattern, then I can get it like this because every step returns a Observable< SmhiResponse >
getWeatherSubscription = getWeatherUsecase.execute().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
smhiResponseModel -> {Log.d(TAG,"Retrieved weather"); locationView.hideLoading();},
err -> {Log.d(TAG,"Error fetching weather"); locationView.hideLoading();}
);
You don't return an observable from the callback but wrap your callbacks into observables to make them combinable (untested):
Observable<WeatherResult> weatherObservable = Observable.create(subscriber -> {
Awareness.SnapshotApi.getWeather(client)
.setResultCallback(weather -> {
if (!weather.getStatus().isSuccess()) {
subscriber.onError(new Exception("Could not get weather."));
Log.d(TAG, "Could not get weather");
} else {
//How do I do here?
subscriber.onNext(weather);
subscriber.onCompleted();
}
});
});
Observable<LocationResult> locationObservable = Observable.create(subscriber -> {
Awareness.SnapshotApi.getLocation(mGoogleApiClient)
.setResultCallback(retrievedLocation -> {
if(!retrievedLocation.getStatus().isSuccess()) {
subscriber.onError(new Exception("Could not get location."));
} else {
Log.d("FRAG", retrievedLocation.getLocation().getLatitude() + "");
subscriber.onNext(retrievedLocation);
subscriber.onCompleted();
}
});
});
now combine them via .combineLatest() or .zip():
Observable<CombinedResult> combinedResults = Observable.zip(weatherObservable, locationObservable,
(weather, location) -> {
/* somehow combine weather and location then return as type "CombinedResult" */
});
don't forget to subscribe, otherwise none of them gets executed:
combinedResults.subscribe(combinedResult -> {/*do something with that stuff...*/});
Observable.combineLatest(getWeather (), getLocation(), new Func2<List<Object_A>, List<Object_B>, Object>() {
#Override
public Object call(Object o, Object o2) {
combine both results and return the combine result to observer
}
})
getweather() and getlocation() return observables