I have an Android app which uses firestore as its database. I have followed this series of blog posts to set up my firestore database in my app : https://firebase.googleblog.com/2017/12/using-android-architecture-components.html and then followed this stackoverflow entry to change my code to work for firestore: Android Architecture Components with Firebase specifically Firestore.
After this I was successful to display the result of my query in a recycler view, however when I added the swap to update (I do soft delete by setting a isActive flag to false) action in my app, LiveData was inconsistent in refreshing the RecyclerView. Here is my code snippets:
MainActivity.java
TaskViewModel viewModel =
ViewModelProviders.of(this).get(TaskViewModel.class);
LiveData<LinkedList<TaskProperties>> liveData = viewModel.getTaskPropertiesLiveData();
final MainActivity mainActivityReference = this;
liveData.observe(this, new Observer<LinkedList<TaskProperties>>() {
#Override
public void onChanged(#Nullable LinkedList<TaskProperties> taskProperties) {
if (taskProperties != null) {
// Get a handle to the RecyclerView.
mRecyclerView = findViewById(R.id.recyclerview);
// Create an adapter and supply the data to be displayed.
mAdapter = new TaskListAdapter(mainActivityReference, taskProperties);
// Connect the adapter with the RecyclerView.
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mAdapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(mRecyclerView);
mRecyclerView.setAdapter(mAdapter);
// Give the RecyclerView a default layout manager.
mRecyclerView.setLayoutManager(new LinearLayoutManager(mainActivityReference));
}
}
});
View Model:
public class TaskViewModel extends ViewModel {
private LinkedList<TaskProperties> taskProperties;
private static final Query PROJECT_REF = FirebaseFirestore.getInstance().collection("project").whereEqualTo("active", true);
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(PROJECT_REF);
public TaskViewModel() {
taskPropertiesLiveData.addSource(liveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable final QuerySnapshot querySnapshot) {
if (querySnapshot != null) {
new Thread(new Runnable() {
#Override
public void run() {
taskProperties = new LinkedList<TaskProperties>();
for (DocumentSnapshot document : querySnapshot.getDocuments()) {
taskProperties.addLast(document.toObject(TaskProperties.class));
}
taskPropertiesLiveData.postValue(taskProperties);
}
}).start();
} else {
taskPropertiesLiveData.setValue(null);
}
}
});
}
#NonNull
public LiveData<LinkedList<TaskProperties>> getTaskPropertiesLiveData() {
return taskPropertiesLiveData;
}
}
Code in the callback class to remove :
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
}
Constructor in Adapter:-
public TaskListAdapter(Context context,LinkedList<TaskProperties> taskList) {
mInflater = LayoutInflater.from(context);
this.taskList = taskList;
}
Code in Adapter to remove:-
public void onItemDismiss(int position) {
TaskDao taskDao = new TaskDao();
taskDao.softDeleteTaskInDB(taskList.get(position));
}
Code in DAO class to update( soft delete) :-
public void softDeleteTaskInDB(TaskProperties taskProperties){
taskProperties.setActive(false);
database.collection("project")
.document(taskProperties.getTask())
.set(taskProperties).
addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(DEBUG_TAG, "DocumentSnapshot successfully written!");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(DEBUG_TAG, "Error writing document", e);
}
});
Log.i(DEBUG_TAG,taskProperties.getTask());
}
I have observed that LiveData was able to refresh the view when I was deleting one component from the end of the list, however when I deleted from the middle of the list the view sometimes does not refresh properly. From the logs I found that the position that is being passed into the adapter class is working fine, however the tasklist array does not have the most updated value.
For example if the task list contains :-
Cat
Dog
Mouse
Rabbit
Tiger
and if delete Mouse and then Rabbit in quick succession, the onItemDismiss in adapter class receives position 3 in both cases, but the taskList variable in the Adapter class still contains Mouse at position 3. This means the LiveData might not have refreshed the RecyclerView.
Can someone please tell me where am I going wrong?
Thanks,
Sangho
Related
hello i have two problems regarding live data with view model and navigation component first one is when i go from fragment A with live data to fragment B and then from B to A the data in my list gets duplicated,
the other problem is one i re call viewModel.loadList() in my fragment after making and event to filter the data also gets duplicated
here is my view model
public class HomeViewModel extends ViewModel {
MutableLiveData<ArrayList<HomeResponseModel>> homeLiveData = new MutableLiveData<>();
ArrayList<HomeResponseModel> homeList = new ArrayList<>();
public MutableLiveData<ArrayList<HomeResponseModel>> geHomeList(HomeRequestModel homeRequestModel, Context context, ApiInterface apiInterface, LottieAnimationView lottieAnimationView) {
if (homeLiveData == null) {
homeLiveData = new MutableLiveData<ArrayList<HomeResponseModel>>();
loadHomeList(homeRequestModel);
}
return homeLiveData;
}
public void loadHomeList(HomeRequestModel homeRequestModel) {
Call<List<HomeResponseModel>> call = apiInterface.getHomeList(homeRequestModel, );
call.enqueue(new Callback<List<HomeResponseModel>>() {
#Override
public void onResponse(Call<List<HomeResponseModel>> call, Response<List<HomeResponseModel>> response) {
if (response.isSuccessful()) {
homeList.addAll(response.body());
homeLiveData.setValue(homeList);
}
}
#Override
public void onFailure(Call<List<HomeResponseModel>> call, Throwable t) {
}
});
}
my observer in onCreateView
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeResponseModels.addAll(homeResponse);
homeAdapter.notifyDataSetChanged();
}
});
}
how i recall load method after a filter event
viewModel.loadHomeList(homeRequestModel);
Clear the list before adding the new models:
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeResponseModels.clear();
homeResponseModels.addAll(homeResponse);
homeAdapter.notifyDataSetChanged();
}
});
}
Or, even better:
In case your adapter holds a List or HomeResponseModel you could create a method to update it:
public update(List<HomeResponseModel> homeResponse) {
this.homeResponseModels = homeResponse;
notifydatasetchanged();
}
and then change the observe method to call it:
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeAdapter.update(homeResponse);
}
});
}
Besides that, in your ViewModel in the loadHomeList method in the onResponse callback you could assign the data received to the liveData:
if (response.isSuccessful()) {
homeLiveData.setValue(response.body());
}
no need to save it in the homeList var, you can get rid of that var. Otherwise, perform homeList.clear(); before adding to it all the received data to avoid duplicates.
I use RxJava in my Android project, I want to retrieve all entities User from a table using RxJava asynchronously and return the list List to an adapter myAdapter(Context context, List<User> users).
But now I can only have Single<User>, how can I get the list directly and put it into my adapter ?
My code:
// here vm.getUserList() should return a List<User>
MyAdapter adapter = new MyAdapter(context, vm.getUserList());
...
And in my vm:
public Single<List<User>> getUserList() {
return Single.fromCallable(() -> myDatabaseRepository.getUsers());
}
You only need to call the users method and handle the success/error in the corresponding method.
getUserList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(new Consumer<List<User>>() {
#Override
public void accept(List<User> users) throws Exception {
// fill adapter
MyAdapter adapter = new MyAdapter(context, users);
}
})
.doOnError(new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
// handle error
}
});
Yes, this is the normal way of retrieving the data from the database/network. You expose from the repository the Observables/Singles, then you subscribe to them and receive the data in the success/error methods to show in the UI to the user.
private void fetchUser() {
('function returning Single<List<User>>').subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onSuccess, this::onFail);
}
private void onSuccess(List<User> users) {
//handle user model here
}
private void onFail(Throwable t) {
//handle error here
}
This question already has answers here:
How to return DataSnapshot value as a result of a method?
(6 answers)
Closed 4 years ago.
Hello I am using Firebase database for my android app.I have my data stored on firebase database and I want to get it whenever the fragment gets created but when I start my app the method(which gets data from firebase database) returns null
and whenever I resume the fragment with the android device the recyclerView gets populated with the data from firebase.
My question is why the list returns null first time. and How can I get data whenever the fragment gets created
OffersFragment
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View offersView = inflater.inflate(R.layout.fragment_offers, container, false);
mOffersRecyclerView = offersView.findViewById(R.id.rv_offers);
mAddCard = offersView.findViewById(R.id.fab_add_card);
setUp();
return offersView;
}
#Override
public void setPresenter(OffersContract.Presenter presenter) {
mPresenter = presenter;
}
#Override
public void setUp() {
mOffersList = mPresenter.getOffersList();
mOffersAdapter = new OffersAdapter(getActivity(), mOffersList);
mOffersRecyclerView.setAdapter(mOffersAdapter);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
mOffersRecyclerView.setLayoutManager(layoutManager);
mOffersRecyclerView.setHasFixedSize(false);
performActions(mAddCard);
}
OffersPresenter
#Override
public List<Offers> getOffersList() {
return databaseHelper.getOffers();
}
DatabaseHelper -> getOffers()
public List<Offers> getOffers() {
DatabaseReference offerReference = getDatabase().child("offers");
offerReference.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
final Iterable<DataSnapshot> children = dataSnapshot.getChildren();
for (DataSnapshot d : children) {
offersList.add(d.getValue(Offers.class));
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return offersList;
}
The firebase callbacks are async so the method getOffers will return null before it gets updated by callback.
You have to pass your adapter instance and list instance to firebase callback so that it update from there
Try this mechanism:
In your activity initialize your list
#Override
public void setUp() {
mOffersList = new ArrayList<Offers> ();
mOffersAdapter = new OffersAdapter(getActivity(), mOffersList);
mOffersRecyclerView.setAdapter(mOffersAdapter);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
mOffersRecyclerView.setLayoutManager(layoutManager);
mOffersRecyclerView.setHasFixedSize(false);
performActions(mAddCard);
}
Update your getter
#Override
public void getOffersList(List<Offers> offers, OffersAdapter adapter) {
return databaseHelper.getOffers(offers, adapter);
}
Helper will be like this:
public void getOffers(List<Offers> offers,OffersAdapter adapter) {
if (offers == null) offers = new ArrayList<Offers> ();
DatabaseReference offerReference = getDatabase().child("offers");
offerReference.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
final Iterable<DataSnapshot> children = dataSnapshot.getChildren();
for (DataSnapshot d : children) {
offers.add(d.getValue(Offers.class));
}
adapter.notifyDataSetChanged();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
Keep in mind you are using value event which will gets called if any data changes and as a result data will be added multiple times. Use single value event or Child event listener to solve the issue if you have any.
It returns null because you are placing your return offersList; at the end of public List<Offers> getOffers() Remove the return statement and make getOffers() void and add logic to Get/Update your data and/or commit() your Fragment inside of onDataChange(DataSnapshot dataSnapshot), so it will return your list when the data is fetched the first time.
What I usually do is to commit the fragment or set its arguments inside of
onDataChange(DataSnapshot dataSnapshot)
I have a table which stores the list of products. I want to get notified only when any of the row gets updated.Can I use RealmChangeListener. Below id my code
public void updateProducts(final List<Product> products) {
Realm realmObj = Realm.getDefaultInstance();
for (Product product : products) {
if (product.shouldBeDeleted()) {
delete(product.getBarcode());
} else {
realmObj.beginTransaction();
realmObj.copyToRealmOrUpdate(product);
realmObj.commitTransaction();
}
}
realmObj.close();
}
Yes you can use realm change listener...
I am using in my project like this
product.addChangeListener(new RealmChangeListener<RealmModel>() {
#Override
public void onChange(RealmModel realmModel) {
//Your code when any row update or insert new record
}
});
For detail you can refer this link
https://realm.io/docs/java/latest/api/io/realm/RealmChangeListener.html
You can use our fine-grained collection notifications, which will report what kind of modification are done:
RealmResults<Product> products = realm.where(Product.class).findAll();
products.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<Product>>() {
#Override
public void onChange(RealmResults<Product> products, OrderedCollectionChangeSet changeSet) {
for (int i : changeSet.getChanges()) {
// Item at index i was updated
}
}
});
public void updateProducts(final List<Product> products) {
try(Realm r = Realm.getDefaultInstance()) {
r.executeTransaction((realm) -> {
for (Product product : products) {
if (product.shouldBeDeleted()) {
delete(product.getBarcode());
} else {
realm.insertOrUpdate(product);
}
}
});
}
}
And elsewhere on UI thread:
private Realm realm;
private RealmResults<Product> results;
private RealmChangeListener<RealmResults<Product>> listener = (element) -> {
if(element.isLoaded()) {
// results are loaded, or a change occurred!
}
};
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.some_layout);
realm = Realm.getDefaultInstance();
results = realm.where(Product.class).findAllAsync();
results.addChangeListener(listener);
}
#Override
public void onDestroy() {
super.onDestroy();
results.removeChangeListener(listener);
results = null;
realm.close();
}
P.S. you'll want to be notified on inserts and deletes as well, or your UI will get desynchronized. Thankfully that is the default behavior.
So I'll try to keep this question as to-the-point as possible, but it will involve code snippets that traverse an entire codepath.
For context, I am fairly new and completely self-taught for Android dev, so please notify me of any clear misunderstandings/poor organization throughout. The main focus of the question is bug I am experiencing now, which is that, after a network request, the variable that was supposed to be set as a result of that network request is null, because the code moved forward before the network request completed.
Here is my activity method. It is supposed to populate the mFriends variable with the result of mUserPresenter.getUserList(), which is (unfortunately) null:
/**
* Grabs a list of friends, populates list with UserAdapter
*/
#Override
public void onResume(){
super.onResume();
mUserPresenter = new UserPresenter();
mFriends = mUserPresenter.getUserList();
if (mGridView.getAdapter() == null) {
UserAdapter adapter = new UserAdapter(getActivity(), mFriends);
mGridView.setAdapter(adapter);
}
else{
((UserAdapter)mGridView.getAdapter()).refill(mFriends);
}
}
Here is how I am structuring my UserPresenter method getUserList:
public List<User> getUserList()
{
ApiService.get_friends(this);
return mUserList;
}
The real magic happens in the ApiService class:
public static void get_friends(final UserPresenter userPresenter){
ApiEndpointInterface apiService = prepareService();
apiService.get_friends().
observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Action1<List<User>>()
{
#Override
public void call(List<User> users) {
userPresenter.setList(users);
}
}
);
}
My thinking was, that by calling userPresenter.setList(users) in ApiService, that would set mUserList to the response from the api request. However, instead, mUserList == null at the time that getUserList responds.
Any ideas of how I can structure this?
I have also started to learn something similar. Here, I would rather use callbacks.
In your presenter,
public void setList(List<User> users) {
yourView.setUserList(users);
}
And your activity which implements a view (MVP)
#Override
public void setUserList(List<User> users) {
((UserAdapter)mGridView.getAdapter()).refill(mFriends);
}
Also, check that retrofit is not returning null list.
I have a made a small app when I was learning about all this. It fetches user data from GitHub and shows in a list. I was also working with ORMLite and Picasso so some db stuff is there. Dagger Dependency is also used (but you can ignore that). Here's the link.
Here's how my Presenter behaves:
private DataRetrieverImpl dataRetriever;
#Override
public void getUserList(String name) {
dataRetriever.getUserList(name);
}
#Override
public void onEvent(DataRetrieverEvent event) {
UserList userList = (UserList)event.getData();
mainView.setItems(userList);
}
DataRetrieverImpl works as a module (sort of).
private DataRetriever dataRetriever;
restAdapter = new RestAdapter.Builder().setEndpoint(SERVER_END_POINT).build();
dataRetriever = restAdapter.create(DataRetriever.class);
public void getUserList(final String name) {
Log.i(TAG, "getting user list for: " + name);
Observable<UserList> observable = dataRetriever.getUserList(name);
Log.i(TAG, "subscribe to get userlist");
observable.subscribe(new Action1<UserList>() {
#Override
public void call(UserList userList) {
eventBus.post(new DataRetrieverEvent("UserList", userList));
// save to database
for (User user : userList.getItems()) {
Log.i(TAG, user.getLogin());
try {
dbHelper.create(user);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
throwable.printStackTrace();
}
});
}
And DataRetriever is interface for retrofit. I'm sorry for the naming confusion.
public interface DataRetriever {
#GET("/search/users")
public Observable<UserList> getUserList(#Query("q") String name);
}
Any my Activity,
#Override
public void setItems(final UserList userList) {
runOnUiThread(new Runnable() {
#Override
public void run() {
UserAdapter userAdapter = (UserAdapter)recyclerView.getAdapter();
userAdapter.setUserList(userList);
userAdapter.notifyItemRangeInserted(0, userAdapter.getItemCount());
}
});
}