I'm new to Espresso testing. In my existing application we are using RxAndroid to do some networking. We use a RxBus to communicate to parts of our application that would otherwise seem "impossible".
We imported RxEspresso which implements IdlingResource so we could use our RxAndroid network calls.
Unfortunately RxEspresso does not allow RxBus to work since it's a "hot observable" and never closes. So it throws android.support.test.espresso.IdlingResourceTimeoutException: Wait for [RxIdlingResource] to become idle timed out
I made a small Android application demonstrating my point.
It has two activities. The first displays some items retrieved through a network call on startup in a RecyclerView.
When clicked upon it communicates through the RxBus (I know it's overkill, but purely to demonstrate the point). The DetailActivity then shows the data.
How can we edit RxEspresso so it will work with our RxBus?
RxIdlingResource also check RxEspresso
/**
* Provides the hooks for both RxJava and Espresso so that Espresso knows when to wait
* until RxJava subscriptions have completed.
*/
public final class RxIdlingResource extends RxJavaObservableExecutionHook implements IdlingResource {
public static final String TAG = "RxIdlingResource";
static LogLevel LOG_LEVEL = NONE;
private final AtomicInteger subscriptions = new AtomicInteger(0);
private static RxIdlingResource INSTANCE;
private ResourceCallback resourceCallback;
private RxIdlingResource() {
//private
}
public static RxIdlingResource get() {
if (INSTANCE == null) {
INSTANCE = new RxIdlingResource();
Espresso.registerIdlingResources(INSTANCE);
}
return INSTANCE;
}
/* ======================== */
/* IdlingResource Overrides */
/* ======================== */
#Override
public String getName() {
return TAG;
}
#Override
public boolean isIdleNow() {
int activeSubscriptionCount = subscriptions.get();
boolean isIdle = activeSubscriptionCount == 0;
if (LOG_LEVEL.atOrAbove(DEBUG)) {
Log.d(TAG, "activeSubscriptionCount: " + activeSubscriptionCount);
Log.d(TAG, "isIdleNow: " + isIdle);
}
return isIdle;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
if (LOG_LEVEL.atOrAbove(DEBUG)) {
Log.d(TAG, "registerIdleTransitionCallback");
}
this.resourceCallback = resourceCallback;
}
/* ======================================= */
/* RxJavaObservableExecutionHook Overrides */
/* ======================================= */
#Override
public <T> Observable.OnSubscribe<T> onSubscribeStart(Observable<? extends T> observableInstance,
final Observable.OnSubscribe<T> onSubscribe) {
int activeSubscriptionCount = subscriptions.incrementAndGet();
if (LOG_LEVEL.atOrAbove(DEBUG)) {
if (LOG_LEVEL.atOrAbove(VERBOSE)) {
Log.d(TAG, onSubscribe + " - onSubscribeStart: " + activeSubscriptionCount, new Throwable());
} else {
Log.d(TAG, onSubscribe + " - onSubscribeStart: " + activeSubscriptionCount);
}
}
onSubscribe.call(new Subscriber<T>() {
#Override
public void onCompleted() {
onFinally(onSubscribe, "onCompleted");
}
#Override
public void onError(Throwable e) {
onFinally(onSubscribe, "onError");
}
#Override
public void onNext(T t) {
//nothing
}
});
return onSubscribe;
}
private <T> void onFinally(Observable.OnSubscribe<T> onSubscribe, final String finalizeCaller) {
int activeSubscriptionCount = subscriptions.decrementAndGet();
if (LOG_LEVEL.atOrAbove(DEBUG)) {
Log.d(TAG, onSubscribe + " - " + finalizeCaller + ": " + activeSubscriptionCount);
}
if (activeSubscriptionCount == 0) {
Log.d(TAG, "onTransitionToIdle");
resourceCallback.onTransitionToIdle();
}
}
}
RxBus
public class RxBus {
//private final PublishSubject<Object> _bus = PublishSubject.create();
// If multiple threads are going to emit events to this
// then it must be made thread-safe like this instead
private final Subject<Object, Object> _bus = new SerializedSubject<>(PublishSubject.create());
public void send(Object o) {
_bus.onNext(o);
}
public Observable<Object> toObserverable() {
return _bus;
}
public boolean hasObservers() {
return _bus.hasObservers();
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
#Bind(R.id.rv)
RecyclerView RV;
private List<NewsItem> newsItems;
private RecyclerViewAdapter adapter;
private Observable<List<NewsItem>> newsItemsObservable;
private CompositeSubscription subscriptions = new CompositeSubscription();
private RxBus rxBus;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
//Subscribe to RxBus
rxBus = new RxBus();
subscriptions.add(rxBus.toObserverable()
.subscribe(new Action1<Object>() {
#Override
public void call(Object event) {
//2.
NewsItem myClickNewsItem = (NewsItem) event;
startActivity(new Intent(MainActivity.this, DetailActivity.class).putExtra("text", myClickNewsItem.getBodyText()));
}
}));
//Set the adapter
adapter = new RecyclerViewAdapter(this);
//Set onClickListener on the list
ItemClickSupport.addTo(RV).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
#Override
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
//Send the clicked item over the RxBus.
//Receives it in 2.
rxBus.send(newsItems.get(position));
}
});
RV.setLayoutManager(new LinearLayoutManager(this));
RestAdapter retrofit = new RestAdapter.Builder()
.setEndpoint("http://URL.com/json")
.build();
ServiceAPI api = retrofit.create(ServiceAPI.class);
newsItemsObservable = api.listNewsItems(); //onComplete goes to setNewsItems
}
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
NewsItemObserver observer = new NewsItemObserver(this);
newsItemsObservable.delaySubscription(1, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io()).subscribe(observer);
}
public void setNewsItems(List<NewsItem> newsItems) {
this.newsItems = newsItems;
adapter.setNewsItems(newsItems);
RV.setAdapter(adapter);
}
Since we didn't obtain any better answer to this problem we assumed objects send through the RxBus were immediate and didn't need to be counted in the subscriptions.incrementAndGet();
We simply filtered the objects out before this line. In our case the objects were of the class SerializedSubject and PublishSubject.
Here is the method we changed.
#Override
public <T> Observable.OnSubscribe<T> onSubscribeStart(Observable<? extends T> observableInstance, final Observable.OnSubscribe<T> onSubscribe) {
int activeSubscriptionCount = 0;
if (observableInstance instanceof SerializedSubject || observableInstance instanceof PublishSubject) {
Log.d(TAG, "Observable we won't register: " + observableInstance.toString());
} else {
activeSubscriptionCount = subscriptions.incrementAndGet();
}
if (LOG_LEVEL.atOrAbove(DEBUG)) {
if (LOG_LEVEL.atOrAbove(VERBOSE)) {
Log.d(TAG, onSubscribe + " - onSubscribeStart: " + activeSubscriptionCount, new Throwable());
} else {
Log.d(TAG, onSubscribe + " - onSubscribeStart: " + activeSubscriptionCount);
}
}
onSubscribe.call(new Subscriber<T>() {
#Override
public void onCompleted() {
onFinally(onSubscribe, "onCompleted");
}
#Override
public void onError(Throwable e) {
onFinally(onSubscribe, "onError");
}
#Override
public void onNext(T t) {
Log.d(TAG, "onNext:: " + t.toString());
//nothing
}
});
return onSubscribe;
}
Related
I am using Android Architecture Components (Model-View-ViewModel) to get the results from geoQuery.addGeoQueryDataEventListener() from GeoFirestore
implementation 'com.github.imperiumlabs:GeoFirestore-Android:v1.5.0'
Each event has its own callback and I am interested in all of them.
I am also using BottomNavigationView what means that I only have one activity and all my logic is placed in the fragments.
I started off by implementing onDocumentEntered() as shown below and I realized that when I navigate to next activity and get back to the previous one where MVVM is been called, the recyclerView momentaneously duplicates the data.
public class FirestoreGeoQuery extends LiveData<List<StoreModel>> {
private static final String TAG = "debinf FBGeoQuery";
private GeoQuery geoQuery;
private Class clazz;
private List<StoreModel> itemList = new ArrayList<>();
public FirestoreGeoQuery(GeoQuery geoQuery, Class clazz) {
this.geoQuery = geoQuery;
this.clazz = clazz;
}
GeoQueryDataEventListener geoQueryDataEventListener = new GeoQueryDataEventListener() {
#Override
public void onDocumentEntered(#NotNull DocumentSnapshot documentSnapshot, #NotNull GeoPoint geoPoint) {
Log.i(TAG, "onDocumentEntered: "+documentSnapshot);
if (documentSnapshot.exists()) {
StoreModel item = (StoreModel) documentSnapshot.toObject(clazz);
item.setGeoPoint(geoPoint);
Log.i(TAG, "onDocumentEntered: addGeoQueryDataEventListener - store.name: "+item.getName()+", address: "+item.getAddress()+", geoPoint: "+item.getGeoPoint());
itemList.add(item);
}
}
#Override
public void onDocumentExited(#NotNull DocumentSnapshot documentSnapshot) {
Log.i(TAG, "onDocumentExited: ");
}
#Override
public void onDocumentMoved(#NotNull DocumentSnapshot documentSnapshot, #NotNull GeoPoint geoPoint) {
Log.i(TAG, "onDocumentMoved: ");
}
#Override
public void onDocumentChanged(#NotNull DocumentSnapshot documentSnapshot, #NotNull GeoPoint geoPoint) {
Log.i(TAG, "onDocumentChanged: ");
}
#Override
public void onGeoQueryReady() {
Log.i(TAG, "onGeoQueryReady: ");
setValue(itemList);
}
#Override
public void onGeoQueryError(#NotNull Exception e) {
Log.i(TAG, "onGeoQueryError: ");
}
};
#Override
protected void onActive() {
super.onActive();
geoQuery.addGeoQueryDataEventListener(geoQueryDataEventListener);
}
#Override
protected void onInactive() {
super.onInactive();
if (!hasActiveObservers()) {
geoQuery.removeGeoQueryEventListener(geoQueryDataEventListener);
}
}
}
So my question is: How to properly handle the results from each event callback?
I appreciate any help!
You can setup a custom class to wrap the data and the status of the event listener and observe it within a LiveData. You can also handle the listener in the ViewModel itself so that it's only added once, and cleared once the ViewModel is cleared. Storing data in your ViewModel is also a good idea if you want it to persist throughout the lifecycle of your Activity.
ViewModel
private MutableLiveData<Resource<StoreModel>> data;
private Resource resource;
private List<StoreModel> itemList = new ArrayList<>();
public MyViewModel() {
resource = new Resource();
data = getData();
GeoQueryDataEventListener geoQueryDataEventListener = new GeoQueryDataEventListener() {
#Override
public void onDocumentEntered(#NotNull DocumentSnapshot documentSnapshot, #NotNull GeoPoint geoPoint) {
Log.i(TAG, "onDocumentEntered: "+documentSnapshot);
if (documentSnapshot.exists()) {
StoreModel item = (StoreModel) documentSnapshot.toObject(clazz);
item.setGeoPoint(geoPoint);
data.postValue(resource.onDocumentEntered(item));
}
}
#Override
public void onDocumentExited(#NotNull DocumentSnapshot documentSnapshot) {
Log.i(TAG, "onDocumentExited: ");
data.postValue(resource.onDocumentExited(...));
}
#Override
public void onDocumentMoved(#NotNull DocumentSnapshot documentSnapshot, #NotNull GeoPoint geoPoint) {
Log.i(TAG, "onDocumentMoved: ");
data.postValue(resource.onDocumentMoved(...));
}
#Override
public void onDocumentChanged(#NotNull DocumentSnapshot documentSnapshot, #NotNull GeoPoint geoPoint) {
Log.i(TAG, "onDocumentChanged: ");
data.postValue(resource.onDocumentChanged(...));
}
#Override
public void onGeoQueryReady() {
Log.i(TAG, "onGeoQueryReady: ");
data.postValue(resource.onGeoQueryReady(...));
}
#Override
public void onGeoQueryError(#NotNull Exception e) {
Log.i(TAG, "onGeoQueryError: ");
data.postValue(resource.onGeoQueryError(...));
}
geoQuery.addGeoQueryDataEventListener(geoQueryDataEventListener);
}
#Override
protected void onCleared() {
geoQuery.removeGeoQueryEventListener(geoQueryDataEventListener);
}
public MutableLiveData<Resource<StoreModel>> getData() {
if (data == null) {
data = new MutableLiveData<Resource<StoreModel>>();
}
return data;
}
Resource
class Resource {
private Status currentStatus;
private StoreModel item;
public Resource() { }
public Resource onDocumentEntered(StoreModel item) {
this.status = Status.ON_DOCUMENT_ENTERED;
this.item = item;
return this;
}
.... handle other functions
}
Status
enum Status {
ON_DOCUMENT_ENTERED,
ON_DOCUMENT_EXITED,
....
}
And you can initialize & observe it in your Fragment as follows:
#Override
protected void onStart() {
// initialize existing data
if (viewModel.getItemList().size() > 0) {
updateUI(data);
}
// observe listener events
observer = new Observer<Resource<List<String>>>() {
#Override
public void onChanged(#Nullable final Resource<List<String>> resource) {
switch(resource.getCurrentStatus()) {
case Status.ON_DOCUMENT_ENTERED:
viewModel.getItemList().add(resource.getItem());
....
break;
case Status.ON_DOCUMENT_EXITED:
...
break;
...
}
}
};
viewModel.getData().observe(this, observer);
}
#Override
protected void onStop() {
viewModel.getData().removeObserver(observer);
}
I use MVVM structure in my project.
I have the main fragment with list observed a web service as you can see in the code
fragment :
mViewModel.getHomePageList().observe(this, homeDataWrapper -> {
if (homeDataWrapper!=null) {
if (homeDataWrapper.isStatus()) {
binding.homeProgressBar.setVisibility(View.INVISIBLE);
ToastUtil.showTosat(homeDataWrapper.getData().getMessage(), getContext());
Log.d(TAG, "onChanged: ");
}
}
});
view model:
ublic class HomePageViewModel extends AndroidViewModel {
private MutableLiveData<DataWrapper<Home>> data;
public ObservableInt loading;
private HomeRepository homeRepository;
private HomePageAdapter adapter;
public HomePageViewModel(#NonNull Application application) {
super(application);
}
public void init() {
adapter = new HomePageAdapter(R.layout.main_page_list, this);
homeRepository = new HomeRepository();
if (this.data != null) {
// ViewModel is created per Fragment so
// we know the userId won't change
return;
}
data = homeRepository.getHomeScreen();
}
public HomePageAdapter getAdapter() {
return adapter;
}
public void onItemClick(Integer index) {
}
public void onSerachClicked(View view) {
Navigation.findNavController(view).navigate(R.id.action_homePageFragment_to_searchActivity);
}
public MutableLiveData<DataWrapper<Home>> getHomePageList() {
return this.data;
}
}
HomeRepository :
public MutableLiveData<DataWrapper<Home>> getHomeScreen() {
final MutableLiveData<DataWrapper<Home>> homeMutableLiveData = new MutableLiveData<>();
final DataWrapper<Home> dataWrapper = new DataWrapper<>();
RetrofitInstance.getApiService().getHome().enqueue(new Callback<Home>() {
#Override
public void onResponse(#NotNull Call<Home> call, #NotNull Response<Home> response) {
Log.d("", "onResponse: " + response);
if (response.code() == 200) {
dataWrapper.setData(response.body());
dataWrapper.setStatus(true);
homeMutableLiveData.postValue(dataWrapper);
}
}
#Override
public void onFailure(Call<Home> call, Throwable t) {
Log.d("", "onResponse: " + t);
dataWrapper.setApiException((Exception) t);
dataWrapper.setStatus(false);
homeMutableLiveData.postValue(dataWrapper);
}
});
return homeMutableLiveData;
}
I would like to add SwipeRefreshLayout to update the main list. what is the correct way to call the web service again and update the list?
can anyone help me?
You can just call getHomeScreen form your Repository class to trigger data pulling from the server again, after pulling request completed, the observers will be notified using the the MutableLiveData.
But here is your issue, you are creating a new MutableLiveData object each time you call getHomeScreen. Thus, the first one will not be notified and the list will not be updated!
To solve the problem you have to initialize your MutableLiveData somewhere else so it will not be created again every time you call getHomeScreen.
I suggest you make your HomeRepository class a singleton class and initialize the MutableLiveData object inside the constructor, then you can use this object to post data to observers once you got new data from the server.
public class HomeRepository {
private static HomeRepository instance;
private MutableLiveData<DataWrapper<Home>> homeMutableLiveData;
public static HomeRepository getInstance() {
if(instance == null) instance = new HomeRepository();
return instance;
}
private HomeRepository() {
homeMutableLiveData = new MutableLiveData<>();
}
public MutableLiveData<DataWrapper<Home>> getHomeScreen() {
final DataWrapper<Home> dataWrapper = new DataWrapper<>();
RetrofitInstance.getApiService().getHome().enqueue(new Callback<Home>() {
#Override
public void onResponse(#NotNull Call<Home> call, #NotNull Response<Home> response) {
Log.d("", "onResponse: " + response);
if (response.code() == 200) {
dataWrapper.setData(response.body());
dataWrapper.setStatus(true);
homeMutableLiveData.postValue(dataWrapper);
}
}
#Override
public void onFailure(Call<Home> call, Throwable t) {
Log.d("", "onResponse: " + t);
dataWrapper.setApiException((Exception) t);
dataWrapper.setStatus(false);
homeMutableLiveData.postValue(dataWrapper);
}
});
return homeMutableLiveData;
}
}
Inside onRefereshListener of fragment
swifeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
mViewModel.getHomeScreenDetail();
}
});
In Viewmodel create getHomeScreenDetail method
public void getHomeScreenDetail(){
data = homeRepository.getHomeScreen();
}
This is my first attempt at working with the Google Lifecycle Architecture component with Fragments. My app uses MVP with a BaseViewModel for persisting the Presenter across configuration changes.
In my Activities, all Activity lifecycle events are called for the Presenter as expected courtesy of the Lifecycle component; however, none of the Fragment Presenters' lifecycle events are called.
Code Time!
BaseViewModel.java
public final class BaseViewModel<V extends BaseContract.View, P extends BaseContract.Presenter<V>>
extends ViewModel {
private P presenter;
private static final String TAG = "[" + BaseViewModel.class.getSimpleName() + "] ";
void setPresenter(P presenter) {
Log.d(APP_TAG, TAG + "setPresenter presenter: " + presenter);
if (this.presenter == null) {
this.presenter = presenter;
}
}
P getPresenter() {
Log.d(APP_TAG, TAG + "getPresenter presenter: " + presenter);
return this.presenter;
}
#Override
protected void onCleared() {
Log.d(APP_TAG, TAG + "onCleared ");
super.onCleared();
presenter.onPresenterDestroy();
presenter = null;
}
}
BaseFragment.java
public abstract class BaseFragment<V extends BaseContract.View, R extends BaseContract.Repository, P extends BaseContract.Presenter<V>>
extends Fragment implements BaseContract.View {
private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
protected P presenter;
#SuppressWarnings("unchecked")
#CallSuper
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
//Initializes the ViewModel
BaseViewModel<V, P> viewModel = ViewModelProviders.of(this).get(BaseViewModel.class);
boolean isPresenterCreated = false;
if (viewModel.getPresenter() == null) {
viewModel.setPresenter(initPresenter());
isPresenterCreated = true;
}
presenter = viewModel.getPresenter();
presenter.attachLifecycle(getLifecycle());
presenter.attachView((V) this);
presenter.setRepo(initRepository());
if (isPresenterCreated) {
presenter.onPresenterCreated();
} else {
//Hacky workaround I don't want to use
presenter.onPresenterRestored();
}
}
#Override
public LifecycleRegistry getLifecycle() {
return lifecycleRegistry;
}
#CallSuper
#Override
public void onDestroyView() {
super.onDestroyView();
presenter.detachLifecycle(getLifecycle());
presenter.detachView();
}
protected abstract P initPresenter();
protected abstract R initRepository();
}
BasePresenter.java
public abstract class BasePresenter<V extends BaseContract.View> implements LifecycleObserver, BaseContract.Presenter<V> {
private Bundle stateBundle;
private V view;
private BaseContract.Repository repo;
#Override
final public V getView() {
return view;
}
#SuppressWarnings("unchecked")
#Override
final public BaseContract.Repository getRepo() {
return repo;
}
#Override
final public void attachLifecycle(Lifecycle lifecycle) {
lifecycle.addObserver(this);
}
#Override
final public void detachLifecycle(Lifecycle lifecycle) {
lifecycle.removeObserver(this);
}
#Override
final public <R extends BaseContract.Repository> void setRepo(R repo) {
this.repo = repo;
}
#Override
final public void attachView(V view) {
this.view = view;
}
#Override
final public void detachView() {
view = null;
}
#Override
final public boolean isViewAttached() {
return view != null;
}
#Override
final public Bundle getStateBundle() {
return stateBundle == null
? stateBundle = new Bundle()
: stateBundle;
}
#CallSuper
#Override
public void onPresenterDestroy() {
if (stateBundle != null && !stateBundle.isEmpty()) {
stateBundle.clear();
}
}
#Override
public void onPresenterCreated() {
Log.d(APP_TAG, "BasePresenter.OnPresenterCreated");
}
#Override
public void onPresenterRestored() {
//Hacky
Log.d(APP_TAG, "BasePresenter.onPresenterRestored");
}
#OnLifecycleEvent(value = Lifecycle.Event.ON_START)
void onStart() {
if (isViewAttached()) {
onStartWithAttachedView();
}
}
public void onStartWithAttachedView() {
}
}
The Fragment I am testing:
GameSituationsFragment.java
public class GameSituationsFragment extends BaseFragment<GameSituationsContract.View,
GameSituationsContract.Repository, GameSituationsContract.Presenter>
implements GameSituationsContract.View {
#BindView(R.id.gs_prev_session_button)
ImageView prevSessButton;
#BindView(R.id.gs_next_session_button)
ImageView nextSessButton;
#BindView(R.id.gs_selected_session)
TextView selectedSessionText;
private static final String TAG = "[" + GameSituationsFragment.class.getSimpleName() + "] ";
private InteractionListener interactionListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) return;
if (activity instanceof GameSituationsFragment.InteractionListener) {
interactionListener = (GameSituationsFragment.InteractionListener) activity;
} else {
if (BuildConfig.DEBUG) {
throw new RuntimeException(activity.toString()
+ " must implement GameSituationsFragment.InteractionListener");
} else {
//Todo what do we want to do?
Log.e(TAG, activity.toString() + " must implement GameSituationsFragment.InteractionListener");
}
}
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof GameSituationsFragment.InteractionListener) {
interactionListener = (GameSituationsFragment.InteractionListener) context;
} else {
if (BuildConfig.DEBUG) {
throw new RuntimeException(context.toString()
+ " must implement GameSituationsFragment.InteractionListener");
} else {
//Todo what do we want to do?
Log.e(TAG, context.toString() + " must implement GameSituationsFragment.InteractionListener");
}
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_game_situations, container, false);
ButterKnife.bind(this, view);
return view;
}
#Override
protected GameSituationsContract.Presenter initPresenter() {
return new GameSituationsPresenter();
}
#Override
protected GameSituationsContract.Repository initRepository() {
return new GameSituationsRepository();
}
public interface InteractionListener{
void onGsNextSessionClicked();
void onGsPreviousSessionClicked();
}
}
And finally the Fragment's Presenter:
GameSituationsPresenter.java
public class GameSituationsPresenter extends BasePresenter<GameSituationsContract.View> implements GameSituationsContract.Presenter{
GameSituationsRepository repo;
private Bundle viewStateBundle = getStateBundle();
private static final String TAG = "[" + GameSituationsPresenter.class.getSimpleName() + "] ";
#Override
public void onPresenterCreated(){
Log.d(APP_TAG, TAG + "onPresenterCreated");
repo = (GameSituationsRepository) getRepo();
...
initializeViews();
}
#Override
public void onPresenterRestored() {
//So hacky
initializeViews();
}
//NOT CALLED
#OnLifecycleEvent(value = Lifecycle.Event.ON_START)
protected void onStart(){
Log.d(APP_TAG, TAG + "onStart");
if (isViewAttached()) {
}
}
//NOT CALLED
#OnLifecycleEvent(value = Lifecycle.Event.ON_STOP)
protected void onStop(){
if (isViewAttached()) {
}
}
//NOT CALLED
#OnLifecycleEvent(value = Lifecycle.Event.ON_DESTROY)
protected void onDestroy() {
if (isViewAttached()) {
//Something to do?
}
//a little cleanup
repo = null;
}
private void initializeViews(){
//Do view stuff
}
}
So the question is are fragments able to receive Lifecycle Events or did I miss something in my implementation?
Thanks in advance.
Recently I was trying this:
I have a list of jobs backed by data source (I am using paging library) and each item in job list is having a save button, and that save button updates the status of the job from unsaved to saved (or vice versa) in database and once updated it invalidates the DataSource, now that invalidation should cause reload for the current page immediately, but that isn't happening.
I checked values in database they actually get updated but that isn't the case with the UI.
Code:
public class JobsPagedListProvider {
private JobListDataSource<JobListItemEntity> mJobListDataSource;
public JobsPagedListProvider(JobsRepository jobsRepository) {
mJobListDataSource = new JobListDataSource<>(jobsRepository);
}
public LivePagedListProvider<Integer, JobListItemEntity> jobList() {
return new LivePagedListProvider<Integer, JobListItemEntity>() {
#Override
protected DataSource<Integer, JobListItemEntity> createDataSource() {
return mJobListDataSource;
}
};
}
public void setQueryFilter(String query) {
mJobListDataSource.setQuery(query);
}
}
Here is my custom datasource:
public class JobListDataSource<T> extends TiledDataSource<T> {
private final JobsRepository mJobsRepository;
private final InvalidationTracker.Observer mObserver;
String query = "";
#Inject
public JobListDataSource(JobsRepository jobsRepository) {
mJobsRepository = jobsRepository;
mJobsRepository.setJobListDataSource(this);
mObserver = new InvalidationTracker.Observer(JobListItemEntity.TABLE_NAME) {
#Override
public void onInvalidated(#NonNull Set<String> tables) {
invalidate();
}
};
jobsRepository.addInvalidationTracker(mObserver);
}
#Override
public boolean isInvalid() {
mJobsRepository.refreshVersionSync();
return super.isInvalid();
}
#Override
public int countItems() {
return DataSource.COUNT_UNDEFINED;
}
#Override
public List<T> loadRange(int startPosition, int count) {
return (List<T>) mJobsRepository.getJobs(query, startPosition, count);
}
public void setQuery(String query) {
this.query = query;
}
}
Here is the code in JobsRepository that updates job from unsaved to saved:
public void saveJob(JobListItemEntity entity) {
Completable.fromCallable(() -> {
JobListItemEntity newJob = new JobListItemEntity(entity);
newJob.isSaved = true;
mJobDao.insert(newJob);
Timber.d("updating entity from " + entity.isSaved + " to "
+ newJob.isSaved); //this gets printed in log
//insertion in db is happening as expected but UI is not receiving new list
mJobListDataSource.invalidate();
return null;
}).subscribeOn(Schedulers.newThread()).subscribe();
}
Here is the Diffing logic for job list:
private static final DiffCallback<JobListItemEntity> DIFF_CALLBACK = new DiffCallback<JobListItemEntity>() {
#Override
public boolean areItemsTheSame(#NonNull JobListItemEntity oldItem, #NonNull JobListItemEntity newItem) {
return oldItem.jobID == newItem.jobID;
}
#Override
public boolean areContentsTheSame(#NonNull JobListItemEntity oldItem, #NonNull JobListItemEntity newItem) {
Timber.d(oldItem.isSaved + " comp with" + newItem.isSaved);
return oldItem.jobID == newItem.jobID
&& oldItem.jobTitle.compareTo(newItem.jobTitle) == 0
&& oldItem.isSaved == newItem.isSaved;
}
};
JobListDataSource in JobRepository (only relevant portion is mentioned below):
public class JobsRepository {
//holds an instance of datasource
private JobListDataSource mJobListDataSource;
//setter
public void setJobListDataSource(JobListDataSource jobListDataSource) {
mJobListDataSource = jobListDataSource;
}
}
getJobs() in JobsRepository:
public List<JobListItemEntity> getJobs(String query, int startPosition, int count) {
if (!isJobListInit) {
Observable<JobList> jobListObservable = mApiService.getOpenJobList(
mRequestJobList.setPageNo(startPosition/count + 1)
.setMaxResults(count)
.setSearchKeyword(query));
List<JobListItemEntity> jobs = mJobDao.getJobsLimitOffset(count, startPosition);
//make a synchronous network call since we have no data in db to return
if(jobs.size() == 0) {
JobList jobList = jobListObservable.blockingSingle();
updateJobList(jobList, startPosition);
} else {
//make an async call and return cached version meanwhile
jobListObservable.subscribe(new Observer<JobList>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(JobList jobList) {
updateJobList(jobList, startPosition);
}
#Override
public void onError(Throwable e) {
Timber.e(e);
}
#Override
public void onComplete() {
}
});
}
}
return mJobDao.getJobsLimitOffset(count, startPosition);
}
updateJobList in jobsRepository:
private void updateJobList(JobList jobList, int startPosition) {
JobListItemEntity[] jobs = jobList.getJobsData();
mJobDao.insert(jobs);
mJobListDataSource.invalidate();
}
After reading the source code of DataSource I realized this:
A DataSource once invalidated will never become valid again.
invalidate() says: If invalidate has already been called, this method does nothing.
I was actually having a singleton of my custom DataSource (JobListDataSource) provided by JobsPagedListProvider, so when I was invalidating my DataSource in saveJob() (defined in JobsRepository), it was trying to get new DataSource instance (to fetch latest data by again calling loadRange() - that's how refreshing a DataSource works)
but since my DataSource was singleton and it was already invalid so no loadRange() query was being made!
So make sure you don't have a singleton of DataSource and invalidate your DataSource either manually (by calling invalidate()) or have a InvalidationTracker in your DataSource's constructor.
So the final solution goes like this:
Don't have a singleton in JobsPagedListProvider:
public class JobsPagedListProvider {
private JobListDataSource<JobListItemEntity> mJobListDataSource;
private final JobsRepository mJobsRepository;
public JobsPagedListProvider(JobsRepository jobsRepository) {
mJobsRepository = jobsRepository;
}
public LivePagedListProvider<Integer, JobListItemEntity> jobList() {
return new LivePagedListProvider<Integer, JobListItemEntity>() {
#Override
protected DataSource<Integer, JobListItemEntity> createDataSource() {
//always return a new instance, because if DataSource gets invalidated a new instance will be required(that's how refreshing a DataSource works)
mJobListDataSource = new JobListDataSource<>(mJobsRepository);
return mJobListDataSource;
}
};
}
public void setQueryFilter(String query) {
mJobListDataSource.setQuery(query);
}
}
Also make sure if you're fetching data from network you need to have right logic to check whether data is stale before querying the network else it will requery everytime the DataSource gets invalidated.
I solved it by having a insertedAt field in JobEntity which keeps track of when this item was inserted in DB and checking if it is stale in getJobs() of JobsRepository.
Here is the code for getJobs():
public List<JobListItemEntity> getJobs(String query, int startPosition, int count) {
Observable<JobList> jobListObservable = mApiService.getOpenJobList(
mRequestJobList.setPageNo(startPosition / count + 1)
.setMaxResults(count)
.setSearchKeyword(query));
List<JobListItemEntity> jobs = mJobDao.getJobsLimitOffset(count, startPosition);
//no data in db, make a synchronous call to network to get the data
if (jobs.size() == 0) {
JobList jobList = jobListObservable.blockingSingle();
updateJobList(jobList, startPosition, false);
} else if (shouldRefetchJobList(jobs)) {
//data available in db, so show a cached version and make async network call to update data
jobListObservable.subscribe(new Observer<JobList>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(JobList jobList) {
updateJobList(jobList, startPosition, true);
}
#Override
public void onError(Throwable e) {
Timber.e(e);
}
#Override
public void onComplete() {
}
});
}
return mJobDao.getJobsLimitOffset(count, startPosition);
}
Finally remove InvalidationTracker in JobListDatasource as we are handling invalidation manually:
public class JobListDataSource<T> extends TiledDataSource<T> {
private final JobsRepository mJobsRepository;
String query = "";
public JobListDataSource(JobsRepository jobsRepository) {
mJobsRepository = jobsRepository;
mJobsRepository.setJobListDataSource(this);
}
#Override
public int countItems() {
return DataSource.COUNT_UNDEFINED;
}
#Override
public List<T> loadRange(int startPosition, int count) {
return (List<T>) mJobsRepository.getJobs(query, startPosition, count);
}
public void setQuery(String query) {
this.query = query;
}
}
I have an Activity1 with a TabLayout of two Fragments (each of them with a presenter). Once I click a button on the Toolbar a new Activity2 is started (with startActivityWithResults) which contains a simple list. At the selection of one of the items in the list the Activity2 returns the selected string to the previous Activity1 (the one with the TabLayout).
Now, once onActivityResult is called in Activity1, this one will call an API (using a presenter) that will get the new results and then it should update the two fragments in the TabLayout. I'm thinking to do it with RxJava but I have no idea where to start from.
The Activity1:
public class Activity1 extends BaseActivity {
#Inject
Actvity1Presenter mPresenter;
public static Intent newIntent(Context packageContext) {
return new Intent(packageContext, Activity1.class);
}
#LayoutRes
protected int getLayoutRedIs() {
return R.layout.app_bar_activity1;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutRedIs());
FwApplication.component(this).inject(this);
mPresenter.attachView(this);
Toolbar tb = (Toolbar) findViewById(R.id.toolbar_chips);
setSupportActionBar(tb);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_back_arrow);
mTabLayout = (TabLayout) findViewById(R.id.tab_layout);
mTabLayout.addTab(mTabLayout.newTab().setText("TAB1"));
mTabLayout.addTab(mTabLayout.newTab().setText("TAB2"));
mTabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
mViewPager = (ViewPager) findViewById(R.id.viewpager);
mViewPager.setAdapter(new PagerAdapter(getSupportFragmentManager(),
mTabLayout.getTabCount()));
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout));
mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
#Override
public void onTabSelected(TabLayout.Tab tab) {
mViewPager.setCurrentItem(tab.getPosition());
}
#Override
public void onTabUnselected(TabLayout.Tab tab) {
}
#Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PICK_ITEM_CODE) {
if (resultCode == RESULT_OK) {
mPresenter.updateResults(data);
}
if (resultCode == RESULT_CANCELED) {
}
}
}
And the pager:
public class PagerAdapter extends FragmentPagerAdapter {
int mNumOfTabs;
public PagerAdapter(FragmentManager fm, int NumOfTabs) {
super(fm);
this.mNumOfTabs = NumOfTabs;
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return Fragment1.newInstance();
break;
case 1:
return Fragment2.newInstance();
break;
}
}
#Override
public int getCount() {
return mNumOfTabs;
}
}
EDIT
ActivityPresenter:
public class ActivityPresenter implements Presenter<ActivityView>,
Interactor.OnFinishedListener<Response> {
private static final String TAG = "FW.ActivityPresenter";
#Inject
QueryPreferences mQueryPreferences;
private Interactor mInteractor;
private ActivityView mView;
private NetworkService mNetworkService;
private boolean mUseCache;
private String mQuery;
private int mPage;
private PublishSubject<Response> mPublishSubject = PublishSubject.create();
Observable<Response> getObservableResults() {
return mPublishSubject;
}
#Inject
public ActivityPresenter(NetworkService networkService) {
mNetworkService = networkService;
mInteractor = new InteractorImpl(mNetworkService);
}
public void onSearchQueryListener(String query, int page) {
mQuery = mQueryPreferences.getStoredQuery();
mUseCache = query.equals(mQuery);
if (!mUseCache) {
mQueryPreferences.setStoredQuery(query);
Log.d(TAG, "Query added to cache: " + query);
}
mPage = page;
mInteractor.loadResults(this, query, false, page);
}
#Override
public void onFinished(Response response) {
if (mView != null) {
mPublishSubject.onNext(response);
}
}
#Override
public void onError(Throwable throwable) {
if (mView != null) {
mView.showMessage(throwable.getMessage());
}
}
#Override
public void attachView(ActivityView mvpView) {
mView = mvpView;
}
#Override
public void detachView() {
mView = null;
mInteractor.unSubscribe();
}
}
InteractorImpl:
public class InteractorImpl implements Interactor {
private static final String TAG = "FW.InteractorImpl";
private NetworkService mNetworkService;
private Subscription mSubscription;
public InteractorImpl(NetworkService networkService) {
mNetworkService = networkService;
}
#Override
public void loadResults(final OnFinishedListener listener, String query, boolean useCache, int page) {
Observable<Response> responseObservable = (Observable<Response>)
mNetworkService.getObservable(mNetworkService.getAPI().getResponseObservable(query, page), Response.class, true, useCache);
mSubscription = responseObservable.subscribe(new Observer<Response>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
Log.e(TAG, e.getMessage());
listener.onError(e);
}
#Override
public void onNext(Response response) {
listener.onFinished(response);
}
});
}
public void unSubscribe() {
if(mSubscription != null && !mSubscription.isUnsubscribed()) {
mSubscription.unsubscribe();
}
}
}
FragmentPresenter:
public class FragmentPresenter implements Presenter<FragmentView>,
Interactor.OnFinishedListener<Response> {
private static final String TAG = "FW.FragmentPres";
#Inject
QueryPreferences mQueryPreferences;
private Interactor mInteractor;
private FragmentView mView;
private NetworkService mNetworkService;
private ActivityPresenter mActvityPresenter;
#Inject
public FragmentPresenter(NetworkService networkService) {
mNetworkService = networkService;
mInteractor = new InteractorImpl(mNetworkService);
}
void attachRecipeActivityPresenter(ActivityPresenter activityPresenter) {
mActvityPresenter = activityPresenter;
mActvityPresenter.getObservableResults().subscribe(data -> showData(data));
}
private void showData(Response response) {
if (response.getResults().getModels().isEmpty() && mPage == 0) {
mView.showNoResults();
} else {
mView.showResults(response.getResults().getModels());
}
}
#Override
public void onError(Throwable throwable) {
if (mView != null) {
mView.hideProgressBar();
mView.showMessage(throwable.getMessage());
}
}
#Override
public void attachView(FragmentView mvpView) {
mView = mvpView;
}
#Override
public void detachView() {
mView = null;
mInteractor.unSubscribe();
}
}
Using Retrofit2 and RxAndroid your method will look like this:
public void updateResults(String data) {
yourRetrofitAPI.getSomething(data)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(() -> {
// show your progress dialog
})
.subscribe(result -> {
// pass result to your view
}, error -> {
// hide your progress dialog
// get error message and send to your view
}, () -> {
// hide your progress dialog
});
}
interface YourRetrofitAPI {
#GET("/yourResource/{data}")
Observable<String> getSomething(#Path("data") String data);
}
So, about notify your fragments, with MVP you can make presenter fragments observe a stream from activity presenter, so both fragments will be notified when you query ends.
public class ExampleUnitTest {
#Test
public void testSample() throws Exception {
ActivityPresenter activityPresenter = new ActivityPresenter();
Fragment1Presenter fragment1Presenter = new Fragment1Presenter();
Fragment2Presenter fragment2Presenter = new Fragment2Presenter();
fragment1Presenter.attachActivityPresenter(activityPresenter);
fragment2Presenter.attachActivityPresenter(activityPresenter);
Observable.range(1, 10)
.delay(2, TimeUnit.SECONDS, Schedulers.immediate())
.subscribe(integer -> activityPresenter.queryData("query: " + integer));
}
class ActivityPresenter {
PublishSubject<String> publishSubject = PublishSubject.create();
Observable<String> serverDataAsObservable() {
return publishSubject.map(s -> String.format("%d - %s", System.currentTimeMillis(), s));
}
void queryData(String input) {
// based on your input you should query data from server
// and then emit those data with publish subject
// then all subscribers will receive changes
publishSubject.onNext(input);
}
}
class Fragment1Presenter {
private ActivityPresenter activityPresenter;
void attachActivityPresenter(ActivityPresenter activityPresenter) {
this.activityPresenter = activityPresenter;
this.activityPresenter.serverDataAsObservable()
.subscribe(data -> showData(data));
}
private void showData(String data) {
System.out.println("showing data on fragment1 with " + data);
}
}
class Fragment2Presenter {
private ActivityPresenter activityPresenter;
void attachActivityPresenter(ActivityPresenter activityPresenter) {
this.activityPresenter = activityPresenter;
this.activityPresenter.serverDataAsObservable()
.subscribe(data -> showData(data));
}
private void showData(String data) {
System.out.println("showing data on fragment2 with " + data);
}
}
}
Hope that it helps.
Best regards.