I'm using MVVM architecture, So basically this is my flow -
getting repository instance and return to Activity the result -
public MainViewModel() {
moviesRepository = MoviesRepository.getInstance();
}
public LiveData<AllMovies> getAllMovies() {
return moviesRepository.getAllMovies();
}
In my repository -
private MoviesRepository() {
allMoviesMutableLiveData = new MutableLiveData<>();
getDataFromApi();
}
private void getDataFromApi() {
disposable.add(
ApiService.getMoivesApi().getAllMovies()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableSingleObserver<AllMovies>() {
#Override
public void onSuccess(AllMovies allMovies) {
allMoviesMutableLiveData.setValue(allMovies);
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
}
})
);
}
I've 2 questions:
1) This is my first time using rxJava, this is the correct flow of getting the response?
2) I'm using disposable to clear if there is a problem, and I should call in onCleared method, dispoable.clear().
the problem is, that I haven't onCleared override method in my repository, so I can I clear this?
If you are planing to use RxJava + LiveDate there is no need to invent stuff that you can reuse.
Better approach is to use LiveDataReactiveStreams. In this case your repository doesn't need to handle LiveData.
Flowable<String> flowable = ... ;
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(flowable);
Main issue is that you are return LiveData a result. Live data you exist inside of ViewModel and should not change it's instance. All you need is to update value of a MutableLiveData
use subscribeOn(Schedulers.io()) instead of Schedulers.newThread()
and use dispoable.clear() in Activity's onPause() method
check this : https://github.com/mohosyny/simpleMVP
Related
I'm using Retrofit, RxJava and MVVM pattern in my app.
I have API that retrieve me a list of movies.
In the API request I must to say what page I want to load ( The API retrieve me 1 page (20 items) for each request).
At the start of the app I want to load 3 pages.
How to I do that?
That what i have now:
Repository:
public MutableLiveData<List<MoviesResult>> getMoviesResultsMutableLiveData(String sort_by, int page, String with_genres, String with_cast) {
MoviesService moviesService = RetrofitInstance.getMoviesService();
mDisposable = moviesService.getMovies(
mApplication.getResources().getString(R.string.api_key),
sort_by,
UserSettings.getInstance().includeAdults(),
true, page, with_genres, with_cast)
.retryWhen(throwable ->
throwable.delay(5, TimeUnit.SECONDS))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<MoviesResponse>() {
#Override
public void accept(MoviesResponse moviesResponse) throws Throwable {
mMoviesResultsMutableLiveData.setValue(moviesResponse.getMoviesResults());
mDisposable.dispose();
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Throwable {
CustomToast customToast = new CustomToast(mApplication.getApplicationContext());
customToast.makeCustomText(throwable.getMessage());
mDisposable.dispose();
}
});
return mMoviesResultsMutableLiveData;
}
ViewModel:
public MutableLiveData<List<MoviesResult>> getMoviesResultMutableLiveData(String sort_by, int page, String with_genres, String with_cast) {
return mMoviesRepository.getMoviesResultsMutableLiveData(sort_by, page, with_genres, with_cast);
}
And MainActivity:
mMoviesViewModel.getMoviesResultMutableLiveData(getString(R.string.sort_by_popularity), 1, null, null)
.observe(this, new Observer<List<MoviesResult>>() {
#Override
public void onChanged(List<MoviesResult> moviesResults) {
Log.d(TAG, "onChanged: "+moviesResults.size());
}
});
I've tried to make a for loop in main activity when "i" equals to pages I want to load, and then set the i to the page in getMovies method - but it's not worked for me.
for (int i = 0; i < 3; i++) {
mMoviesViewModel.getMoviesResultMutableLiveData(getString(R.string.sort_by_popularity), i, null, null)
.observe(this, new Observer<List<MoviesResult>>() {
#Override
public void onChanged(List<MoviesResult> moviesResults) {
Log.d(TAG, "onChanged: "+moviesResults.size());
}
});
}
In this case i receive only 1 page .
Link to add Paging without using paging library Link
It's not a good idea to add logic inside your View (Activity/Fragment), you are using a for loop inside View which is not ideal. So move this into your ViewModel or UseCase.
RxJava has an operator named concatWith which concats multiple observables together. thus when your Activity/Fragment lunches you can initiate three API calls and contact them together and return the three as a single model to your observer.
By the way, Using Android Paging from Android Jetpack is a better solution for what you what to achieve.
I constructed MVVM and get data from Network by Retrofit 2. Getting data flow goes like this: MainActivity - > ViewModel -> Repository -> APiService. So, I call enqueu from the Repository, like this:
public List<Result> getArticles() {
final List<Result>[] articles = new List[]{new ArrayList<>()};
Log.d(TAG, "getArticles");
ApiService.getService().getArticles("test", "thumbnail").enqueue(new Callback<Example>() {
#Override
public void onResponse(Call<Example> call, Response<Example> response) {
Log.d(TAG, "onResponse");
if (response.isSuccessful()) {
Log.d(TAG, "isSuccessful");
articles[0] = response.body().getResponse().getResults();
}
}
#Override
public void onFailure(Call<Example> call, Throwable t) {
Log.d(TAG, "onFailure");
}
});
return articles[0];
}
And I call getArticles from my ViewModel like this:
public List<Result> getArticleList() {
Log.d(TAG, "getArticleList");
articleRepository = new ArticleRepository();
articleRepository.getArticles();
return articleList;
}
However, my enqueue doesn't work and I spent a couple of hours to figure out why, still can't. The only thing I have noticed is that when I make a call not from the ViewModel, but from MainActivity, enqueue does work!!!
Can anyone tell me what am I missing here? Why the same thing doesn't work from ViewModel? I think there is some threading or lifecycle problem, but can't figure out what exactly.
Also, noticed that when getting data I try to print in MainActivity, it doesn't work:
for (Result article : articleList) {
Log.d(TAG, article.getSectionName());
}
But when I print it from retrofit enqueue onResponse callback it does work. What's the problem here?
Try to change your ViewModel to return articleRepository.getArticles()
public List<Result> getArticleList() {
Log.d(TAG, "getArticleList");
articleRepository = new ArticleRepository();
return articleRepository.getArticles();
}
ok, so i'm trying to implement rxJava2 with retrofit2. The goal is to make a call only once and broadcast the results to different classes. For exmaple: I have a list of geofences in my backend. I need that list in my MapFragment to dispaly them on the map, but I also need that data to set the pendingIntent service for the actual trigger.
I tried following this awnser, but I get all sorts of errors:
Single Observable with Multiple Subscribers
The current situation is as follow:
GeofenceRetrofitEndpoint:
public interface GeofenceEndpoint {
#GET("geofences")
Observable<List<Point>> getGeofenceAreas();
}
GeofenceDAO:
public class GeofenceDao {
#Inject
Retrofit retrofit;
private final GeofenceEndpoint geofenceEndpoint;
public GeofenceDao(){
InjectHelper.getRootComponent().inject(this);
geofenceEndpoint = retrofit.create(GeofenceEndpoint.class);
}
public Observable<List<Point>> loadGeofences() {
return geofenceEndpoint.getGeofenceAreas().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.share();
}
}
MapFragment / any other class where I need the results
private void getGeofences() {
new GeofenceDao().loadGeofences().subscribe(this::handleGeoResponse, this::handleGeoError);
}
private void handleGeoResponse(List<Point> points) {
// handle response
}
private void handleGeoError(Throwable error) {
// handle error
}
What am I doing wrong, because when I call new GeofenceDao().loadGeofences().subscribe(this::handleGeoResponse, this::handleGeoError); it's doing a separate call each time. Thx
new GeofenceDao().loadGeofences() returns two different instances of the Observable. share() only applies to the instance, not the the method. If you want to actually share the observable, you'd have to subscribe to the same instance. You could share the it with a (static) member loadGeofences.
private void getGeofences() {
if (loadGeofences == null) {
loadGeofences = new GeofenceDao().loadGeofences();
}
loadGeofences.subscribe(this::handleGeoResponse, this::handleGeoError);
}
But be careful not to leak the Obserable.
Maybe it's not answering your question directly, however I'd like to suggest you a little different approach:
Create a BehaviourSubject in your GeofenceDao and subscribe your retrofit request to this subject. This subject will act as a bridge between your clients and api, by doing this you will achieve:
Response cache - handy for screen rotations
Replaying response for every interested observer
Subscription between clients and subject doesn't rely on subscription between subject and API so you can break one without breaking another
How to create my own hot Observable from scratch?
I would like create my own function, returning observable, returning locations:
public static Observable<Location> locationObservable(Context context, String provider, long minTime, float minDistance) {
This is for Android. It is recommended to use Observable.create() for this purposes, but example shows just passing constant list of integers to each subscriber, which is not hot.
If I do something else here, for example, remember a list of subscribers, then how will I implement unsubscribing and many other features?
I.e. absolutely no idea is what to do inside Observable.OnSubscribe<Integer>() implementation?
Generally to create hot observable you use some kind of Subject: PublishSubject, BehaviorSubject, etc.
See examples for BehaviorSubject here.
class LocationService {
private Subject<Location> subject = BehaviorSubject.create();
Observable<Location> locationObservable(...) {
return subject;
}
void onNewLocationListener(Location newLocation) {
subject.onNext(newLocation);
}
}
It is not recommended to write your own, at least until you are proficient with the existing ones and need a peculiar caching/emission pattern not covered by the default 5 (Async, Behavior, Publish, Replay, Unicast).
I have a 3 part series on the subject (pun intended) if you really want to:
Part 1
Part 2
Part 3
Look at this wonderful example taken directly from Realm's RealmObservableFactory:
#Override
public Observable<Realm> from(Realm realm) {
final RealmConfiguration realmConfig = realm.getConfiguration();
return Observable.create(new Observable.OnSubscribe<Realm>() { // create new observable
#Override
public void call(final Subscriber<? super Realm> subscriber) { // this is executed on `subscribeOn(Scheduler)`
final Realm observableRealm = Realm.getInstance(realmConfig);
final RealmChangeListener<Realm> listener = new RealmChangeListener<Realm>() {
#Override
public void onChange(Realm realm) {
if (!subscriber.isUnsubscribed()) { // always check if subscriber is unsubscribed!
subscriber.onNext(observableRealm);
}
}
};
subscriber.add(Subscriptions.create(new Action0() { // add unsubscription first! thread specified by unsubscribeOn(Scheduler)
#Override
public void call() {
observableRealm.removeChangeListener(listener); // remove listener
observableRealm.close();
}
}));
observableRealm.addChangeListener(listener); // add listener
subscriber.onNext(observableRealm); // initial value
}
});
}
And read the comments, it's a pretty good example.
I'm using Paging library to paginate a list of items I'm retrieving from my server. Initially, when my fragment is loaded, it returns an empty list. But after changing fragments and going back to that fragment, I can see the list loaded. After debugging I saw that data was actually being fetched, but an empty list was passed to my fragment.
ItemDataSource:
#Override
public void loadInitial(#NonNull LoadInitialParams<Integer> params, #NonNull LoadInitialCallback<Integer, Item> callback) {
apiService.getItems(OFFSET)
.enqueue(new Callback<ItemWrapper>() {
#Override
public void onResponse(#NonNull Call<ItemWrapper> call,#NonNull Response<ItemWrapper> response) {
callback.onResult(response.body().getItems(), null, OFFSET + 25);
}
#Override
public void onFailure(#NonNull Call<ItemWrapper> call,#NonNull Throwable t) {
t.printStackTrace();
}
});
}
#Override
public void loadBefore(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, Item> callback) {
}
#Override
public void loadAfter(#NonNull LoadParams<Integer> params, #NonNull LoadCallback<Integer, Item> callback) {
apiService.getItems(params.key)
.enqueue(new Callback<ItemWrapper>() {
#Override
public void onResponse(#NonNull Call<ItemWrapper> call,#NonNull Response<ItemWrapper> response) {
Integer key = response.body().getItems().isEmpty() ? null : params.key + 25;
callback.onResult(response.body().getItems(), key);
}
#Override
public void onFailure(#NonNull Call<ItemWrapper> call,#NonNull Throwable t) {
t.printStackTrace();
}
});
}
ItemDataSourceFactory:
#Override
public DataSource create() {
ItemDataSource itemDataSource = new ItemDataSource();
itemLiveDataSource.postValue(itemDataSource);
return itemDataSource;
}
public MutableLiveData<ItemDataSource> getItemLiveDataSource() {
return itemLiveDataSource;
}
ItemViewModel:
private LiveData<ItemDataSource> liveDataSource;
private LiveData<PagedList<Item>> itemPagedList;
private ItemViewModel(Application application) {
ItemDataSourceFactory factory = new ItemDataSourceFactory();
liveDataSource = factory.getItemLiveDataSource();
PagedList.Config config = (new PagedList.Config.Builder())
.setEnablePlaceholders(false)
.setPageSize(ItemDataSource.LIMIT).build();
itemPagedList = (new LivePagedListBuilder(factory, config)).build();
}
public LiveData<PagedList<Item>> getItems() {
return itemPagedList;
}
Fragment:
ItemViewModel itemViewModel = ViewModelProviders.of(this).get(ItemViewModel.class);
itemViewModel.getItems.observe(this, items -> {
adapter.submitList(items);
})
Not 100% sure, but I think this is because you are running an asynchronous request. try to change it to run synchronously for loadInitial() like so request.execute()
I also had this problem once and I still can't figure it out why it does not work for some fragments. The solution I found, aldo being more like a fast sketchy fix is to load the fragment twice.
Yassin Ajdi is right. loadinitial() calls immediately on the same thread where PagedList is created on. As your API is async, the method runs empty for the first time
If, like me, anyone is using an asynchronous RxJava/Kotlin call in loadInitial. I finally figured out a solution after many painful hours.
I tried using a delayed Handler (500ms) in the Observer method but it was fickle and didn't work in every scenario. No matter how much I tried to make it synchronous using setFetcher and Rx observeOn it wouldn't consistently work.
My solution was to use .blockingSubscribe in my Observable. My data fetcher was using a Socket library that had its own concurrency out of my scope, so I couldn't guarantee that I could make the process wholly synchronous as Paging requires. (A process which needs better documentation IMO). Anyway, here's my solution, hopefully it helps others with same issue:
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, ResultItem>
) {
mySocketClientRxRequest()
.subscribe ({
callback.onResult(it.resultItems 1, 2)
},{
it.printStackTrace()
})
}
to
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, ResultItem>
) {
mySocketClientRxRequest()
.blockingSubscribe ({
callback.onResult(it.resultItems 1, 2)
},{
it.printStackTrace()
})
}
Just as in the question I was loading the data asynchronously inside loadInitial, but the data did not come to the adapter when callback was called.
Solved just by updating the library version from
androidx.paging:paging-runtime-ktx:2.1.2
to androidx.paging:paging-runtime-ktx:3.0.0.
I've been having this same problem as well for a few days until finally I found the solution. But just to note I used Paging version 3, so this might not answer this question directly, but might help anyone who's still struggling in blank recyclerView initially
In your fragment where you applied PagingAdapter, use loadStateFlow on the adapter to observe changes in loadState. You can place this in onCreateView for Fragment
val pagingAdapter = PagingAdapter()
lifecycleScope.launch {
pagingAdapter.loadStateFlow.collect { loadState ->
if (loadState.prepend.endOfPaginationReached) {
// Apply this if PagingDataAdapter start binding data in view holder
println("APPLYING ADAPTER")
binding.recyclerView.adapter = pagingAdapter
cancel() // Cancel this flow after applying adapter
}
}
}