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;
}
}
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 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 have 3 layers in my app. Layer1 subscribes to Observable from layer2. Layer2 subscribes to layer3 in order to emit returned data to layer1.
Layer1
layer2.getData(data).subscribe(newData -> {Log.d("onNext", "returned");},
throwable -> {Log.d("onError", throwable.getMessage());});
Suppose layer3 has a method called downloadDataFromApi(data);
public Observable<Data> getData(String data) {
return Observable.create(new Observable.OnSubscribe<Data>() {
#Override
public void call(Subscriber<? super Data> subscriber) {
Data data = new Data();
subscriber.onNext(data);
subscriber.onCompleted();
// Can't find a way to connect to layer3.
}
});
}
What do I need to do in layer2's getData() method? I basically want to have logics before returning Observable back to layer1.
Does that make sense?
Just return the Observable directly. Then layer1 handles subscription as usual.
class Layer2 {
public Observable<Data> getData(String data) {
return layer3.getData(data);
}
}
From what I see you have 3 layers (presentation, business logic, data access).
So what you could do is the following:
class PresentationLayer {
private BusinessLogicLayer layer;
PresentationLayer() {
layer = new BusinessLogicLayer();
}
public void showName() {
layer.getNameWithoutRxPrefix()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
#Override
public void accept(String name) throws Exception {
// show name somewhere
Log.d("PresentationLayer", "name: " + name);
}
});
}
}
class BusinessLogicLayer {
private DataAccessLayer layer;
BusinessLogicLayer() {
layer = new DataAccessLayer();
}
public Observable<String> getNameWithoutRxPrefix() {
return layer.getName()
.map(new Function<String, String>() {
#Override
public String apply(String name) throws Exception {
return name.replace("Rx", "");
}
});
}
}
class DataAccessLayer {
public Observable<String> getName() {
return Observable.just("RxAndroid");
}
}
As you can see, I return an Observable in my data access layer (getName), and chain another method to it in my business logic method (map) before returning it to the presentation layer.
Here is the example:
Student[] students = ...;
Subscriber<Course> subscriber = new Subscriber<Course>() {
#Override
public void onNext(Course course) {
Log.d(tag, course.getName());
}
...
};
Observable.from(students)
.flatMap(new Func1<Student, Observable<Course>>() {
#Override
public Observable<Course> call(Student student) {
return Observable.from(student.getCourses());
}
})
.subscribe(subscriber);
It's easy to observe courses, but what if I want to print out the pair of student-course? Is that necessary to change Observable<Course> to Observable<Pair<Student, Course>>? This method could be very tedious, because we could have multiple operators during transforming, but we don't want to keep the Pairs all the way transforming the observable.
Is that any other good way we can refer to the original item based on the transformed one?
Depending on your goals and implementation you can use an overloaded variant of flatMap like this:
Observable.from(students)
.flatMap(new Func1<Student, Observable<Course>>() {
#Override
public Observable<Course> call(Student student) {
return Observable.from(student.getCourses());
}
}, new Func2<Student, Course, Void>() {
#Override
public Void call(Student student, Course course) {
Log.d(tag, student + "->" + course.getName());
return null;
}
})
.subscribe();
students.doOnNext(new Action1<String>() {
#Override
public void call(String student) {
ArrayList<String> studentCourses = courses.get(student);
for (int i=0 ; i<studentCourses.size() ; i++) {
System.out.println(student + " -> " + studentCourses.get(i) + " " + test);
}
}
})
.subscribe(new Subscriber<String>() {
#Override
public void onNext(String s) { /*System.out.println(s); */}
#Override
public void onCompleted() { }
#Override public void onError(Throwable e) { }
});
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.