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;
}
}
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 have a list of different mines. Each mine has a list of blocks.
I have the mines in a spinner and the blocks in a recyclerview.
I want to display the different lists of blocks whenever the user changes the mine in the mine spinner
I am using Firebase in the backend as my database.
When I change the mine in the spinner, I update the block list by creating a new MutableLiveData which I've extended in a class called FirebaseQueryLiveData
The first time that I initialise the FirebaseQueryLiveData with the quesry containing the mine name, all the events inside it fire. However, after that, I call it and nothing fires. It breaks in the constructor if I have a breakpoint there, but it never reaches the run() method, onActive() method or the onDataChanged in the ValueEventListener.
I have done some research, and I have seen suggestions to replace the LiveData with MutableLiveData. I've done this, and it doesn't seem to make a difference.
Can anyone see anything in the code which I might be missing? I'm not very familiar with the android architecture components and I got the FirebaseQueryLiveData class from another helpful website with a tutorial, so I'm battling to understand where I have gone wrong.
I have done some research, and I have seen suggestions to replace the LiveData with MutableLiveData. I've done this, and it doesn't seem to make a difference.
public class BlockListActivityViewModel extends ViewModel {
private static DatabaseReference blockOutlineRef; // = FirebaseDatabase.getInstance().getReference(FireBasePaths.BLOCKOUTLINE.getPath("Therisa"));
private static DatabaseReference mineListRef;
private FirebaseQueryLiveData blockOutlineLiveDataQuery = null;
private LiveData<BlockOutlineList> blockOutlineLiveData = null;
private MediatorLiveData<String> selectedBlockNameMutableLiveData;
private MediatorLiveData<ArrayList<String>> mineListMutableLiveData;
public BlockListActivityViewModel() {
User loggedInUser = UserSingleton.getInstance();
setUpFirebasePersistance();
setupMineLiveData(loggedInUser);
// setupBlockOutlineListLiveData();
}
private void setupBlockOutlineListLiveData(String mineName) {
if (mineName != "") {
blockOutlineRef = FirebaseDatabase.getInstance().getReference(FireBasePaths.BLOCKOUTLINE.getPath(mineName));
blockOutlineLiveDataQuery = new FirebaseQueryLiveData(blockOutlineRef);
blockOutlineLiveData = Transformations.map(blockOutlineLiveDataQuery, new BlockOutlineHashMapDeserialiser());
}
}
private void setupMineLiveData(User user) {
ArrayList<String> mineNames = new ArrayList<>();
if (user != null) {
if (user.getWriteMines() != null) {
for (String mineName : user.getWriteMines().values()) {
mineNames.add(mineName);
}
}
}
setMineListMutableLiveData(mineNames);
if (mineNames.size() > 0) {
updateMineLiveData(mineNames.get(0));
}
}
public void updateMineLiveData(String mineName) {
SelectedMineSingleton.setMineName(mineName);
setupBlockOutlineListLiveData(SelectedMineSingleton.getInstance());
}
public void setUpFirebasePersistance() {
int i = 0;
// FirebaseDatabase.getInstance().setPersistenceEnabled(true);
}
private MutableLiveData<NamedBlockOutline> selectedBlockOutlineMutableLiveData;
public MutableLiveData<NamedBlockOutline> getSelectedBlockOutlineMutableLiveData() {
if (selectedBlockOutlineMutableLiveData == null) {
selectedBlockOutlineMutableLiveData = new MutableLiveData<>();
}
return selectedBlockOutlineMutableLiveData;
}
public void setSelectedBlockOutlineMutableLiveData(NamedBlockOutline namedBlockOutline) {
getSelectedBlockOutlineMutableLiveData().postValue(namedBlockOutline);
}
public MediatorLiveData<String> getSelectedBlockNameMutableLiveData() {
if (selectedBlockNameMutableLiveData == null)
selectedBlockNameMutableLiveData = new MediatorLiveData<>();
return selectedBlockNameMutableLiveData;
}
public void setSelectedBlockNameMutableLiveData(String blockName) {
selectedBlockNameMutableLiveData.postValue(blockName);
}
public MediatorLiveData<ArrayList<String>> getMineListMutableLiveData() {
if (mineListMutableLiveData == null)
mineListMutableLiveData = new MediatorLiveData<>();
return mineListMutableLiveData;
}
public void setMineListMutableLiveData(ArrayList<String> mineListString) {
getMineListMutableLiveData().postValue(mineListString);
}
private class BlockOutlineHashMapDeserialiser implements Function<DataSnapshot, BlockOutlineList>, android.arch.core.util.Function<DataSnapshot, BlockOutlineList> {
#Override
public BlockOutlineList apply(DataSnapshot dataSnapshot) {
BlockOutlineList blockOutlineList = new BlockOutlineList();
HashMap<String, NamedBlockOutline> blockOutlineStringHashMap = new HashMap<>();
for (DataSnapshot childData : dataSnapshot.getChildren()) {
NamedBlockOutline thisNamedOutline = new NamedBlockOutline();
HashMap<String, Object> blockOutlinePointHeader = (HashMap<String, Object>) childData.getValue();
HashMap<String, BlockPoint> blockOutlinePoints = (HashMap<String, BlockPoint>) blockOutlinePointHeader.get("blockOutlinePoints");
thisNamedOutline.setBlockName(childData.getKey());
thisNamedOutline.setBlockOutlinePoints(blockOutlinePoints);
blockOutlineStringHashMap.put(childData.getKey(), thisNamedOutline);
}
blockOutlineList.setBlockOutlineHashMap(blockOutlineStringHashMap);
return blockOutlineList;
}
}
#NonNull
public LiveData<BlockOutlineList> getBlockOutlineLiveData() {
return blockOutlineLiveData;
}
}
LiveData
public class FirebaseQueryLiveData extends MutableLiveData<DataSnapshot> {
private static final String LOG_TAG = "FirebaseQueryLiveData";
private final Query query;
private final MyValueEventListener listener = new MyValueEventListener();
private boolean listenerRemovePending = false;
private final Handler handler = new Handler();
public FirebaseQueryLiveData(Query query) {
this.query = query;
}
public FirebaseQueryLiveData(DatabaseReference ref) {
this.query = ref;
}
private final Runnable removeListener = new Runnable() {
#Override
public void run() {
query.removeEventListener(listener);
listenerRemovePending = false;
Log.d(LOG_TAG, "run");
}
};
#Override
protected void onActive() {
super.onActive();
if (listenerRemovePending) {
handler.removeCallbacks(removeListener);
Log.d(LOG_TAG, "listenerRemovePending");
}
else {
query.addValueEventListener(listener);
Log.d(LOG_TAG, "addValueEventListener");
}
listenerRemovePending = false;
Log.d(LOG_TAG, "listenerRemovePending");
}
#Override
protected void onInactive() {
super.onInactive();
// Listener removal is schedule on a two second delay
handler.postDelayed(removeListener, 4000);
listenerRemovePending = true;
Log.d(LOG_TAG, "listenerRemovePending");
}
private class MyValueEventListener implements ValueEventListener {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
setValue(dataSnapshot);
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.e(LOG_TAG, "Can't listen to query " + query, databaseError.toException());
}
}
}
I have the following table with a PrimaryKey in it. I have inserted some values in the table. Now I need to update a particular value in a particular row. I have a row with gameType as Puzzle and I need to update the currentLevel in the row. But I am not able to achieve that.
GamesDetails table:
public class GamesDetail extends RealmObject {
#PrimaryKey
private String gameType;
private int currentLevel;
private int totalLevel;
private int totalCoins;
private int currentBadge;
public String getGameType() {
return gameType;
}
public void setGameType(String gameType) {
this.gameType = gameType;
}
public int getCurrentLevel() {
return currentLevel;
}
public void setCurrentLevel(int currentLevel) {
this.currentLevel = currentLevel;
}
public int getTotalLevel() {
return totalLevel;
}
public void setTotalLevel(int totalLevel) {
this.totalLevel = totalLevel;
}
public int getTotalCoins() {
return totalCoins;
}
public void setTotalCoins(int totalCoins) {
this.totalCoins = totalCoins;
}
public int getCurrentBadge() {
return currentBadge;
}
public void setCurrentBadge(int currentBadge) {
this.currentBadge = currentBadge;
}
}
Here is what I have tried to update a particular row in the table:
final GamesDetail puzzleGameDetail = realm.where(GamesDetail.class).equalTo("gameType","Puzzle").findFirst();
final int[] nextLevel = {puzzleGameDetail.getCurrentLevel()};
realm.executeTransactionAsync(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
puzzleGameDetail.setCurrentLevel(++nextLevel[0]);
realm.copyToRealmOrUpdate(puzzleGameDetail);
}
}, new Realm.Transaction.OnSuccess() {
#Override
public void onSuccess() {
Log.e(TAG, "Done");
}
}, new Realm.Transaction.OnError() {
#Override
public void onError(Throwable error) {
Log.e(TAG,error.getMessage());
}
});
But the value is not getting updated and I am getting this following error:
Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
How can I update a particular value in a particular row in the table ?
When calling executeTransactionAsync, the execute block will run in a background thread, any Realm objects access from that thread need to be created/queried on that thread from the Realm instance which is the param of execute.
Move your finding GamesDetail query inside execute block and rest will work fine.
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;
}
i'm new in Rx programming (and I'm having a lot of fun so far ^^).
I'm trying to transform a AsyncTask call into an Rx function.
My function :
Get all the installed apps
normalize the labels
sort everything alphabetically
arrange them by group of letter (it was a Multimap(letter, list of apps)) and pass the result to an adapter to display everything.
Here is how I'm doing so far with Rx :
Observable.from(getInstalledApps(getActivity(), false))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.map(new Func1<ResolvedActivityInfoWrapper, ResolvedActivityInfoWrapper>() {
#Override
public ResolvedActivityInfoWrapper call(ResolvedActivityInfoWrapper act) {
// Normalize labels
act.setLabel(Normalizer.normalize(act.getLabel(getPackageManager()).replace(String.valueOf((char) 160), "").trim(), Normalizer.Form.NFD).replaceAll("\\p{M}", ""));
return act;
}
})
.toList()
.subscribe(new Observer<List<ResolvedActivityInfoWrapper>>() {
List<ResolvedActivityInfoWrapper> list;
#Override
public void onCompleted() {
Observable.from(list).groupBy(new Func1<ResolvedActivityInfoWrapper, String>() {
#Override
public String call(ResolvedActivityInfoWrapper input) {
//Get groups by letter
String label = input.getLabel(getPackageManager());
if (!TextUtils.isEmpty(label)) {
String firstChar = label.substring(0, 1);
if (pattern.matcher(firstChar).matches()) {
return firstChar.toUpperCase();
}
}
return "#";
}
}).subscribe(this); // implementation below
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(List<ResolvedActivityInfoWrapper> list) {
Collections.sort(list, new Comparator<ActivityInfoWrapper>() {
#Override
// Sort all the apps in the list, not sure it's a good way to do it
public int compare(ActivityInfoWrapper info1, ActivityInfoWrapper info2) {
return info1.getLabel(getPackageManager()).compareToIgnoreCase(info2.getLabel(getPackageManager()));
}
});
this.list = list;
}
});
Once I groupedBy letters, on complete I subscribe with this :
#Override
public void onCompleted() {
//display the apps
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(GroupedObservable<String, ResolvedActivityInfoWrapper> input) {
//For each list of apps by letter i subscribe with an observer that will handle those apps (observer code below)
input.subscribe(new TestObserver(input.getKey()));
}
Observer :
private class TestObserver implements Observer<ResolvedActivityInfoWrapper> {
List<ResolvedActivityInfoWrapper> list;
String letter;
public TestObserver(String letter) {
list = new ArrayList<>();
this.letter = letter;
}
#Override
public void onCompleted() {
adapter.addData(letter, list);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(ResolvedActivityInfoWrapper input) {
list.add(input);
}
}
Everything works correctly excpets for one problem : the observer's onCompleted are called not in the right order. So I got all my apps, sorted by letter, but the groups are nots displayed in the right order (C first, then Y, then M etc ...).
I guess there are plenty of errors in the code, can you help me with this probleme and maybe understanding how all this works please ?
Thanks
UPDATE :
Following the advices in the commentary section (thanks people), here is what I'm trying after normalizing the labels :
Observable.from(list).groupBy(new Func1<ResolvedActivityInfoWrapper, String>() {
#Override
public String call(ResolvedActivityInfoWrapper input) {
String label = input.getLabel(getPackageManager());
if (!TextUtils.isEmpty(label)) {
String firstChar = label.substring(0, 1);
if (pattern.matcher(firstChar).matches()) {
return firstChar.toUpperCase();
}
}
return "#";
}
})
.toSortedList(new Func2<GroupedObservable<String, ResolvedActivityInfoWrapper>, GroupedObservable<String, ResolvedActivityInfoWrapper>, Integer>() {
#Override
public Integer call(GroupedObservable<String, ResolvedActivityInfoWrapper> obs1, GroupedObservable<String, ResolvedActivityInfoWrapper> obs2) {
return obs1.getKey().compareToIgnoreCase(obs2.getKey());
}
})
.subscribe(new Observer<List<GroupedObservable<String, ResolvedActivityInfoWrapper>>>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(List<GroupedObservable<String, ResolvedActivityInfoWrapper>> input) {
String test = input.get(0).getKey();
}
});
But it never goes into the Compare function.