I looked the same topics and found nothing that could help me.
So this is my problem. I have an activity with a RecyclerView. I'm using Retrofit2 library to get list of news, here it is:
#GET(VERSION_V1 + "/news")
Observable<BaseResponse<NewsListResponse>> getCompanyNews(#Query("companyId") String companyId,
#Query("query") String query,
#Query("offset") Integer offset,
#Query("limit") Integer limit,
#Query("creator") NewsCreatorType creator,
#Query("date")NewsFilterType dateFilter);
And I have a Presenter:
public void loadData(boolean refresh) {
mCoreServices
.getApiService()
.getApi()
.getCompanyNews(mCompanyId, "", 0, 100, mCompanyId == null ? NewsCreatorType.SYSTEM : NewsCreatorType.COMPANY, mFilterType)
.map(response -> response.getResult().getItems())
.flatMap(news -> {
if (mCompanyId == null || "".equals(mCompanyId)) {
return Observable.just(news);
} else {
return mCoreServices
.getApiService()
.getApi()
.getCompanyNews(null, "", 0, 100, NewsCreatorType.SYSTEM, mFilterType)
.map(response -> {
news.addAll(response.getResult().getItems());
return news;
});
}
})
.flatMapIterable(newsItems -> newsItems)
.map(NewsViewModel::new)
.toSortedList((news1, news2) -> news2.getDate().compareTo(news1.getDate()))
.retryWhen(errors -> retryAfterRefreshToken(errors))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(() -> {
if (refresh) {
getViewState().onShowRefresh(true);
} else {
getViewState().onShowProgressBar(true);
}
})
.doOnUnsubscribe(() -> {
if (refresh) {
getViewState().onShowRefresh(false);
} else {
getViewState().onShowProgressBar(false);
}
})
.subscribe(news -> {
mResults = news;
getViewState().onLoadNews(mResults);
}, new ApiExceptionObservable(TAG) {
#Override
public void call(String message) {
getViewState().onShowErrorMessage(message);
}
#Override
public void unauthorized() {
getViewState().showUnauthorizedDialog();
}
});
}
And ofc I have a RecyclerView adapter, this is my onBindViewHolder:
#Override
public void onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position) {
if (baseHolder instanceof ViewHolder) {
ViewHolder holder = (ViewHolder) baseHolder;
NewsViewModel newsViewModel = mResults.get(position);
holder.mTVTitle.setText(newsViewModel.getTitle());
holder.mTVContent.setText(newsViewModel.getContent());
if (NewsCreatorType.SYSTEM.equals(newsViewModel.getCreator())) {
holder.mTVCreator.setText(newsViewModel.getCreator().getTitle());
} else {
holder.mTVCreator.setText(
holder.mTVCreator.getContext().getString(
newsViewModel.getCreator().getTitle(),
mCompanyTitle
)
);
}
}
}
Well, I have 2 companies doing news. News from company#1 are not duplicated, news from company#2 are always duplicated.. I don't understand where's my mistake. I checked (using debugger) the answer on my GET, it has no duplicated news, so the problem with my adapter I guess..
Thank you.
EDIT:
I get duplicated news from Creator.SYSTEM only.
EDIT 2.0:
Okay, I removed .flatMap in my Presenter, now it works fine, but I can't understand why it was there, can someone explain please?
Related
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
What can I use instead of addAll() method in my adapter, I'm using realm version 2.0.1 and that method is deprecated, I'm trying to get all the data from the API, save it to my database and pass it to my adapter, I'm using like this:
public void getData(int page) {
if (GlobalModel.existeConexao()) {
Call<PedidosResponse> call = ((NavigationMain) getActivity()).apiService.getPedidos(GlobalModel.getToken(), GlobalModel.geEmpresaId(), page);
call.enqueue(new Callback<PedidosResponse>() {
#Override
public void onResponse(Call<PedidosResponse> call, Response<PedidosResponse> response) {
if (response.isSuccessful()) {
for (int i = 0; i < response.body().getPedidos().size(); i++) {
Pedidos mPedido = response.body().getPedidos().get(i);
int myInt = (mPedido.isProjecao()) ? 1 : 0;
if (!mRepositorio.checkIfExists(mPedido.getId())) {
mRepositorio.addPedido(mPedido.getId(), mPedido.getCliente_id(), mPedido.getData_hora(), mPedido.getData_pedido_cliente(), mPedido.getPrevisao_entrega(), mPedido.getFrete_tipo(), myInt, mPedido.getObservacao(), mPedido.getAliquota_projecao(), mPedido.getStatus(), mPedido.getPedido_cliente());
}
}
arraypedidos = mRepositorio.findAllPedidos();
if (mPedidosAdapter == null) {
mPedidosAdapter = new PedidosAdapter(getActivity(), arraypedidos);
listpedidos.setAdapter(mPedidosAdapter);
} else {
mPedidosAdapter.setData(arraypedidos);
}
}
}
#Override
public void onFailure(Call<PedidosResponse> call, Throwable t) {
if (t.getMessage() != null) {
Log.v("pedidos", t.getMessage());
}
}
});
} else {
Toast.makeText(getActivity(), "Verifique sua conexão", Toast.LENGTH_SHORT).show();
}
}
But when I run the app I get this message:
java.lang.UnsupportedOperationException: This method is not supported by RealmResults.
That's because RealmResults is just a set of pointers that satisfy the condition defined in the query. You can't manipulate it, nor should you if you just intend to show every element in your adapter.
In fact, Realm was explicitly designed to simplify the workflow of "downloading data on a background thread and saving the data in a database", and "showing the data downloaded on a background thread automatically on the UI thread".
This is what RealmChangeListener is for.
Simply put, all of this code is unnecessary:
arraypedidos = mRepositorio.findAllPedidos();
if (mPedidosAdapter == null) {
mPedidosAdapter = new PedidosAdapter(getActivity(), arraypedidos);
listpedidos.setAdapter(mPedidosAdapter);
} else {
mPedidosAdapter.setData(arraypedidos);
}
And could be replaced with this:
public class SomeActivity extends AppCompatActivity {
PedidosAdapter pedidosAdapter;
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.some_view);
pedidosAdapter = new PedidosAdapter(context, mRepositorio.findAllPedidos());
// set adapter, ...
}
}
And
public class PedidosAdapter extends RealmRecyclerViewAdapter<Pedidos, PedidosViewHolder> {
public PedidosAdapter(Context context, RealmResults<Pedidos> results) {
super(context, results, true);
}
// onBindViewHolder
// onCreateViewHolder
}
For this, use RealmRecyclerViewAdapter, unless you intend to handle the RealmChangeListener manually.
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
I have an observable which might fail with a special exception in which case I want to show a dialog with a retry button. I've seen this answer, but it doesn't quite do what I want. I wasn't able to use retryWhen to solve my problem, so instead I used onErrorResumeNext. If you can come up with a way to do the same with retryWhen, please tell.
Right now I have this piece of code:
public Observable<Order> proceedWithOrdering(Activity activity) {
return apiService.createOrder()
.subscribeOn(Schedulers.io())
.compose(applyRetryLogic(activity))
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread());
}
public <T extends ApiResponse> Observable.Transformer<T, T> applyRetryLogic(Activity activity) {
return observable -> observable
.onErrorResumeNext(retry(observable, activity))
.subscribeOn(AndroidSchedulers.mainThread());
}
public <T> Func1<Throwable, ? extends Observable<? extends T>> retry(Observable toRetry, Activity activity) {
return throwable -> {
if (throwable instanceof NetworkException) {
MaterialDialog dialog = retryDialog(activity);
View retry = dialog.getActionButton(DialogAction.POSITIVE);
View cancel = dialog.getActionButton(DialogAction.NEGATIVE);
Observable<Object> retryClick = RxView.clicks(retry).map(o -> {
dialog.dismiss();
return o;
});
Observable<Object> cancelClick = RxView.clicks(cancel).flatMap(o -> {
dialog.dismiss();
return Observable.error(throwable);
});
dialog.show();
return Observable.amb(retryClick, cancelClick)
.flatMap(o -> toRetry.compose(applyRetryLogic(activity)));
} else {
return Observable.error(throwable);
}
};
}
The problem is that the call inside the retry gets executed not on the main thread and it raises the Can't create handler inside thread that has not called Looper.prepare() exception.
The question is - how do I force it to be executed on the main thread? As you can see, I have already tried doing subscribeOn(AndroidSchedulers.mainThread()) right after both compose and onErrorResumeNext with no luck.
I have tested my code using simple observables that don't operate on separate threads and it works fine.
You can accomplish this by flatMapping a PublishSubject which is then updated once the relevant button is pressed. Here is a classical Java Swing example.
public class RetryWhenEnter {
public static void main(String[] args) {
AtomicInteger d = new AtomicInteger();
Observable<Integer> source = Observable.just(1);
source.flatMap(v -> {
if (d.incrementAndGet() < 3) {
return Observable.error(new RuntimeException());
}
return Observable.just(v);
})
.retryWhen(err -> {
return err.flatMap(e -> {
System.out.println(Thread.currentThread() + " Error!");
PublishSubject<Integer> choice = PublishSubject.create();
SwingUtilities.invokeLater(() -> {
int c = JOptionPane.showConfirmDialog(null,
e.toString() + "\r\nRetry?", "Error",
JOptionPane.YES_NO_OPTION);
if (c == JOptionPane.YES_OPTION) {
choice.onNext(1);
} else {
choice.onCompleted();
}
});
return choice;
});
}).subscribe(System.out::println,
Throwable::printStackTrace);
}
}
Edit:
Or use observeOn(AndroidSchedulers.mainThread()) just before onErrorResumeNext or when using retryWhen: retryWhen(o -> o.observeOn(AndroidSchedulers.mainThread())...).
Edit 2
I've rolled back the change so the answer is meaningful again.
There is a way of solving my problem using retryWhen (thanks #akarnokd):
public <T extends ApiResponse> Observable.Transformer<T, T> applyRetryLogic(Activity activity) {
return observable -> observable
.retryWhen(err -> err.flatMap(throwable -> {
L.d(Thread.currentThread() + " Error!");
if (throwable instanceof NetworkException) {
PublishSubject<Integer> choice = PublishSubject.create();
activity.runOnUiThread(() -> {
MaterialDialog dialog = retryDialog(activity);
View retry = dialog.getActionButton(DialogAction.POSITIVE);
View cancel = dialog.getActionButton(DialogAction.NEGATIVE);
RxView.clicks(retry).subscribe(o -> {
dialog.dismiss();
choice.onNext(1);
});
RxView.clicks(cancel).subscribe(o -> {
dialog.dismiss();
choice.onError(throwable);
});
dialog.show();
});
return choice;
} else {
return Observable.error(throwable);
}
}));
}
I'm new in rxjava or rxandroid, and looking for a better way dealing with multiple requests. I need to get the token from server and use the result as a parameter to do login verification and if it returns success then get the sessionId through getSessionId method.
I've considered about zip or merge, but I don't think it'll work. So can you give me an idea or I don know , train of thought?
Thank you.
Here's my code:
private void getToken(final String name , final String pwd){
api.newToken()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<TokenModel>() {
#Override public void call(TokenModel tokenModel) {
String token = tokenModel.request_token;
if (!"".equals(token)){
login(token, name, pwd);
}else {
Timber.e("got token failed");
}
}
});
}
private void login(String token, String name, String pwd){
api.validateToken(token, name, pwd)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<TokenModel>() {
#Override public void call(TokenModel tokenModel) {
String token = tokenModel.request_token;
if (!"".equals(token)){
getSessionId(token);
}else {
Timber.e("got token failed");
}
}
});
}
private void getSessionId(String token){
api.newSessionn(token)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<TokenModel>() {
#Override public void onCompleted() {
//go to home activity
}
#Override public void onError(Throwable e) {
//handle error
}
#Override public void onNext(TokenModel tokenModel) {
//store session id
}
});
}
Your first subscription call your second subscription, ...
You can avoid this using flapmap operator.
api.newToken(...)
.flapMap(token -> api.validateToken(token))
.flapMap(token -> api.newSession(token)).subscribe()
New observable in a subscription can offen be replaced by a flatMap call.
If you want to manage your error, in a flatMap, if the token is invalid, your can return an error observable instead of returning new api call observable.
.flatMap(token -> if(token.isValid){ return api.newCall(); } else { return Observable.error(...); ;)