Android: How to show data in widget coming from Firestore? - android

I am trying to fetch data from a firestore collection and show it as a list in a widget, i am not too experienced on how widget logic works so i thought that there might be a function that invokes the widget view to be updated when the data is filled.
I basically fetch the information from a RemoteViewFactory class, but since the firebase functions are all asynchronous, i don't know how to make the widget to wait until the data is filled, or invoke a function within the class that would invoke the update.
Currently my class looks like this:
public class TaskDataProvider implements RemoteViewsService.RemoteViewsFactory {
private static final String TAG = TaskDataProvider.class.getSimpleName();
private static final int TOTAL = 10;
List<Task> tasks = new ArrayList<>();
private Context context;
public TaskDataProvider(Context context) {
this.context = context;
}
#Override
public void onCreate() {
this.loadData(value -> this.tasks = value);
}
#Override
public void onDataSetChanged() {
this.loadData(value -> this.tasks = value);
}
#Override
public void onDestroy() {
}
#Override
public int getCount() {
return this.tasks.size();
}
#Override
public RemoteViews getViewAt(int position) {
final RemoteViews remoteViews = new RemoteViews(this.context.getPackageName(), R.layout.widget_task_item);
final Task currentTask = this.tasks.get(position);
remoteViews.setTextViewText(R.id.widget_task_item_title, currentTask.getTitle());
remoteViews.setTextViewText(R.id.widget_task_item_description, currentTask.getDescription());
return remoteViews;
}
#Override
public RemoteViews getLoadingView() {
return null;
}
#Override
public int getViewTypeCount() {
return 1;
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public boolean hasStableIds() {
return true;
}
private void loadData(ResultListener<List<Task>> resultListener) {
FirebaseAuth auth = FirebaseAuth.getInstance();
FirebaseUser user = auth.getCurrentUser();
if (user != null) {
this.retrieveTasks(user.getUid(), resultListener);
}
}
private void retrieveTasks(String userId, ResultListener<List<Task>> resultListener) {
FirebaseFirestore firestore = FirebaseFirestore.getInstance();
firestore.collectionGroup(this.context.getString(R.string.database_project_task_collection_name))
.whereEqualTo(Task.FIELD_ASSIGNEE, userId)
.whereEqualTo(Task.FIELD_STATUS, TaskStatus.STATUS_OPEN)
.get()
.addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
Log.e(TAG, "Unable to retrieve task list", task.getException());
} else {
final QuerySnapshot result = task.getResult();
if (result != null) {
Log.d(TAG, String.format("Found tasks: %d", result.size()));
resultListener.onResult(result.toObjects(Task.class));
}
}
});
}
}
Both onCreate and onDataSetChanged are calling loadData which takes care of fetching the collection data from firestore, i know that at the time both onCreate and onDataSetChanged finished running the tasks property is still not filled and therefore nothing is filled in the list i am rendering in the widget.
How can i properly fetch information from firebase and then fill a ListView widget with that information?

Related

Android - How to access data from the Room database to the Stack Widget

I just learned about Android and here I have homework. I tried to make a stack widget which accesses a data from the database Room, here I try to access it but the data does not appear, I am confused about the solution and still do not understand
private Context context;
private List<MovieEntity> list = new ArrayList<>();
public StackRemoteViewsFactory(Context context) {
this.context = context;
}
#Override
public void onCreate() {
}
#Override
public void onDataSetChanged() {
new AsyncTask<Context, Void, List<MovieEntity>>() {
#Override
protected List<MovieEntity> doInBackground(Context... contexts) {
AppDatabase database = AppDatabase.getInstance(context);
list = database.favoriteDao().getMovies();
return list;
}
#Override
protected void onPostExecute(List<MovieEntity> movieEntities) {
super.onPostExecute(movieEntities);
}
}.execute(context);
}
#Override
public RemoteViews getViewAt(int position) {
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.item_widget);
for (MovieEntity movieEntity : list) {
Bitmap bmp = null;
try {
bmp = Glide.with(context)
.asBitmap()
.load("https://image.tmdb.org/t/p/w300_and_h450_bestv2" + movieEntity.getPosterPath())
.into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
} catch (Exception e) {
Log.d("ERROR", "error");
}
rv.setTextViewText(R.id.txt_item_title, movieEntity.getTitle());
rv.setImageViewBitmap(R.id.image_item_poster, bmp);
}
return rv;
}
#Override
public int getViewTypeCount() {
return 1;
}
The problem is that you are not using the movieEntities list in onPostExecute()
The way AsyncTask works is doInBackground() is for doing the actual background work (network access, database access, etc...).
Then, whatever you return from this method is passed to onPostExecute() which operates on the main thread and is where you should then use it.

