How to load multiple pages using retrofit and mvvm - java

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.

Related

Api calls with rxJava

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

Why doesn't Retrofit enqueue work when making a call from ViewModel?

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();
}

java can't find local variable in reterofit 2 api call

I have done an API call to retrieve a list of messages, then i want to check if each message has a flag of 2 and then if it does do another API call to receive information on whether the message has been "paid" and if it has alter the object message to paid = true;
Here is my failed attempt.
for (int i = 0; i < chatHistory.getData().size(); i++) {
final ChatMessage chatMessage = chatHistory.getData().get(i).getBody();
if (chatMessage.flag.equals("2")) {
RestClient.getInstance().getApiService().getPaymentRequest(chatMessage.payment_request_id, new Callback<SinglePaymentRequest>() {
#Override
public void success(SinglePaymentRequest singlePaymentRequest, Response response) {
Payment payment = singlePaymentRequest.getPayment();
if(payment.getStatus().equals("paid")) {
chatMessage.isPaid=true;
}
}
#Override
public void failure(RetrofitError error) {
System.out.println("fail");
}
});
}
chatMessages.add(chatMessage);
Log.e("chat history", chatMessage.from);
}
addData(chatMessages);
The problem I am facing is that the api call cannot find local variable chatmessage, any ideas as to why this is?
Thanks
Notice the bit of code new Callback<SinglePaymentRequest>() that creates your new Callback object? It does not have access to the variables outside it, for good reason too.
What you should be doing, is calling a setter method that's part of your container class (the one that is the parent of the Callback) that will, in turn manipulate the values that you want to change.

Binding data to the UI in GWT

In Silverlight, a frequently used pattern is:
Request data
Get back an empty container for the data
Asynchronously fire off a query to fill the container
When the query returns, fire an event on the container
Update the UI according to the container's contents
Can this be done in GWT?
The reason I ask is that I'm trying to make a SuggestBox that contains a list of group names and icons. First, I query Facebook to get a list of groups IDs that are close to the current String in the SuggestBox. Then, I fire off queries to get icons for each group id. The problem is that I have to return the suggestions before those queries are done. I'm not sure how to go back and insert the data after I have it. I don't want to block until the calls are complete, and there's no real way to know in advance what data to load.
I could return a widget for the suggestion that loads an image, but the suggestion must be a plain String.
What is the right approach here?
Let's assume you're using GWT RPC. You'll have some service interface that lets you fetch the groupIds for a suggestion and the icon for a specific group id.
public interface FacebookService extends RemoteService {
List<String> getFacebookGroupIds(String suggestion);
Icon getIconForGroup(String groupId);
}
You should build your own implementation of Suggestion that can display itself with either just a groupId or a groupId and an Icon.
public class FacebookGroupSuggestion implements Suggestion {
private String groupId;
private Icon icon;
public FacebookGroupSuggestion(String groupId) {
this.groupId = groupId;
}
public String getDisplayString() {
StringBuilder builder = new StringBuilder();
builder.append("<b>");
builder.append(this.groupId);
builder.append("</b>");
if (this.icon != null) {
builder.append(this.icon.toSafeHtml());
}
return builder.toString();
}
}
I'm using Icon as your own implementation of an icon, it's not a standard class.
Then, you can make your implementation of SuggestOracle to fetch the groupIds and icons asynchronously. The SuggestOracle uses a callback to inform the suggestBox that some response to a request is available. So fetch your results, and call the callback when you get them. It'll look something like this.
public class FacebookSuggestOracle extends SuggestOracle {
private FacebookServiceAsync service = GWT.create(FacebookService.class);
private Request currentRequest;
private Callback currentCallback;
#Override
public void requestSuggestions(Request request, Callback callback) {
// Save request & callback for future use.
this.currentRequest = request;
this.currentCallback = callback;
// Fetch the groupIds
service.getFacebookGroupIds(request.getQuery(), new AsyncCallback<List<String>>() {
public void onSuccess(List<String> result) {
createSuggestionsForGroupIds(result);
}
});
}
private void createSuggestionsForGroupIds(List<String> groupIds) {
List<FacebookGroupSuggestion> suggestions = new ArrayList<FacebookGroupSuggestion>();
for (String groupId : groupIds) {
suggestions.add(new FacebookGroupSuggestion(groupId));
}
Response response = new Response(suggestions);
// Tell the suggestBox to display some new suggestions
currentCallback.onSuggestionsReady(currentRequest, response);
// Fetch the icons
for (String groupId : groupIds) {
service.getIconForGroup(groupId, new AsyncCallback<Icon>() {
public void onSuccess(Icon result) {
// match the icon to the groupId in the suggestion list
// use the callback again to tell the display to update itself
}
});
}
}
}

Paging library returns empty list initially

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
}
}
}

Categories