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

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().

Related

Recyclerview data disappears when device is rotated

Even though I am using ViewModel, whenever the device is rotated, the data in the Recyclerview disappears. I had to put the makeSearch() method inside the onClick() method because I need to get the text that the button grabs and use it as the search parameter. Is there a better way I can handle this to avoid this problem? My code is right here:
SearchActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// What happens when the search button is clicked
materialButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Objects.requireNonNull(textInputEditText.getText()).toString().isEmpty()) {
textInputEditText.setError("Type a search query");
} else {
mSearchInput = Objects.requireNonNull(textInputEditText.getText()).toString();
textInputEditText.setText("");
makeSearch();
}
}
});
}
// Gets the ViewModel, Observes the Question LiveData and delivers it to the Recyclerview
private void makeSearch() {
final SearchAdapter searchAdapter = new SearchAdapter();
SearchViewModel mSearchViewModel = new ViewModelProvider(this,
new CustomSearchViewModelFactory(new SearchRepository())).get(SearchViewModel.class);
mSearchViewModel.setQuery(mSearchInput);
mSearchViewModel.getQuestionLiveData().observe(this, new Observer<List<Question>>() {
#Override
public void onChanged(List<Question> questions) {
mQuestions = questions;
searchAdapter.setQuestions(questions);
}
});
mRecyclerView.setAdapter(searchAdapter);
searchAdapter.setOnClickListener(mOnClickListener);
}
SearchViewModel:
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<>();
private LiveData<List<Question>> mQuestionLiveData = Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
SearchViewModel(SearchRepository searchRepository) {
this.mSearchRepository = searchRepository;
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
}
SearchRepository:
public class SearchRepository {
//private String inTitle;
private MutableLiveData<List<Question>> mQuestions = new MutableLiveData<>();
public SearchRepository() {
//getQuestionsWithTextInTitle();
}
private void getQuestionsWithTextInTitle(String inTitle) {
ApiService apiService = RestApiClient.getApiService(ApiService.class);
Call<QuestionsResponse> call = apiService.getQuestionsWithTextInTitle(inTitle);
call.enqueue(new Callback<QuestionsResponse>() {
#Override
public void onResponse(Call<QuestionsResponse> call, Response<QuestionsResponse> response) {
QuestionsResponse questionsResponse = response.body();
if (questionsResponse != null) {
mQuestions.postValue(questionsResponse.getItems());
//shouldShowData = true;
} else {
Log.d("SearchRepository", "No matching question");
//shouldShowData = false;
}
}
#Override
public void onFailure(Call<QuestionsResponse> call, Throwable t) {
//shouldShowData = false;
t.printStackTrace();
}
});
}
public LiveData<List<Question>> getQuestions(String inTitle) {
getQuestionsWithTextInTitle(inTitle);
return mQuestions;
}
}
Your approach of passing the search input in through your CustomSearchViewModelFactory and into the constructor for the ViewModel and into the constructor for your SearchRepository isn't going to work in any case. While the first time you search your CustomSearchViewModelFactory creates the ViewModel, the second time you hit search, your SearchViewModel is already created and your factory is not invoked a second time, meaning you never get the second query.
Instead, you should file the ViewModel Overview documentation, and use Transformations.switchMap() to convert your input (the search string) into a new LiveData<List<Question>> for that given query.
This means that your ViewModel would look something like
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<String>();
private LiveData<List<Question>> mQuestionLiveData =
Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
public SearchViewModel() {
mSearchRepository = new SearchRepository();
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
}
You'd then update your Activity to:
Always observe the getQuestionLiveData() (note that you won't get a callback to your Observer until you actually set the first query)
Call setQuery() on your SearchViewModel in your makeSearch()
Remove your CustomSearchViewModelFactory entirely (it would no longer be needed).

call MutableLiveData web service agine to update the list

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

Not getting LiveData observable values within Fragments (ItemKeyedDataSource)

I am working with Firestore and successfully integrated it with Paging Library using ItemKeyedDataSource. Here is a gist:
public class MessageDataSource extends ItemKeyedDataSource<Query, Message> {
//... private members
MessageDataSource(Query query) {
mQuery = query;
}
#Override
public void loadInitial(#NonNull LoadInitialParams<Query> params, #NonNull LoadInitialCallback<Message> callback) {
mLoadStateObserver.postValue(LoadingState.LOADING);
mQuery.limit(params.requestedLoadSize).get()
.addOnCompleteListener(new OnLoadCompleteListener() {
#Override
protected void onSuccess(QuerySnapshot snapshots) {
getLastDocument(snapshots);
// I'm able to get the values here
List<Message> m = snapshots.toObjects(Message.class);
for (Message message : m) {
Log.d(TAG, "onSuccess() returned: " + message.getTitle());
}
callback.onResult(snapshots.toObjects(Message.class));
}
#Override
protected void onError(Exception e) {
Log.w(TAG, "loadInitial onError: " + e);
}
});
}
#Override
public void loadAfter(#NonNull LoadParams<Query> params, #NonNull LoadCallback<Message> callback) {
Log.d(TAG, "LoadingState: loading");
mLoadStateObserver.postValue(LoadingState.LOADING);
params.key.limit(params.requestedLoadSize).get()
.addOnCompleteListener(new OnLoadCompleteListener() {
#Override
protected void onSuccess(QuerySnapshot snapshots) {
getLastDocument(snapshots);
callback.onResult(snapshots.toObjects(Message.class));
}
#Override
protected void onError(Exception e) {
Log.w(TAG, "loadAfter onError: " + e);
}
});
}
private void getLastDocument(QuerySnapshot queryDocumentSnapshots) {
int lastDocumentPosition = queryDocumentSnapshots.size() - 1;
if (lastDocumentPosition >= 0) {
mLastDocument = queryDocumentSnapshots.getDocuments().get(lastDocumentPosition);
}
}
#Override
public void loadBefore(#NonNull LoadParams<Query> params, #NonNull LoadCallback<Message> callback) {}
#NonNull
#Override
public Query getKey(#NonNull Message item) {
return mQuery.startAfter(mLastDocument);
}
/*
* Public Getters
*/
public LiveData<LoadingState> getLoadState() {
return mLoadStateObserver;
}
/* Factory Class */
public static class Factory extends DataSource.Factory<Query, Message> {
private final Query mQuery;
private MutableLiveData<MessageDataSource> mSourceLiveData = new MutableLiveData<>();
public Factory(Query query) {
mQuery = query;
}
#Override
public DataSource<Query, Message> create() {
MessageDataSource itemKeyedDataSource = new MessageDataSource(mQuery);
mSourceLiveData.postValue(itemKeyedDataSource);
return itemKeyedDataSource;
}
public LiveData<MessageDataSource> getSourceLiveData() {
return mSourceLiveData;
}
}
}
And then within MessageViewModel class's constructor:
MessageViewModel() {
//... Init collections and query
// Init Paging
MessageDataSource.Factory mFactory = new MessageDataSource.Factory(query);
PagedList.Config config = new PagedList.Config.Builder()
.setPrefetchDistance(10)
.setPageSize(10)
.setEnablePlaceholders(false)
.build();
// Build Observables
mMessageObservable = new LivePagedListBuilder<>(mFactory, config)
.build();
mLoadStateObservable = Transformations.switchMap(mMessageObservable, pagedListInput -> {
// No result here
Log.d(TAG, "MessageViewModel: " + mMessageObservable.getValue());
MessageDataSource dataSource = (MessageDataSource) pagedListInput.getDataSource();
return dataSource.getLoadState();
});
}
Note the situation:
When I'm initializing the viewmodel in MainActivity#oncreate method and observing it, it is working as intended and is able to view it in recyclerview.
Later I decided to create a Fragment and refactored it by moving all the logic to the Fragment and when I try to observe the same livedata, no values are returned. Here's how I doing it.
Within Fragment:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
mViewModel = ViewModelProviders.of(getActivity()).get(MessageViewModel.class);
}
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//...
mViewModel.getMessageObserver().observe(this, messages -> {
Log.d(TAG, "onCreateView() returned: " + messages.size());
});
mViewModel.getLoadingStateObserver().observe(this, loadingState -> {
Log.d(TAG, "onCreateView() returned: " + loadingState.name());
});
return view;
}
The interesting part:
Within the Fragment the loadstate is returning the values LOADING and SUCCESS
Within MessageDataSource, values of the query is successfully returned but while observing the same in the Fragment, I get no values.
What am I doing wrong here?
P.S: I'm learning Android.
With fragments a few problems can occur. Observers are set in onActivityCreated() to ensure the view is created and change 'this' in the observe statement to 'getViewLifecycleOwner()'. This prevent observers from firing more than once after the fragment is popped of the backstack for example. You can read about it here.
So change your observer to:
mViewModel.getLoadingStateObserver().observe(getViewLifecycleOwner(), loadingState -> {
Log.d(TAG, "onCreateView() returned: " + loadingState.name());
});
The example code showed on Share data between fragments is bare minimal and just looking at it I got the wrong overview until I read this part very carefully:
These fragments can share a ViewModel using their activity scope to
handle this communication, as illustrated by the following sample
code:
So basically you have to initialize the viewmodel in the Activity: ViewModelProviders.of(this).get(SomeViewModel.class);
And then on the Activity's fragment you can initialize it as:
mViewModel = ViewModelProviders.of(getActivity()).get(SomeViewModel.class);
mViewModel.someMethod().observe(this, ref -> {
// do things
});
This is what I was doing wrong and now it's fixed.