LiveData query is not being run the second time I initialise it

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

Android callback won't run again after app onStop/OnResume

I have a class that runs an asynchronous call to Firestore. I've implemented an interface and callback so I can get the data outside of the class. The problem I'm having is that when I minimize/reopen the activity the callback stops receiving data. I tested the Firestore call itself, and data is definitely being retrieved. It just seems that the callback stops passing data from the Firestore get() to the Activity.
Here's my class:
public class FirebaseGetBooks {
//firebase objects
private FirebaseFirestore mDbase;
private Activity activity;
private String groupID;
//default constructor
public FirebaseGetBooks() {
}
public FirebaseGetBooks(Activity activity) {
this.activity = activity;
//firebase new instances
mDbase = FirebaseFirestore.getInstance();
FirebaseGetGroupID firebaseGetGroupID = new FirebaseGetGroupID(activity);
groupID = firebaseGetGroupID.getGroupID();
}
public interface FirestoreCallback {
void onCallback(List<Book> books);
}
public void readDataRTUpdate(final FirestoreCallback firestoreCallback) {
mDbase.collection("books").whereEqualTo("groupID", groupID)
.addSnapshotListener(activity, new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
if (value != null) {
int i = 0;
List<Book> books = new ArrayList<>();
for (QueryDocumentSnapshot document : value) {
books.add(document.toObject(Book.class));
Log.d(TAG, "Book: " + books.get(i).toString());
i++;
}
firestoreCallback.onCallback(books);
Log.d(TAG, "Document updated.");
}
else {
Log.d(TAG, "No such document");
}
}
});
}
}
And here's my callback as seen in my activity:
public class MainActivity extends AppCompatActivity {
private FirebaseGetbook firebaseGetBooks;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
firebaseGetBooks = new FirebaseGetBooks(this);
firebaseGetBooks.readDataRTUpdate(new FirebaseGetBooks.FirestoreCallback() {
#Override
public void onCallback(List<Book> books) {
Log.d(TAG, "Books Still Firing: " + books.toString());
}
});
}
}
any help/insight would be greatly appreciated.
Thanks!
You are using the activity-scoped form of addSnapshotListener(). The listener is automatically removed when the onStop() method of the activity passed as the first parameter is called.
If you want the listener to remain active when the activity is in the background, remove activity from the call to addSnapshotListener(). Otherwise, move your call of firebaseGetBooks.readDataRTUpdate() from onCreate() to onStart().

Paging Library invalidating data source not working

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

Load Large data from firebase into Viewpager

I have a situation when I have large set of data in firebase database and I want to show those data in ViewPager with Fragments.
The database and callbacks works fine but it clogging up the main thread when I try to add it to ViewPager and call notifyDataSetChanged().
Im using EventBus for communication between the Activity and Singleton class to handle add firebase related taks.
Here are some code snippets
Singleton Class
databaseProjectsRef.addChildEventListener(new ChildEventListener() {
public void onChildAdded(DataSnapshot snapshot, String s) {
// Get the project from the snapshot and add it to the UI
Project project = snapshot.getValue(Project.class);
project._id = snapshot.getKey();
add(project, ProjectType.NORMAL);
}
...
});
private void add(Project project, ProjectType projectType) {
if (!data.contains(project)) {
project.projectType = projectType;
data.add(project);
EventBus.getDefault().post(new RefreshDataEvent());
}
}
MainActivity
#Subscribe(threadMode = ThreadMode.MAIN)
public void onRefreshEvent(RefreshDataEvent event) {
if (mAdaptor != null) {
mAdaptor.setData();
}
}
Adaptor
public class ProjectAdaptor extends FragmentStatePagerAdapter {
private final EmptyInterface emptyInterface;
private List<Project> mData;
public ProjectAdaptor(FragmentManager fm, EmptyInterface emptyInterface) {
super(fm);
mData = new ArrayList<>();
this.emptyInterface = emptyInterface;
}
#Override
public Fragment getItem(int position) {
PagerFragment item = PagerFragment.newInstance(mData.get(position));
return item;
}
#Override
public int getCount() {
return mData.size();
}
public void setData() {
mData = ProjectContext.getInstance().getData();
emptyInterface.isEmpty(mData.isEmpty());
notifyDataSetChanged();
}
public interface EmptyInterface {
void isEmpty(boolean isEmpty);
}
Any help would be appreciated.
The most relevant question i could find was this but with no answers.

Categories

Resources