I'm just trying to learn mvvm, and I faced some issue - My List which should contain response from API are empty. I'm not sure why it is. Here's some code:
MainActivity
mViewModel = new ViewModelProvider(this).get(ViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Model>() {
#Override
public void onChanged(List<Model> list) {
if (data.size() > 0) {
data.clear();
}
if (list != null) {
data.addAll(list);
Log.i(TAG, "onChanged: " + data.size());
}
}
});
ViewModel
private Repository mRepository;
private MutableLiveData<List<Model> liveData;
public ViewModel(#NonNull Application application) {
super(application);
mRepository = Repository.getInstance();
liveData = mRepository.getData();
}
public MutableLiveData<List<Model> getData(){
return liveData;
}
Repository
public MutableLiveData<List<Model>> getData(){
MutableLiveData<List<Model> mLiveData = new MutableLiveData<>();
mApiCall.callApi()
.enqueue(new Callback<List<Model>() {
#Override
public void onResponse(Call<List<Model> call, Response<List<Model> response) {
mLiveData.setValue(response.body());
}
#Override
public void onFailure(Call<List<Model> call, Throwable t) {
t.getMessage();
}
});
return mLiveData;
}
in function getData() you are returning a live data and in this line in your view model :
liveData = mRepository.getData();
you are assigning it to liveDate which is a mutable live data created in your view model and the problem is here.
when this assignment is happened the observer in the liveData variable in view model will be removed and that's why we should use switchMap like this :
private var result : LiveData<List<Response>> = MutableLiveData()
result : LiveData<Response> = Transformations.map(mRepository.getData()){
it
}
and now all you need to do is to observe on repo in your view like this :
viewmodel.result.observe(this, Observer{ list ->
// to do with the result
})
response.body() will provide with class APIResponse. But what you need as response is List.
To get the expected response as List, try response.body().getMetadata().getResults()
It could be either repository initialisation as well. Do try creating
public void init() {
mRepository = Repository.getInstance();
liveData = mRepository.getData();
}
use viewModel.init();
Model class properties should match with response JSON fields to get the model objects from the response.
Debug on the response.body() and see the data coming. if the model is empty then there would be a mismatch on the fields.
Related
Since I'm new to programming Android apps I followed a tutorial on how to use the Android Architecture Components and Firebase for implementing the MVVM (using LiveData, ViewModel, etc.).
The tutorial I followed can be found here:
Part 1
Part 2
Part 3
I'm now left with what I think is a decent implementation of the MVVM, but I can not wrap my head around how I am supposed to pass query parameters to it. Right now I need to hardcode the ID of the document I want to retrieve:
public class AlarmDAO {
private FirebaseFirestore firebaseFirestore = FirebaseFirestore.getInstance();
public AlarmLiveData getFirestoreLiveData() {
DocumentReference documentReference = firebaseFirestore.collection(Collection.ALARMS.name).document("5RxJNuNyhDJlz49wpBkw");
return new AlarmLiveData(documentReference);
}
}
That then gets called by a class extending ViewModel.
public class AlarmViewModel extends ViewModel {
private AlarmDAO DAO = new AlarmDAO();
private AlarmLiveData liveData = null;
public LiveData<Alarm> getAlarmLiveData() {
liveData = DAO.getFirestoreLiveData();
return liveData;
}
public LiveData<Alarm> getAlarm() {
return liveData.alarm;
}
}
And then I observe that data in my activity:
model.getAlarmLiveData().observe(this, Observable -> {});
model.getAlarm().observe(this, alarm -> {
if (alarm != null) {
alarmTextView.setText(alarm.getTest());
else {
Log.d(TAG, "Waiting for data");
}
});
My problem is that I do not see a way of querying for a specific alarm. For instance model.getAlarm("someId"). I am under the impression that it should be done in the DAO and/or the ViewModel, but I can't figure out how. Another thing I do not understand is why I need to observe both model.getAlarmLiveData() and model.getAlarm() in my activity, as using only one does not work. The answer to both of those questions is most likely very simple, but thus far I haven't been able to figure it out.
For completeness: the Alarm class is nothing besides a getter and setter for two strings, and the AlarmLiveData class is below.
public class AlarmLiveData extends LiveData<Alarm> implements EventListener<DocumentSnapshot> {
private static final String TAG = AlarmLiveData.class.getSimpleName();
private Alarm alarmTemp = new Alarm();
private DocumentReference documentReference;
private ListenerRegistration listenerRegistration = () -> {};
public MutableLiveData<Alarm> alarm = new MutableLiveData<>();
public AlarmLiveData(DocumentReference documentReference) {
this.documentReference = documentReference;
}
#Override
protected void onActive() {
listenerRegistration = documentReference.addSnapshotListener(this);
super.onActive();
}
#Override
protected void onInactive() {
listenerRegistration.remove();
super.onInactive();
}
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable FirebaseFirestoreException e) {
if (documentSnapshot != null && documentSnapshot.exists()) {
alarmTemp = new Alarm();
alarmTemp.setId(documentSnapshot.getId());
alarmTemp.setTest(documentSnapshot.get("test").toString());
alarm.setValue(alarmTemp);
} else {
Log.d(TAG, "ERROR");
}
}
}
Thank you for reading, I'm looking forward to the answer(s)!
The reason you have to use both model.getAlarmLiveData() and model.getAlarm() looks to be that your AlarmLiveData class extends LiveData but sets a value for the contained MutableLiveData member variable instead of setting its own class value.
Inside your AlarmLiveData class:
// Comment out/Remove your 'public MutableLiveData<alarm> alarm' member variable from the top.
// You're going to want to set the value of the AlarmLiveData class itself instead.
// ...
// Then inside of your onEvent callback
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable FirebaseFirestoreException e) {
if (documentSnapshot != null && documentSnapshot.exists()) {
alarmTemp = new Alarm();
alarmTemp.setId(documentSnapshot.getId());
alarmTemp.setTest(documentSnapshot.get("test").toString());
// Set the value for the AlarmLiveData class directly
setValue(alarmTemp);
} else {
Log.d(TAG, "ERROR");
}
}
I'm not sure why you're creating a DAO class and I would most likely move that code directly into the AlarmViewModel class.
But, here is how you can alter your current DAO class if you don't want to remove it:
// Pass in the document id you want to create a document reference for
public AlarmLiveData getFirestoreLiveData(String documentId) {
DocumentReference documentReference = firebaseFirestore.collection(Collection.ALARMS.name).document(documentId);
return new AlarmLiveData(documentReference);
}
Your AlarmViewModel class would look something like this:
public class AlarmViewModel extends ViewModel {
private AlarmDAO DAO = new AlarmDAO();
private AlarmLiveData liveData = null;
// Make sure to take in the document id so you can create the corresponding LiveData
public LiveData<Alarm> getAlarmLiveData(String documentId) {
// Only create a new LiveData instance if the current one is null.
// This is helpful if you intend to use this as a Shared ViewModel.
if(liveData == null){
liveData = DAO.getFirestoreLiveData(documentId);
}
return liveData;
}
}
Finally, in your Activity:
// Pass in the document id and observe the ViewModel
model.getAlarmLiveData("MY_DOCUMENT_ID").observe(this, alarm -> {
if (alarm != null) {
alarmTextView.setText(alarm.getTest());
}else{
Log.d(TAG, "Waiting for data");
}
});
I am developing android application and I want to refresh viewModel livedata from second activity. When I get back to first activity data is not refreshed.
FirstActivity:
mViewModel = ViewModelProviders.of(this).get(MenuViewModel.class);
mViewModel.getMenus().observe(this, menuResponse -> {
if (menuResponse != null) {
resMenus.addAll(menuResponse.getMenus());
progressBar.setVisibility(View.GONE);
mAdapter.notifyDataSetChanged();
}
});
MenuViewModel:
public class MenuViewModel extends AndroidViewModel {
private MutableLiveData<MenuResponse> restMenuData;
private MenusRepository mRepository;
public MainActivityViewModel(#NonNull Application application) {
super(application);
mRepository = MenusRepository.getInstance(application);
restMenuData = mRepository.getMenus();
}
public LiveData<MenuResponse> getMenus() {
return restMenuData;
}
}
MenusRepository
private MenusRepository(Context context) {
apiRequest= RetrofitInstance.getInstance(context).getApiRequest();
}
public synchronized static MenusRepository getInstance(Context context) {
if (projectRepository == null) {
projectRepository = new MenusRepository(context);
}
return projectRepository;
}
public MutableLiveData<MenuResponse> getMenus() {
final MutableLiveData<MenuResponse> data = new MutableLiveData<>();
apiRequest.getMenus().enqueue(new Callback<MenuResponse>() {
#Override
public void onResponse(#NonNull Call<MenuResponse> call, #NonNull Response<MenuResponse> response) {
if (response.isSuccessful() && response.body() != null) {
data.setValue(response.body());
}
}
#Override
public void onFailure(#NonNull Call<MenuResponse> call, #NonNull Throwable t) {
data.setValue(null);
}
});
return data;
}
SecondActivity:
MenuViewModel mViewModel = ViewModelProviders.of(Objects.requireNonNull(SecondActivity.this)).get(MenuViewModel.class);
mViewModel.getMenus();
// This line not refresh menus
I except to refresh data from viewmodel, but it return old data.
How can I refresh viewmodel data in best practices?
MenusRepository.getMenus() methods creates a new instance of LiveData for every call. This is not the correct way to go about it.
You should have only one instance of LiveData and different objects subscribe to it (activity, viewModel, etc).
What you could do is - create a singleton of MenusRepository (which I think you have already done). Create one instance of MutableLiveData only and use it to update the data.
class MenusRepository {
private val liveData = MutableLiveData<MenuResponse>()
fun getMenus() {
// Your api call. Do not create a new instance of the livedata.
}
fun menus(): LiveData<MenuResponse> {
return liveData
}
fun update(data: MenuResponse) {
liveData.post(data)
}
This code is in Kotlin, but it applies similarly to Java as well.
You can update method to post an update to liveData. When you update it, all the observers will receive the new data. Use MenusRepository.menus() to access LiveData in your ViewModel.
Update
Your MenuRepository class could be like this.
private final MutableLiveData<MenuResponse> liveData = new MutableData<>();
private MenusRepository(Context context) {
apiRequest= RetrofitInstance.getInstance(context).getApiRequest();
}
public synchronized static MenusRepository getInstance(Context context) {
if (projectRepository == null) {
projectRepository = new MenusRepository(context);
}
return projectRepository;
}
public MutableLiveData<MenuResponse> loadMenus() {
apiRequest.getMenus().enqueue(new Callback<MenuResponse>() {
#Override
public void onResponse(#NonNull Call<MenuResponse> call, #NonNull Response<MenuResponse> response) {
if (response.isSuccessful() && response.body() != null) {
liveData.setValue(response.body());
}
}
#Override
public void onFailure(#NonNull Call<MenuResponse> call, #NonNull Throwable t) {
liveData.setValue(null);
}
});
}
public LiveData<MenuResponse> getMenus() {
return liveData;
}
public void updateData(response: MenuResponse) {
liveData.postValue(response);
}
When you want to update the data manually (from another activity),
use menuRepository.update() method. This will post the data to your LiveData which will update all its observers, ie. the ViewModel.
Call menuRepository.loadMenu() when you want to get the data using API.
Use menuRepository.getMenus() to get the LiveData and attach your observers.
Since MenuRepository is a singleton, there's only one instance of LiveData. When you will post an update to this instance of LiveData, all the observers will receive the new data.
public class MenuViewModel extends AndroidViewModel {
private MutableLiveData<MenuResponse> restMenuData;
private MenusRepository mRepository;
public MainActivityViewModel(#NonNull Application application) {
super(application);
mRepository = MenusRepository.getInstance(application);
restMenuData = mRepository.getMenus();
}
public LiveData<MenuResponse> getMenus() {
restMenuData = new MutableLiveData<>();
return restMenuData;
}
}
Change the View model code as above.
So that the live data is always cleared before returning to activity.
UPDATE:::
I've updated the question to include demo other LiveData that were also required:
so we have userLD that we need the value of to get the goalWeeklyLD, and we need the goalWeeklyLD value to get the remaining 4 LiveData values as they come from Room querys that use goalWeekly.dateproperties in the query
:::::
I've hit a problem where I have a fragment that has to populate LiveData that uses a query dependent on another LiveData value.
how can i get my live data to work correctly when it is dependent on other results?
Without using The Transitions.map() the view model throws an error because the values of the other live data are still null.
with the Transitions.map() in the view model the activities observer throws an error because the LiveData is still null.
I could possibly cheat my way past this by using a horrendously big nested query to return all i need in one custom DTO. but i'd rather understand whats going on here and how to handle this sort of situation properly.
Hopefully some code will make this clear
The Activity:
public class SomeFragment extends Fragment {
public static SomeFragment newInstance() {
return new SomeFragment();
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
someViewModel = ViewModelProviders.of(this).get(SomeViewModel.class);
//getting user details from previous activity
Intent intent = getActivity().getIntent();
if (intent != null){
if (intent.hasExtra(USER_ID)){
user = new User(intent.getStringExtra(USERNAME));
user.setId(intent.getLongExtra(USER_ID,0));
someViewModel.setUserLD(user);
}
}
someViewModel.getUserLD().observe(this, new Observer<User>() {
#Override
public void onChanged(#Nullable User userVal) {
user = userVal;
}
});
someViewModel.getGoalWeeklyLD().observe(this, new Observer<User>() {
#Override
public void onChanged(#Nullable User userVal) {
user = userVal;
}
});
//the below Observer calls throw an error because LiveData is null. makes sense.
//but how can i say "don't try and observe these until the transition.map has ran (because then it wont be null after if my understanding is right)" or something to that effect
someViewModel.getFirstLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgSportGradeVal) {
//Update UI
}
});
someViewModel.getSecondLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
someViewModel.getThriLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
someViewModel.getFourthLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
}}
The View Model:
public class SomeViewModel extends AndroidViewModel {
DaoRepository daoRepository;
MutableLiveData<User> userLD;
LiveData<XObject> firstLD;
LiveData<XObject> secondLD;
LiveData<XObject> thirdLD;
LiveData<XObject> fourthLD;
public MutableLiveData<User> getUserLD() {
return userLD;
}
public void setUserLD(User user){
userLD.setValue(user);
}
public LiveData<XObject> getFirstLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getSecondLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getThirdLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getForthLD(long userId) {
return goalWeeklyLD;
}
public SomeViewModel(#NonNull Application application) {
super(application);
daoRepository = new DaoRepository(application);
userLD = new MutableLiveData<>();
//so the first LiveData waits for the user to be populated before getting its LiveData becasue we need the userId for our Room query to run
firstLD = Transformations.map(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()).getValue());
//the remaining live data uses values from the first...
setupOtherTransformMaps(userLD.getValue())
}
public void setupOtherTransformMaps(long userId) {
//the secondLD, thirdLD and fourthLD all depends on values from the first (in runs a query that uses its dateExpired)
secondLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
thirdLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
fourthLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
}}
Thankfully Google was smart and created a component which lets you combine variable number of LiveData into a single LiveData, and only emit events when you choose to do so!
This is called MediatorLiveData.
In your case though, you only need to channel 1 LiveData (userLD) into 1 another LiveData, that will emit each time userLd has a new value.
So you can use a predefined MediatorLiveData that does exactly this, specifically Transformations.switchMap.
firstLD = Transformations.switchMap(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()));
EDIT: Yup, you seem to need to expose these LiveData separately from one another, but they all depend on the first query to execute.
So you need to replace Transformations.map { ...getValue() with Transformations.switchMap and you'll be good to go.
public SomeViewModel(#NonNull Application application) {
super(application);
CustomApplication app = (CustomApplication) application;
daoRepository = app.daoRepository();
userLD = new MutableLiveData<>();
firstLD = Transformations.switchMap(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()));
secondLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
thirdLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
fourthLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
}
Google's android architecture components tutorial here has a part that explains how to abstract the logic of getting data over the network. In it, they create an abstract class called NetworkBoundResource using LiveData to create a reactive stream as the basis for all reactive network requests.
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final AppExecutors appExecutors;
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
#MainThread
NetworkBoundResource(AppExecutors appExecutors) {
this.appExecutors = appExecutors;
result.setValue(Resource.loading(null));
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch()) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource, newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
result.addSource(dbSource, newData -> result.setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
//noinspection ConstantConditions
if (response.isSuccessful()) {
appExecutors.diskIO().execute(() -> {
saveCallResult(processResponse(response));
appExecutors.mainThread().execute(() ->
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)))
);
});
} else {
onFetchFailed();
result.addSource(dbSource,
newData -> result.setValue(Resource.error(response.errorMessage, newData)));
}
});
}
protected void onFetchFailed() {
}
public LiveData<Resource<ResultType>> asLiveData() {
return result;
}
#WorkerThread
protected RequestType processResponse(ApiResponse<RequestType> response) {
return response.body;
}
#WorkerThread
protected abstract void saveCallResult(#NonNull RequestType item);
#MainThread
protected abstract boolean shouldFetch();
#NonNull
#MainThread
protected abstract LiveData<ResultType> loadFromDb();
#NonNull
#MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
}
From What I understand, the logic of this class is to:
a) Create a MediatorLiveData called "result" as the main return object and set its initial value to Resource.loading(null)
b) Get the data from Android Room db as dbSource LiveData and add it to "result" as a source LiveData
c) On dbSource LiveData's first emission, remove the dbSource LiveData from "result" and call "shouldFetchFromNetwork()" which will
IF TRUE, call "fetchDataFromNetwork(dbSource)" which creates a network call through "createCall()" that returns a LiveData of the response encapsulated as an ApiResponse object
add back dbSource LiveData to "result" and on set emitted values to Resource.loading(data)
add apiResponce LiveData to "result" and on first emission remove dbSource and apiResponce LiveDatas
If apiResponse is successful, call "saveCallResult(processResponse(response))" and add back dbSource LiveData to "result" and set emitted values to Resource.success(newData)
If apiResponse failed, call "onFetchFailed()" and add back dbSource LiveData to "result" and set emitted values to Resource.error(response.errorMessage, newData))
IF FALSE, just add the dbSource LiveData to "result" and set emitted values to Resource.success(newData)
Given that this logic is the correct interpretation, I have tried to refactor this class to use RxJava Observables instead of LiveData. This is my attempt at a successful refactoring (I removed the initial Resource.loading(null) as I see this as superfluous).
public abstract class NetworkBoundResource<ResultType, RequestType> {
private Observable<Resource<ResultType>> result;
#MainThread
NetworkBoundResource() {
Observable<Resource<ResultType>> source;
if (shouldFetch()) {
source = createCall()
.subscribeOn(Schedulers.io())
.doOnNext(apiResponse -> saveCallResult(processResponse(apiResponse)))
.flatMap(apiResponse -> loadFromDb().toObservable().map(Resource::success))
.doOnError(t -> onFetchFailed())
.onErrorResumeNext(t -> {
return loadFromDb()
.toObservable()
.map(data -> Resource.error(t.getMessage(), data))
})
.observeOn(AndroidSchedulers.mainThread());
} else {
source = loadFromDb()
.toObservable()
.map(Resource::success);
}
result = Observable.concat(
loadFromDb()
.toObservable()
.map(Resource::loading)
.take(1),
source
);
}
public Observable<Resource<ResultType>> asObservable() {return result;}
protected void onFetchFailed() {}
#WorkerThread
protected RequestType processResponse(ApiResponse<RequestType> response) {return response.body;}
#WorkerThread
protected abstract void saveCallResult(#NonNull RequestType item);
#MainThread
protected abstract boolean shouldFetch();
#NonNull
#MainThread
protected abstract Flowable<ResultType> loadFromDb();
#NonNull
#MainThread
protected abstract Observable<ApiResponse<RequestType>> createCall();
}
As I am new to RxJava, my question is am I correctly refactoring to RxJava and maintaining the same logic as the LiveData version of this class?
public abstract class ApiRepositorySource<RawResponse extends BaseResponse, ResultType> {
// result is a Flowable because Room Database only returns Flowables
// Retrofit response will also be folded into the stream as a Flowable
private Flowable<ApiResource<ResultType>> result;
private AppDatabase appDatabase;
#MainThread
ApiRepositorySource(AppDatabase appDatabase) {
this.appDatabase = appDatabase;
Flowable<ApiResource<ResultType>> source;
if (shouldFetch()) {
source = createCall()
.doOnNext(this::saveCallResult)
.flatMap(apiResponse -> loadFromDb().toObservable().map(ApiResource::success))
.doOnError(this::onFetchFailed)
.onErrorResumeNext(t -> {
return loadFromDb()
.toObservable()
.map(data -> {
ApiResource apiResource;
if (t instanceof HttpException && ((HttpException) t).code() >= 400 && ((HttpException) t).code() < 500) {
apiResource = ApiResource.invalid(t.getMessage(), data);
} else {
apiResource = ApiResource.error(t.getMessage(), data);
}
return apiResource;
});
})
.toFlowable(BackpressureStrategy.LATEST);
} else {
source = loadFromDb()
.subscribeOn(Schedulers.io())
.map(ApiResource::success);
}
result = Flowable.concat(initLoadDb()
.map(ApiResource::loading)
.take(1),
source)
.subscribeOn(Schedulers.io());
}
public Observable<ApiResource<ResultType>> asObservable() {
return result.toObservable();
}
#SuppressWarnings("WeakerAccess")
protected void onFetchFailed(Throwable t) {
Timber.e(t);
}
#WorkerThread
protected void saveCallResult(#NonNull RawResult resultType) {
resultType.saveResponseToDb(appDatabase);
}
#MainThread
protected abstract boolean shouldFetch();
#NonNull
#MainThread
protected abstract Flowable<ResultType> loadFromDb();
#NonNull
#MainThread
protected abstract Observable<RawResult> createCall();
#NonNull
#MainThread
protected Flowable<ResultType> initLoadDb() {
return loadFromDb();
}
}
So here is what I have decided on using after many iterations. This is currently in production and is working well for my app. Here are some take away notes:
Create a BaseResponse interface
public interface BaseResponse {
void saveResponseToDb(AppDatabase appDatabase);
}
and implement it in all of your api response object classes. Doing this means you don't have to implement save_to_database logic in every ApiResource, you can just default it to what ever the response's implementation is, if you want.
I have chosen to handle Retrofit error responses in the onErrorResumeNext block for simplicity, but I recommend you create a Transformer class that can hold all this logic. In this case, I added an extra Status enum value for ApiResources called INVALID for 400-level responses.
You might be tempted to use the the Reactive Streams architecture component library for LiveData
implementation "android.arch.lifecycle:reactivestreams:$lifecycle_version" and add a method to this class called
public LiveData<ApiResource<ResultType>> asLiveData {
return LiveDataReactiveStreams.fromPublisher(result);
}
In theory, this would work perfectly as our ViewModels wouldn't have to convert Observable emissions to LiveData emissions or implement lifecycle logic for Observables in Views. Unfortunately, this stream gets rebuilt on every configuration change because it disposes of the LiveData in any onDestroy called (whether isFinishing is true or false). Thus, we have to manage the lifecycle of this stream, which defeats the purpose of using it in the first place, or have duplicated calls every time the device rotates.
Here is an example of a UserRepository creating an instance of an ApiNetworkResource
#Singleton
public class UserRepository {
private final RetrofitApi retrofitApi;
private final AppDatabase appDatabase;
#Inject
UserRepository(RetrofitApi retrofitApi, AppDatabase appDatabase) {
this.retrofitApi = retrofitApi;
this.appDatabase = appDatabase;
}
public Observable<ApiResource<User>> getUser(long userId) {
return new ApiRepositorySource<UserResponse, User>(appDatabase) {
#Override
protected boolean shouldFetch() {
return true;
}
#NonNull
#Override
protected Flowable<User> loadFromDb() {
return appDatabase.userDao().getUserFlowable(userId);
}
#NonNull
#Override
protected Observable<UserResponse> createCall() {
return retrofitApi.getUserById(userId);
}
}.asObservable();
}
}
I'm totally new to RxJava and I've spent all day understanding it, I'm tying to think how to solve this problem:
I have one object, fetched by Retrofit, it contains two ArrayLists, I have to process every ArrayList differently. Currently it looks like:
apiService.getUser(token).enqueue(new Callback<User>() {
#Override
public void onResponse(Response<User> response) {
final User user = response.body();
for (Skill s : user.getSkills()) {
// process here first ArrayList
}
for (OrganizerAction o : user.getOrganizerActions()) {
// process here second ArrayList
}
}
#Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
});
UPDATE:
public class User {
// fields
#SerializedName("organizer_actions")
#Expose
private List<OrganizerAction> mOrganizerActions;
#SerializedName("skills")
#Expose
private List<Skill> mSkills;
public List<OrganizerAction> getOrganizerActions() {
return mOrganizerActions;
}
public List<Skill> getSkills() {
return mSkills;
}
}
Thanks,
Anton
This answer is for Retrofit 2.0.0-beta, which is what you appear to be using. Also, you didn't give your POJO or service definitions, so going to use a general GitHub API example as a guide, modify to match your specify data.
First step is to convert your service definition to use Observable instead of Call.
public interface GitHubService {
#GET("/users/{user}")
Observable<User> getUser(#Path("user") String user);
}
Where User is
public class User {
public String login;
public int id;
}
Next, add a custom call adapter with to your retrofit builder with addCallAdapterFactory --
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
Get your service in the usual way --
GitHubService gitHubService = retrofit.create(GitHubService.class);
Next, get your observable and call cache on it to create an Observable that will replay the result. From that Observable, you can subscribe multiple times, in your case, you can subscribe twice. Once for each type of data you are interested in, and use the map function to transform from the User object to your specific fields. map allows you to apply function to the data in the observable. See the docs for more details. In this example, we will make two streams. One each for the id and login fields.
Observable<User> getUserResult = gitHubService.getUser("octocat").cache(1);
getUserResult.map(new Func1<User, Integer>() {
#Override
public Integer call(User user) {
return user.id;
}
}).subscribe(new Action1<Integer>() {
#Override
public void call(Integer id) {
Log.d("Stream 1", "id = " + id);
}
});
getUserResult.map(new Func1<User, String>() {
#Override
public String call(User user) {
return user.login;
}
}).subscribe(new Action1<String>() {
#Override
public void call(String login) {
Log.d("Stream 2", "login = " + login);
}
});
Finally, make sure your gradle file has the needed dependencies,
compile 'io.reactivex:rxjava:1.0.14'
compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta1'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
And, not directly related to your question, but if you are going to doing RxJava in Android, I recommend you checkout Retrolambda if you have not already. The above map and subscribe code, and Rx code in general, is more succinct with lambdas.
getUserResult.map(user -> user.id).subscribe(
id -> { Log.d("Stream 1", "id = " + id); }
);
getUserResult.map(user -> user.login).subscribe(
login -> { Log.d("Stream 2", "login = " + login); }
);