Configuring RxJava to Send Data to activity from GCMListenerService

I am trying to send an update to my Activity from my GCMServiceListener so, I am using RxJava/RxAndroid And created a BusClass for handling sending and Observers
public class ClientBus {
//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();
}
}
And in my Application Class I did this to initialize the BusClass
private ClientBus clientBus;
public ClientBus getRxBusSingleton() {
if (clientBus == null) {
clientBus = new ClientBus();
}
return clientBus;
}
In the activity I want to receive the message, I registered a CompositeSubscription and get a reference to my ClientBus class from the Application Class
clientBus = ((MyApplication) getApplicationContext()).getRxBusSingleton();
#Override
protected void onStart() {
super.onStart();
initSubscriptions();
}
#Override
protected void onStop() {
super.onStop();
_subscriptions.unsubscribe();
}
void initSubscriptions() {
_subscriptions = new CompositeSubscription();
_subscriptions.add(clientBus.toObserverable().subscribe(new Action1<Object>() {
#Override
public void call(Object event) {
Log.e("New Event", "Event Received");
if (event instanceof MyGcmListenerService.Message) {
String msg = ((MyGcmListenerService.Message) event).getMessage();
if (msg.equals("Update Available")) {
scheduleArrayList = getSchedules();
scheduleAdapter = new ScheduleAdapter(getApplicationContext(), scheduleArrayList, ScheduledUberActivity.this);
scheduledList.setAdapter(scheduleAdapter);
scheduleAdapter.notifyDataSetChanged();
} else if (msg.equals("Refresh")) {
fetchTrips();
}
}
}
}));
}
And from the MyGcmListenerService class I did this when I get a new notification
private void sendRefreshNotif() {
if (clientBus.hasObservers()) {<--It enters the if cause the Log prints. But, the activity doesn't get the message
Log.e("Obervers", "Observers aren't null");
clientBus.send(new Message("Refresh"));
}
}
What I don't understand is why isn't it working here? I use it to interact between activities and fragments. I closed my application to check if the notification comes in, It'll enter this block if (clientBus.hasObservers()) { but it didn't and starting the app and testing the Observer, it notices there's an active Observer. Any help? Thanks.
It seems like you used different instances of the ClientBus class in CompositeSubscription and MyApplication.
Try to make a singleton from ClientBus class, it works fine for me.
public class ClientBus {
public ClientBus(SingletonAccessor accessor) {}
private static ClientBus instance;
private static class SingletonAccessor{}
public static ClientBus getInstance() {
if (instance == null) instance = new ClientBus(new SingletonAccessor());
return instance;
}
private final Subject<Object, Object> mBus = new SerializedSubject<>(PublishSubject.create());
public void send(Object o) {
mBus.onNext(o);
}
public Observable<Object> toObserverable() {
return mBus;
}
public boolean hasObservers() {
return mBus.hasObservers();
}
}

