I am using Realm + Retrofit2
I am trying to implement following :
UI asks DataManger for data.
DataManger returns cached data, and checks if data has expired then calls for fresh data.
When fresh data is saved in Realm NetworkManager triggers event which is captured by UI for updating data.
Issue
When NetworkHelper saves the data in Realm, after commitTransaction() due to onChangeListeners of RealmObjects, the code in DataManger onCall() part is executed again, which again calls NetworkHelper for new data, which subsequently again saves data from the network and process goes into infinite loop. I tried gitHubUser.removeChangeListeners() at multiple points but it still not working. Please point out anything fundamentally being wrong or the correct way to implement with Realm.
Implemented codes are as follows:
DataManager
public Observable<GitHubUser> getGitHubUser(final String user){
return databaseHelper.getGitHubUser(user).doOnNext(new Action1<GitHubUser>() {
#Override
public void call(GitHubUser gitHubUser) {
if(gitHubUser==null || !isDataUpToDate(CACHE_TIME_OUT,gitHubUser.getTimestamp())){
if(gitHubUser!=null)
System.out.println("isDataUpToDate = " + isDataUpToDate(CACHE_TIME_OUT,gitHubUser.getTimestamp()));
networkHelper.getGitHubUserRxBus(user);
}
}
});
}
DataBaseHelper
public Observable<GitHubUser> saveGitHubUser(GitHubUser user, String userId) {
realmInstance.beginTransaction();
user.setUserId(userId);
user.setTimestamp(System.currentTimeMillis());
GitHubUser userSaved = realmInstance.copyToRealm(user);
Observable<GitHubUser> userSavedObservable = userSaved.asObservable();
realmInstance.commitTransaction();
return userSavedObservable;
}
public Observable<GitHubUser> getGitHubUser(String user){
System.out.println("DatabaseHelper.getGitHubUser");
GitHubUser result = realmInstance.where(GitHubUser.class).contains("userId",user, Case.INSENSITIVE).findFirst();
if(result != null){
return result.asObservable();
}else{
return Observable.just(null);
}
}
NetworkHelper
public void getGitHubUserRxBus(final String user){
System.out.println("NetworkHelper.getGitHubUserRxBus");
retroFitService.user(user)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<GitHubUser, Observable<GitHubUser>>() {
#Override
public Observable<GitHubUser> call(GitHubUser gitHubUser) {
System.out.println("NetworkHelper.call");
return databaseHelper.saveGitHubUser(gitHubUser,user);
}
}).subscribe(new Action1<GitHubUser>() {
#Override
public void call(GitHubUser gitHubUser) {
if (rxBus.hasObservers()) {
System.out.println("NetworkHelper.call");
rxBus.send(gitHubUser);
}
}
});
}
Activity
subscription.add(dataManager.getGitHubUser("gitHubUserName")
.subscribe(new Subscriber<GitHubUser>() {
#Override
public void onCompleted() {
System.out.println("LoginActivity.call" + " OnComplete");
}
#Override
public void onError(Throwable e) {
System.out.println("throwable = [" + e.toString() + "]");
}
#Override
public void onNext(GitHubUser gitHubUser) {
System.out.println("LoginActivity.call" + " OnNext");
if (gitHubUser != null) {
sampleResponseText.setText(gitHubUser.getName() + " timestamp " + gitHubUser.getTimestamp());
}
onCompleted();
}
}));
subscription.add(rxBus.toObserverable().subscribe(new Action1<Object>() {
#Override
public void call(Object o) {
if(o instanceof GitHubUser){
GitHubUser gitHubUser = ((GitHubUser)o);
sampleResponseText.setText(gitHubUser.getName() + " time " + gitHubUser.getTimestamp());
}
}
}));
UPDATE
Finally Solved it by following in DataManger:
return Observable.concat(databaseHelper.getGitHubUser(user).take(1),
networkHelper.getGitHubUser(user))
.takeUntil(new Func1<GitHubUser, Boolean>() {
#Override
public Boolean call(GitHubUser gitHubUser) {
boolean result = gitHubUser!=null && isDataUpToDate(CACHE_TIME_OUT,gitHubUser.getTimestamp());
System.out.println("isDataUpToDate = " + result);
return result;
}
});
I think you have a loop going in your code:
1) You create an observable from a RealmResults in getGithubUser().Realm observables will emit every time you change data that might effect them.
2) You call networkHelper.getGitHubUserRxBus(user) after retrieving the user from Realm.
3) When getting a user from the network, you save it to Realm, which will trigger the Observable created in 1) to emit again, which creates your cycle.
To break it, you can do something like result.asObservable().first() in getGitHubUser() as that will only emit once and then complete, but it depends on your use case if that is acceptable.
Related
In my todoApp I've implemented MediatorLiveData for learning purpose in the following manner:
private val todoListMediator = MediatorLiveData<NetworkResult<List<TodoEntity>>>()
private var todoSource: LiveData<NetworkResult<List<TodoEntity>>> =
MutableLiveData()
val todoResponse: LiveData<NetworkResult<List<TodoEntity>>> get() = todoListMediator
viewModelScope.launch(dispatcherProvider.main) {
todoSource =
todoRepository.getTodoList()
todoListMediator.addSource(todoSource) {
todoListMediator.value = it
}
}
The above code works fine. Now I wanna make following changes and I don't have clear picture how can I achieve them.
As soon as todoListMediator.addSource() observes the todoList:
1] I want to iterate over that original Todo list and make a network call for each element and add some more field to them.
2] I wanna save the new todo list(where each Todo now has some extra field we received by network call in step 1)
3] and finally, I want to assign that new todo list(with extra field) to the todoListMediator.
// sudo to illustrate the above scenario
viewModelScope.launch(dispatcherProvider.main) {
//step.1 get original todo list
todoListMediator.addSource(todoSource) { it ->
// step 2. iterate over original todo list from step 1 and make network call to get extra field for each element and update the original list
//for example
val newFieldForTodoElement = NetworkResult<PendingTodoValues?> = todoRepository.getPendingTodoValues()
// step 3. one all the original list is updated with new field values, save the Todo list in local db
// step 4. Then pass the todo list with new fields to mediator live data from db
todoListMediator.value = it
}
}
Any tricks with detailed explanation on code will be a great help for my learning. Thanks!
you can use RXjava with Flatmap operator
something like that may help ?
getPostsObservable()
.subscribeOn(Schedulers.io())
.flatMap(new Function<Post, ObservableSource<Post>>() {
#Override
public ObservableSource<Post> apply(Post post) throws Exception {
return getCommentsObservable(post);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Post>() {
#Override
public void onSubscribe(Disposable d) {
disposables.add(d);
}
#Override
public void onNext(Post post) {
updatePost(post);
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError: ", e);
}
#Override
public void onComplete() {
}
});
}
private Observable<Post> getPostsObservable(){
return ServiceGenerator.getRequestApi()
.getPosts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Function<List<Post>, ObservableSource<Post>>() {
#Override
public ObservableSource<Post> apply(final List<Post> posts) throws Exception {
adapter.setPosts(posts);
return Observable.fromIterable(posts)
.subscribeOn(Schedulers.io());
}
});
}
private void updatePost(final Post p){
Observable
.fromIterable(adapter.getPosts())
.filter(new Predicate<Post>() {
#Override
public boolean test(Post post) throws Exception {
return post.getId() == p.getId();
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Post>() {
#Override
public void onSubscribe(Disposable d) {
disposables.add(d);
}
#Override
public void onNext(Post post) {
Log.d(TAG, "onNext: updating post: " + post.getId() + ", thread: " + Thread.currentThread().getName());
adapter.updatePost(post);
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError: ", e);
}
#Override
public void onComplete() {
}
});
}
private Observable<Post> getCommentsObservable(final Post post){
return ServiceGenerator.getRequestApi()
.getComments(post.getId())
.map(new Function<List<Comment>, Post>() {
#Override
public Post apply(List<Comment> comments) throws Exception {
int delay = ((new Random()).nextInt(5) + 1) * 1000; // sleep thread for x ms
Thread.sleep(delay);
Log.d(TAG, "apply: sleeping thread " + Thread.currentThread().getName() + " for " + String.valueOf(delay)+ "ms");
post.setComments(comments);
return post;
}
})
.subscribeOn(Schedulers.io());
I'm new in RxJava. I have currently executed three API calls parallel which is independent of each other via Retrofit using Single.Zip Operator. On getting a successful response of all three API calls, I have to insert the data from all three APIs into Room database into Different entities which takes 20 seconds.
So I need to execute database operations inside Single.Zip operator. Because the logic is written inside onSuccess method running away before Database Operation performed.
I have tried to take separate Observer for performing database operation but didn't work.
public void callOfflineDataAPIs() {
setIsLoading(true);
Single<BaseResponse<ProductResponse>> single1 = getDataManager().getOfflineProductListApiCall(getDataManager().getLastTimeStampOfflineProductCall()).subscribeOn(getSchedulerProvider().io()).observeOn(getSchedulerProvider().ui());
Single<BaseResponse<LocationResponse>> single2 = getDataManager().getOfflineLocationListApiCall(getDataManager().getLastTimeStampOfflineLocationCall()).subscribeOn(getSchedulerProvider().io()).observeOn(getSchedulerProvider().ui());
Single<BaseResponse<OfflineMasterData>> single3 = getDataManager().getOfflineMasterDataListApiCall(getDataManager().getLastTimeStampOfflineMasterCall()).subscribeOn(getSchedulerProvider().io()).observeOn(getSchedulerProvider().ui());
DisposableSingleObserver<List<Boolean>> result = Single.zip(single3, single1, single2,
(offlineMasterDataBaseResponse, productResponseBaseResponse, locationResponseBaseResponse) -> {
List<Boolean> apiCalls = new ArrayList<>();
apiCalls.add(masterDataCRUDOperation(offlineMasterDataBaseResponse));
apiCalls.add(productDataCRUDOperation(productResponseBaseResponse));
apiCalls.add(locationDataCRUDOperation(locationResponseBaseResponse));
return apiCalls;
}).subscribeOn(getSchedulerProvider().io()).observeOn(getSchedulerProvider().ui()).subscribeWith(new DisposableSingleObserver<List<Boolean>>() {
#Override
public void onSuccess(List<Boolean> apiCalls) {
setIsLoading(false);
LogHelper.e(TAG, "DisposableSingleObserver- onSuccess");
boolean isSync = true;
for (int i = 0; i < apiCalls.size(); i++) {
if (!apiCalls.get(i)) {
isSync = false;
LogHelper.e(TAG, "DisposableSingleObserver- onSuccess- apiCalls.get(i)", i);
callOfflineDataAPIs();
break;
}
}
if (isSync) {
LogHelper.e(TAG, "IF-isSync");
if (BuildConfig.IS_CLIENT_BUILD) {
LogHelper.e(TAG, "IF-isSync-IS_CLIENT_BUILD-true");
getDataManager().setCurrentWarehouseKey(1);
getNavigator().onGoButtonClick();
} else {
LogHelper.e(TAG, "ELSE-isSync-IS_CLIENT_BUILD-false");
getWarehouseList();
}
}
}
#Override
public void onError(Throwable e) {
LogHelper.e(TAG, "DisposableSingleObserver- Throwable");
setIsLoading(false);
String errorMessage = new NetworkError(e).getAppErrorMessage();
getNavigator().exitApplicationOnError(errorMessage);
}
});
}
Logic written inside onSuccess Method execute once all DB Operation performed.
You can modify your code to something like:
DisposableSingleObserver<List<Boolean>> result = Single.zip(single3, single1, single2,
(offlineMasterDataBaseResponse, productResponseBaseResponse, locationResponseBaseResponse) -> {
List<Boolean> apiCalls = new ArrayList<>();
apiCalls.add(masterDataCRUDOperation(offlineMasterDataBaseResponse));
apiCalls.add(productDataCRUDOperation(productResponseBaseResponse));
apiCalls.add(locationDataCRUDOperation(locationResponseBaseResponse));
return apiCalls;
}).subscribeOn(getSchedulerProvider().io())
.map(new Function<List<Boolean> apiCalls, List<Boolean> apiCalls>() {
#Override
public List<Boolean> apiCalls apply(List<Boolean> apiCalls) throws Exception {
// perform database operations here
return apiCalls;
}
})
.observeOn(getSchedulerProvider().ui())
.subscribe(new Observer<List<Boolean>>() {
#Override
public void onNext(User user) {
// Do something
}
#Override
public void onError(Throwable e) {
// Do something
}
#Override
public void onComplete() {
// Do something
}
});
I have successfully implemented a PagedList.BoundaryCallback, which loads a list of upcoming movies from "themoviedb" database, and saves the response into the database.
But it does not work the way I want it. Since the request return a list of upcoming movies, the response changes frequently. But if I already have data in my database, the onZeroItemsLoaded() method is not called.
My question is, how can I force the data source, or this boundary callback to always make an api request, and refresh the content of my database from the network?
public class UpcomingMoviesBoundaryCallback extends PagedList.BoundaryCallback<MovieListItemEntity> {
public static final String TAG = UpcomingMoviesBoundaryCallback.class.getSimpleName();
private UpcomingMoviesRepository upcomingMoviesRepository;
private int page = 1;
public UpcomingMoviesBoundaryCallback(UpcomingMoviesRepository upcomingMoviesRepository) {
this.upcomingMoviesRepository = upcomingMoviesRepository;
}
#Override
public void onZeroItemsLoaded() {
super.onZeroItemsLoaded();
Log.d(TAG, "onZeroItemsLoaded: ");
load();
}
#Override
public void onItemAtEndLoaded(#NonNull MovieListItemEntity itemAtEnd) {
super.onItemAtEndLoaded(itemAtEnd);
Log.d(TAG, "onItemAtEndLoaded: ");
load();
}
#SuppressLint("CheckResult")
private void load() {
upcomingMoviesRepository.getUpcoming(page)
.doOnSuccess(result -> {
upcomingMoviesRepository.saveUpcomingMovies(result);
page = result.getPage() + 1;
})
.subscribeOn(Schedulers.io())
.subscribe(result -> {
Log.d(TAG, "load: " + result);
}, error -> {
Log.d(TAG, "load: error", error);
});
}
}
public class UpcomingMoviesRepositoryImpl implements UpcomingMoviesRepository {
private static final String TAG = UpcomingMoviesRepository.class.getSimpleName();
private MovieResponseMapper movieResponseMapper = new MovieResponseMapper();
private MovieAppApi mMovieAppApi;
private UpcomingDao mUpcomingDao;
public UpcomingMoviesRepositoryImpl(MovieAppApi mMovieAppApi, UpcomingDao mUpcomingDao) {
this.mMovieAppApi = mMovieAppApi;
this.mUpcomingDao = mUpcomingDao;
}
#Override
public Single<MovieListResponse> getUpcoming(int page) {
return mMovieAppApi.upcoming(page);
}
#Override
public Single<MovieListResponse> getUpcoming() {
return mMovieAppApi.upcoming();
}
#Override
public void saveUpcomingMovies(MovieListResponse movieListResponse) {
Executors.newSingleThreadExecutor().execute(() -> {
long[] inseted = mUpcomingDao.save(movieResponseMapper.map2(movieListResponse.getResults()));
Log.d(TAG, "saveUpcomingMovies: " + inseted.length);
});
}
#Override
public LiveData<PagedList<MovieListItemEntity>> getUpcomingLiveData() {
PagedList.Config config = new PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(12)
.build();
DataSource.Factory<Integer, MovieListItemEntity> dataSource = mUpcomingDao.upcoming();
LivePagedListBuilder builder =
new LivePagedListBuilder(dataSource, config)
.setBoundaryCallback(new UpcomingMoviesBoundaryCallback(this));
return builder.build();
}
}
Inside the repository you can query database to check if data is old then you can start an async network call that will write the result directly to the database. Because the database is being observed, the UI bound to the LiveData<PagedList> will update automatically to account for the new dataset.
#Override
public LiveData<PagedList<MovieListItemEntity>> getUpcomingLiveData() {
if(mUpcomingDao.isDatasetValid()) //Check last update time or creation date and invalidate data if needed
upcomingMoviesRepository.getUpcoming()
.doOnSuccess(result -> {
upcomingMoviesRepository.clearUpcomingMovies()
upcomingMoviesRepository.saveUpcomingMovies(result);
})
.subscribeOn(Schedulers.io())
.subscribe(result -> {
Log.d(TAG, "load: " + result);
}, error -> {
Log.d(TAG, "load: error", error);
});
}
I try to replicate database trigger function with Realm with Rx. Once I get RealmList emitted, I do some stuff with it and save. Sadly, this results into Realm's change listener to be executed again, emitting the list over and over again.
Dummy example:
realm.where(MyRealmObject.class)
.equalTo("state", "new")
.findAll()
.asObservable()
.flatMap(new Func1<RealmResults<MyRealmObject>, Observable<MyRealmObject>>() {
#Override
public Observable<MyRealmObject> call(RealmResults<MyRealmObject> list) {
return Observable.from(list);
}
})
.subscribe(new Action1<MyRealmObject>() {
#Override
public void call(final MyRealmObject object) {
realm.executeTransaction(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
// do any realm change
}
});
}
});
Once I commit the transaction in subscriber, new RealmList is emited from observable. I know why this happens, I just don't see any way how to workaround this.
This takes us to my question. Is there any way how to replicate trigger functionality with realm where I will do any realm change?
Workaround can be built with helper stream determing whether next item from db should be consumed. Every data store into db should be accompanied with write into helper stream. Running test below yields:
upstream: IgnoreAction{action='start', ignoreNext=false}
result: 1
result: 2
result: 3
upstream: IgnoreAction{action='1', ignoreNext=true}
upstream: IgnoreAction{action='2', ignoreNext=true}
upstream: IgnoreAction{action='3', ignoreNext=true}
So, first data ("start") is consumed, and writes triggered in onNext are ignored.
#Test
public void rxIgnore() throws Exception {
MockDb mockDb = new MockDb();
BehaviorSubject<Boolean> ignoreNextStream = BehaviorSubject.create(false);
Observable<String> dataStream = mockDb.dataSource();
dataStream.zipWith(ignoreNextStream, Data::new)
.doOnNext(action -> System.out.println("upstream: " + action))
.filter(Data::isTakeNext)
.flatMap(__ -> Observable.just(1, 2, 3))
.subscribe(new Observer<Integer>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(Integer val) {
System.out.println("result: " + val);
ignoreNextStream.onNext(true);
mockDb.data(String.valueOf(val));
}
});
mockDb.data("start");
Observable.empty().delay(1, TimeUnit.MINUTES).toBlocking().subscribe();
}
private static class Data {
private final String action;
private final boolean ignoreNext;
public Data(String action, boolean ignoreNext) {
this.action = action;
this.ignoreNext = ignoreNext;
}
public boolean isTakeNext() {
return !ignoreNext;
}
#Override
public String toString() {
return "IgnoreAction{" +
"action='" + action + '\'' +
", ignoreNext=" + ignoreNext +
'}';
}
}
private static class MockDb {
private final Subject<String, String> subj = PublishSubject.<String>create()
.toSerialized();
public void data(String action) {
subj.onNext(action);
}
Observable<String> dataSource() {
return subj;
}
}
I'm very new to RXJava.
I have a function called politelyrefresh() that concats two observables together, but the functions in these two observables only run the first time I called politeRefresh, I'm not sure this is the right way to do it. What I want is run this function inside the observables everytime.
public void politelyRefresh() {
Observable.concat(refreshStoreDataObservable, refreshProjectDataObservable)
.finallyDo(()-> {
try {
//someother other long runnning-network requests
} catch (Exception e) {
Log.e(TAG, "politelyRefresh finallyDo Error", e);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(reloadUiFromLocalStorageSubscriber);
}
//the other observable is pretty much the same but making another request
Observable<String> refreshStoreDataObservable = Observable.create(new Observable.OnSubscribe<String>() {
#Override
public void call(Subscriber<? super String> subscriber) {
//DOESN'T GET HERE SECOND TIME!
Store.syncStores(new ListCallback() {
#Override
public void syncSuccess() {
getSyncStateManager().setStoresRefreshed();
subscriber.onCompleted();
}
#Override
public void syncError() {
subscriber.onError(new Throwable("SYNC STORES ERROR"));
getSyncStateManager().setStoresSyncCompleted();
}
});
}
});
Subscriber<String> reloadUiFromLocalStorageSubscriber = new Subscriber<String>() {
#Override
public void onCompleted() {
if (mStoreRefreshLayout != null){
mStoreRefreshLayout.setRefreshing(false);
}
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "reloadUiFromLocalStorageSubscriber: onError", e);
if (mStoreRefreshLayout != null){
mStoreRefreshLayout.setRefreshing(false);
}
}
#Override
public void onNext(String s) {
Log.d(TAG, "reloadUiFromLocalStorageSubscriber: onNext " + s);
}
};
I think you're looking for Observable.defer(). What this basically does is defer the creation of the Observable to when it is being subscribed to.
Here's a quick example:
public class Refresher {
Refresher() {
politelyRefresh();
politelyRefresh();
}
public void politelyRefresh() {
Observable.defer(() -> Observable.concat(refreshProjectData(), refreshStoreData()))
.map(this::processData)
.subscribe(this::printData);
}
private Observable<String> refreshStoreData() {
System.out.println("StoreData Refreshed");
return Observable.just("data1","data2","data3");
}
private Observable<String> refreshProjectData() {
System.out.println("ProjectData Refreshed");
return Observable.just("Project1","Project2", "Project3");
}
private String processData(String data) {
return data + " processed";
}
private void printData(String data) {
System.out.println(data);
}
}
If you instantiate our refresher object, you'll get
StoreData Refreshed
StoreData Refreshed
Project1 processed
Project2 processed
Project3 processed
data1 processed
data2 processed
data3 processed
StoreData Refreshed
StoreData Refreshed
Project1 processed
Project2 processed
Project3 processed
data1 processed
data2 processed
data3 processed
If you'd like something to run on a different thread, you'd specify that on the specific observable you're looking to run on a non-ui thread.
So, for example, you might want to run the Observable in politelyRefresh on a background thread and subscribe to it on the UI thread. The creation of the other Observables will happen in a background thread too!
I finally got this to work by move the subscriber from an class instance to inside the .subscribe() function(). I have no idea why this is happening.
Observable.concat(refreshStoreDataObservable, refreshProjectDataObservable)
.finallyDo(()-> {
try {
//someother other long runnning-network requests
} catch (Exception e) {
Log.e(TAG, "politelyRefresh finallyDo Error", e);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe( new Subscriber<String>() { /*rest of code */}); //**here