onDataChange not called

I'm trying to read from Firebase (offline), but the method onDataChange is never called,
private class MyAuthStateListener implements Firebase.AuthStateListener {
#Override
public void onAuthStateChanged(AuthData authData) {
if (authData != null) {
// user is logged in
userId = authData.getUid();
mFirebase = mFirebase.child(authData.getUid()).child("xxx");
Query mQuery = mFirebase.orderByChild("externalId").equalTo(externalId);
mQuery.addListenerForSingleValueEvent(new MyValueEventListener());
}
}
}
private class MyValueEventListener implements ValueEventListener {
#Override
public void onDataChange(DataSnapshot mDataSnapshot) {
if (mDataSnapshot.hasChildren()) {
Iterable<DataSnapshot> i = mDataSnapshot.getChildren();
Iterator<DataSnapshot> mIterator = i.iterator();
if (mIterator.hasNext()) {
mArrayList.clear();
}
while (mIterator.hasNext()) {
DataSnapshot c = mIterator.next();
mArrayList.add(c.getValue(mObject.class));
}
}
onTaskComplete(); // call the notifyDataSetChange() on the adapter.
}
#Override
public void onCancelled(FirebaseError firebaseError) {
}
}
Have I done something wrong?
Online it works well, but offline it won't works.
The new objects are created with a FAB that open a new activity (to modify the object) and than I return to that activity.
Thank you all for the help.

Categories

Resources