Lag in displaying data from Oberver on LiveData - android

I'm using a ViewModel that populates a RecyclerView.Adapter, and loads data from LiveData from my Room database. The problem is that my display is always blank, and checking with the Dao (for debug, on the main thread) shows me that the data is retreived just fine. (ergo there is data in the DB).
The problem is that the Observer on my LiveData always returns a null (or no data) and I end up having to refresh the fragment at least once (by moving away and moving back) to see anything - even the meagre one record I put in the Database for testing.
Restarting the app or fragment means a blank screen and a few refreshes before I see anything which is strange because, well, the data is already there.
I'm out of ideas on how to get this to show me data in more or less real time. Can anyone help?
Sharing the DAO, ViewModel and Fragment code here.
Fragment
... import libs and set up variables ...
private HouseCallAdapter houseCallAdapter;
private RecyclerView recyclerView;
private TextView emptyView;
RevivDatabase revivDatabase;
private LiveData<List<HouseCall>> liveHousecalls;
private List<HouseCall> houseCalls;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_reviv_housecall_request_list, container, false);
Bundle arguments = getArguments();
String action = arguments.getString("data");
revivDatabase = RevivDatabase.getDatabase(getActivity().getApplicationContext());
emptyView = view.findViewById(R.id.txtNoData);
recyclerView = view.findViewById(R.id.hcrecyclerView);
viewModel = ((Reviv) getActivity()).getViewModel();
if(liveHousecalls == null) {
liveHousecalls = new MutableLiveData<List<HouseCall>>();
}
houseCallAdapter = new HouseCallAdapter(getContext(), apikey, false, false);
liveHousecalls = viewModel.getOpenHousecalls();
// this is to test if there is actually any data retreived
// calling on main thread. Lose this code later.
houseCalls = revivDatabase.revivDao().getHousecallsByStatus(action);
break;
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity().getApplicationContext(), LinearLayoutManager.VERTICAL, false));
houseCallAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onChanged() {
super.onChanged();
checkEmpty();
}
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
checkEmpty();
}
#Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
checkEmpty();
}
void checkEmpty() {
//emptyView.setText (R.string.no_data_available);
emptyView.setVisibility(houseCallAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
recyclerView.setVisibility (houseCallAdapter.getItemCount() == 0 ? View.GONE : View.VISIBLE);
}
});
houseCallAdapter.setData(houseCalls);
houseCallAdapter.notifyDataSetChanged();
liveHousecalls.observe(getActivity(), new Observer<List<HouseCall>>() {
#Override
public void onChanged(#Nullable List<HouseCall> houseCalls) {
if(houseCalls != null) {
houseCallAdapter.setData(houseCalls);
houseCallAdapter.notifyDataSetChanged();
}
}
});
recyclerView.setItemAnimator (new DefaultItemAnimator());
recyclerView.setAdapter(houseCallAdapter);
emptyView.setVisibility(houseCallAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
return view;
}
ViewModel
private LiveData<List<HouseCall>> housecallList;
private LiveData<List<HouseCall>> openHousecalls, confirmedHousecalls, closedHousecalls, missedHousecalls, userCancelledHousecalls, respCancelledHousecalls;
private LiveData<List<Incident>> incidentList, openIncidents;
private LiveData<List<Incident>> closedIncidents, usercancelIncidents, respcancelIncidents;
private LiveData<Incident> liveIncident;
private RevivDatabase database;
Context context;
/////////////////////////////////////////////////////////
// CONSTRUCTOR
/////////////////////////////////////////////////////////
public RevivViewModel(Application application) {
super(application);
//////////////////////////////////////////////////////////////////////////
// //
// DANGER WILL ROBINSON //
// Storing context in ViewModel is Not A Good Idea (TM) //
context = application.getApplicationContext(); //
// //
//////////////////////////////////////////////////////////////////////////
database = RevivDatabase.getDatabase(application);
}
/////////////////////////////////////////////////////////
// GETTERS AND SETTERS
/////////////////////////////////////////////////////////
// Housecalls
public LiveData<List<HouseCall>> getHousecallList() {
if (housecallList == null) {
housecallList = new MutableLiveData<>();
loadHousecalls();
}
return housecallList;
}
public LiveData<List<HouseCall>> getOpenHousecalls() {
if (openHousecalls == null) {
openHousecalls = new MutableLiveData<>();
loadOpenHousecalls();
}
return openHousecalls;
}
/////////////////////////////////////////////////////////
// TRIGGER REFRESH FROM VIEWMODEL
/////////////////////////////////////////////////////////
// TBD
/////////////////////////////////////////////////////////
// EXTERNAL CALLS - REFRESH FROM DB
/////////////////////////////////////////////////////////
// Methods to accept/cancel incidents and housecalls
public void loadHousecalls(){
class OneShotTask implements Runnable {
OneShotTask() {
}
public void run() {
housecallList = database.revivDao().getAllLiveHousecalls();
//housecallList.postValue(hc);
}
}
Thread t = new Thread(new OneShotTask());
t.start();
}
public void loadOpenHousecalls(){
class OneShotTask implements Runnable {
OneShotTask() {
}
public void run() {
openHousecalls = database.revivDao().getLiveHousecallsByStatus("open");
}
}
Thread t = new Thread(new OneShotTask());
t.start();
}
}
DAOInterface
public interface RevivDaoInterface {
// Housecalls
... numerous insert, delete and update calls ...
#Query("SELECT * FROM housecalls WHERE housecallid = :housecallid")
public HouseCall getHousecallById(String housecallid);
#Query("SELECT * FROM housecalls WHERE status = :status")
public List<HouseCall> getHousecallsByStatus(String status);
#Update(onConflict = OnConflictStrategy.IGNORE)
void updateHousecall(HouseCall houseCall);
#Query("SELECT * FROM housecalls WHERE status = \'open\'")
public LiveData<List<HouseCall>> getOpenHousecalls();
#Query("SELECT * FROM housecalls WHERE status = :status")
public LiveData<List<HouseCall>> getLiveHousecallsByStatus(String status);
#Query("SELECT * FROM housecalls")
public List<HouseCall> getAllHousecalls();
}
DAO
imports
#Dao
public abstract class RevivDao implements RevivDaoInterface {
#Transaction
public void upsert(HouseCall houseCall){
try {
this.insert(houseCall);
} catch (SQLiteConstraintException exception) {
this.update(houseCall);
Log.e(TAG, "upsert: ", exception);
}
}
#Transaction
public void upsert(List<HouseCall> houseCall){
for(HouseCall hc : houseCall) {
try {
this.insert(hc);
} catch (SQLiteConstraintException exception) {
this.update(hc);
Log.e(TAG, "upsert: ", exception);
}
}
}
}

Thanks to #pskink, I figured a way around the lag in updating the data into my ViewModel.
To solve this issue, I had to implement PagedListAdapter.
In the build.gradle (Module) file
implementation 'android.arch.paging:runtime:1.0.1'
In the DAO
#Query("SELECT * from housecalls where status = :status")
public abstract DataSource.Factory<Integer, HouseCall> getHousecallPagesByStatus(String status);
In the ViewModel
//declare a LiveData of PagedList
LiveData<PagedList<HouseCall>> openhousecallPages;
// Define Configuration for Paged List
Config pagedListConfig = (new PagedList.Config.Builder()).setEnablePlaceholders(true)
.setPrefetchDistance(10)
.setPageSize(20).build();
// Function to access the data
public LiveData<PagedList<HouseCall>> getOpenhousecallPages(){
openhousecallPages = new LivePagedListBuilder<>(database.revivDao().getHousecallPagesByStatus("open"),
pagedListConfig).build();
return openhousecallPages;
}
Set up the PagedListAdapter
package packagename;
import static android.content.ContentValues.TAG;
public class HouseCallPagedAdapter extends PagedListAdapter<HouseCall, HouseCallViewHolder>{
protected HouseCallPagedAdapter(#NonNull DiffUtil.ItemCallback<HouseCall> diffCallback) {
super(diffCallback);
}
public HouseCallPagedAdapter(#NonNull DiffUtil.ItemCallback diffcallback){
super(diffcallback);
}
#NonNull
#Override
public HouseCallViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int i) {
return new HouseCallViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.card_housecall_request, parent, false),parent.getContext());
}
#Override
public void onBindViewHolder(#NonNull in.portmanteau.reviv.Adapters.HouseCallViewHolder holder, int i) {
holder.bindTo(getItem(i));
}
}
Define a ViewHolder with the following structure
public class HouseCallViewHolder extends RecyclerView.ViewHolder{
// declare values, elements, etc
public HouseCallViewHolder(View itemView, Context mContext) {
super(itemView);
// set up UI elements
}
void bindTo(final HouseCall houseCall){
this.houseCall = houseCall;
//populate values, set onClickListeners, etc.
}
}
Finally, use the Adapter in your Activity/Fragment!
// Implement an DiffUtil.ItemCallback
private DiffUtil.ItemCallback<HouseCall> diffCallback = new DiffUtil.ItemCallback<HouseCall>() {
#Override
public boolean areItemsTheSame(#NonNull HouseCall houseCall, #NonNull HouseCall newhouseCall) {
return houseCall.getHousecallid().equalsIgnoreCase(newhouseCall.getHousecallid()) ;
}
#Override
public boolean areContentsTheSame(#NonNull HouseCall houseCall, #NonNull HouseCall newhouseCall) {
return houseCall.isTheSame(newhouseCall);
}
};
HouseCallPagedAdapter houseCallPagedAdapter = new HouseCallPagedAdapter(diffCallback);
viewModel.getOpenhousecallPages().observe(this, houseCallPagedAdapter::submitList);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity().getApplicationContext(), LinearLayoutManager.VERTICAL, false));
recyclerView.setItemAnimator (new DefaultItemAnimator());
recyclerView.setAdapter(houseCallPagedAdapter);

Related

Android Fragment LiveData observer is not triggered when update is done on a record data

I am trying to figure out why the LiveData observer for getAllGoals() does not trigger immediately in the fragment when I update a record. However, the observer is called only after switching to another fragment using the bottom tab navigation and then coming back to the original fragment.
The fragment in question:
MyGoalsFragment.java
public class MyGoalsFragment extends Fragment implements MyGoalsAdapter.MyGoalsCallback {
FragmentMyGoalsBinding myGoalsBinding;
private MyGoalsViewModel myGoalsViewModel;
MyGoalsAdapter myGoalsAdapter;
ConstraintSet smallConstraintSet = new ConstraintSet();
public View onCreateView(#NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
myGoalsViewModel = new ViewModelProvider(getActivity(), ViewModelProvider.AndroidViewModelFactory.getInstance(getActivity().getApplication())).get(MyGoalsViewModel.class);
myGoalsBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_my_goals, container, false);
myGoalsBinding.recyclerView2.setLayoutManager(new LinearLayoutManager(getActivity()));
DrawerLayout drawerLayout = (DrawerLayout) getActivity().findViewById(R.id.drawer_layout);
myGoalsBinding.menu.setOnClickListener(v -> {
drawerLayout.openDrawer(GravityCompat.START);
});
TransitionManager.beginDelayedTransition(myGoalsBinding.recyclerView2);
myGoalsAdapter = new MyGoalsAdapter();
myGoalsAdapter.setCallback(this);
myGoalsAdapter.setContext(getActivity());
myGoalsAdapter.setRecyclerView(myGoalsBinding.recyclerView2);
myGoalsBinding.recyclerView2.setAdapter(myGoalsAdapter);
myGoalsBinding.floatingActionButton.setOnClickListener(v -> {
startActivity(new Intent(getActivity(), CreateGoalActivity.class));
getActivity().finish();
});
enableSwipeToDeleteAndUndo();
myGoalsBinding.recyclerView2.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0 && myGoalsBinding.floatingActionButton.getVisibility() == View.VISIBLE) {
myGoalsBinding.floatingActionButton.hide();
} else if (dy < 0 && myGoalsBinding.floatingActionButton.getVisibility() != View.VISIBLE) {
myGoalsBinding.floatingActionButton.show();
}
}
});
myGoalsViewModel.getAllGoals().observe(getViewLifecycleOwner(), new Observer<List<Goal>>() {
#Override
public void onChanged(List<Goal> goals) {
myGoalsAdapter.submitList(goals); // This observer is not called even after updating a record
}
});
return myGoalsBinding.getRoot();
}
#Override
public void editGoalCallback(Goal goal) {
Intent intent = new Intent(getActivity(), CreateGoalActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable("goal", goal);
intent.putExtras(bundle);
startActivity(intent);
}
#Override
public void goalCheckBoxCallback(Goal goal) {
myGoalsViewModel.updateGoal(goal);
}
private void enableSwipeToDeleteAndUndo() {
SwipeToDeleteCallback swipeToDeleteCallback = new SwipeToDeleteCallback(getActivity()) {
#Override
public void onSwiped(#NonNull RecyclerView.ViewHolder viewHolder, int i) {
if(i==ItemTouchHelper.LEFT) {
Goal tempGoal = myGoalsAdapter.getGoalAt(viewHolder.getAdapterPosition());
myGoalsViewModel.deleteGoal(tempGoal);
Snackbar.make(myGoalsBinding.rootConstraintLayout, "Goal Deleted", Snackbar.LENGTH_LONG)
.setAction("Undo", v -> {
myGoalsViewModel.insertGoal(tempGoal);
})
.setActionTextColor(getActivity().getResources().getColor(R.color.arcticLimeGreen))
.show();
}else if(i==ItemTouchHelper.RIGHT){
Goal tempGoal = myGoalsAdapter.getGoalAt(viewHolder.getAdapterPosition());
if(tempGoal.isCompleted())
tempGoal.setCompleted(false);
else
tempGoal.setCompleted(true);
TransitionManager.beginDelayedTransition(myGoalsBinding.recyclerView2);
myGoalsViewModel.updateGoal(tempGoal); // This is where the update is called
}
}
};
ItemTouchHelper itemTouchhelper = new ItemTouchHelper(swipeToDeleteCallback);
itemTouchhelper.attachToRecyclerView(myGoalsBinding.recyclerView2);
}
}
The MyGoals ViewModel:
public class MyGoalsViewModel extends AndroidViewModel {
private NoteRepository repository;
private LiveData<List<Goal>> allGoals;
public MyGoalsViewModel(#NonNull Application application) {
super(application);
repository = new NoteRepository(application);
allGoals = repository.getAllGoals();
}
public LiveData<List<Goal>> getAllGoals(){
return allGoals;
}
public void deleteGoal(Goal goal){repository.deleteGoal(goal);}
public void insertGoal(Goal goal){repository.insertGoal(goal);}
public void updateGoal(Goal goal){repository.updateGoal(goal);}
}
The Repository:
public class NoteRepository {
private String DB_NAME = "db_task";
Context context;
private GoalDao goalDao;
private LiveData<List<Goal>> allGoals;
private NoteDatabase noteDatabase;
public NoteRepository(Context context) {
noteDatabase = NoteDatabase.getInstance(context);
goalDao = noteDatabase.goalDao();
allGoals = goalDao.getAllGoals();
this.context = context;
}
public void insertGoal(Goal goal){
new InsertGoalAsyncTask(goalDao).execute(goal);
}
public void deleteGoal(Goal goal){
new DeleteGoalAsyncTask(goalDao).execute(goal);
}
public void updateGoal(Goal goal){
new UpdateGoalAsyncTask(goalDao).execute(goal);
}
public void deleteAllGoals(){
new DeleteAllGoalAsyncTask(goalDao).execute();
}
public LiveData<List<Goal>> getAllGoals(){
return allGoals;
}
private static class InsertGoalAsyncTask extends AsyncTask<Goal,Void,Void>{
private GoalDao goalDao;
private InsertGoalAsyncTask(GoalDao goalDao){
this.goalDao = goalDao;
}
#Override
protected Void doInBackground(Goal... goals) {
goalDao.insert(goals[0]);
return null;
}
}
private static class DeleteGoalAsyncTask extends AsyncTask<Goal,Void,Void>{
private GoalDao goalDao;
private DeleteGoalAsyncTask(GoalDao goalDao){
this.goalDao = goalDao;
}
#Override
protected Void doInBackground(Goal... goals) {
goalDao.delete(goals[0]);
return null;
}
}
private static class UpdateGoalAsyncTask extends AsyncTask<Goal,Void,Void>{
private GoalDao goalDao;
private UpdateGoalAsyncTask(GoalDao goalDao){
this.goalDao = goalDao;
}
#Override
protected Void doInBackground(Goal... goals) {
goalDao.update(goals[0]);
return null;
}
}
private static class DeleteAllGoalAsyncTask extends AsyncTask<Void,Void,Void>{
private GoalDao goalDao;
private DeleteAllGoalAsyncTask(GoalDao goalDao){
this.goalDao = goalDao;
}
#Override
protected Void doInBackground(Void... voids) {
goalDao.deleteAllGoals();
return null;
}
}
}
The DAO class:
#Dao
public interface GoalDao {
#Insert
void insert(Goal goal);
#Update
void update(Goal goal);
#Delete
void delete(Goal goal);
#Query("DELETE from goal_table")
void deleteAllGoals();
#Query("Select * from goal_table order by end_date")
LiveData<List<Goal>> getAllGoals();
}
I have this issue in 2 fragments and there are 2 other fragments that do not have this issue with the exact same implementation. Why is the observer not being called as soon as I update a record in MyGoals fragment?
I found the solution, the problem was not in the LiveData code, but in the Recyclerview ListAdapter & DiffUtil Implementation which stopped from triggering LiveData change.
In MyGoalsAdapter I have used DiffUtil & ListAdapter to have smooth animations and increase performance. For it to work properly we need to compare the new list with the old list. The Problem is where the contents of an object were being marked as equal when they were actually different. I solved this by adding a date field in my Model class modifiedAt
and updated the field before that Object was updated. Here is the snippet of code to explain it better.
MyGoalsAdapter:
public class MyGoalsAdapter extends ListAdapter<Goal, MyGoalsAdapter.MyGoalsViewHolder> {
private Context context;
public MyGoalsAdapter() {
super(DIFF_CALLBACK);
}
private static final DiffUtil.ItemCallback<Goal> DIFF_CALLBACK = new DiffUtil.ItemCallback<Goal>() {
#Override
public boolean areItemsTheSame(#NonNull Goal oldItem, #NonNull Goal newItem) {
return oldItem.getId() == newItem.getId();
}
#Override
public boolean areContentsTheSame(#NonNull Goal oldItem, #NonNull Goal newItem) { //Here we check if the objects in the list have changed fields.
boolean id,desc,iscomp,edate,etime,sdate,stime,title, naya, purana, createdAt, modifiedAt;
id = oldItem.getId() == newItem.getId();
desc = oldItem.getDescription().equals(newItem.getDescription());
purana = oldItem.isCompleted();
naya = newItem.isCompleted();
iscomp = purana && naya;
edate = oldItem.getEnd_date().equals(newItem.getEnd_date());
etime = oldItem.getEnd_time().equals(newItem.getEnd_time());
sdate = oldItem.getStart_date().equals(newItem.getStart_date());
stime = oldItem.getStart_time().equals(newItem.getStart_time());
title = oldItem.getTitle().equals(newItem.getTitle());
createdAt = oldItem.getCreatedAt().equals(newItem.getCreatedAt());
modifiedAt = oldItem.getModifiedAt().equals(newItem.getModifiedAt()); //This will return false for the object that is changed
return id &&
desc &&
iscomp &&
edate &&
etime &&
sdate &&
stime &&
title &&
createdAt &&
modifiedAt
;
}
};
}
When I am updating I set the Object modifiedAt field with the current Date and Time.
Goal tempGoal = myGoalsAdapter.getGoalAt(viewHolder.getAdapterPosition()); //Get the object to make change to it
//make change to the object's field
tempGoal.setModifiedAt(Calendar.getInstance().getTime()); //set the modified date with Current date
myGoalsViewModel.updateGoal(tempGoal); //Update the object to the database
Changing the modifiedAt field will tell the Adapter when there is an object that is updated, triggering the animation and showing the updated object in the List, instantly.
I hope this will help someone.

Only update RecyclerView when a change in the dataset occurs

I have a dataset of restaurants and a recycler view where they are displayed. Depending on a few options, they should or not be visible: opening time, food type, etc.
Right now every time the activity with the recycler view is opened I run adapter.updateDataset() which internally goes through the whole dataset, creates a subset based on all the possible filters, and then does notifyDataSetChanged().
How can I make it so that I only need to run adapter.updateDataset() when a change actually occurs? Since these changes occur in a different context from the RecyclerView activity, I can't just call the function there. What alternative do I have, to improve performance?
You should probably use a list of LiveData objects either from room or a network resource and bind it your viewmodel. Then you will be observing the changes in your fragment/activity. When the change occurs, update the adapters data list and do not forget to use DiffUtil in order to update only changed items. A good example is in google sample codes on room database usage.
In your Room Dao query it should be like:
#Query("SELECT * FROM products")
LiveData<List<ProductEntity>> loadProducts();
Then in your viewmodel:
public class ProductListViewModel extends AndroidViewModel {
// MediatorLiveData can observe other LiveData objects and react on their emissions.
private final MediatorLiveData<List<ProductEntity>> observableProducts;
public ProductListViewModel(#NonNull Application application) {
super(application);
observableProducts = new MediatorLiveData<>();
// set by default null, until we get data from the database.
observableProducts.setValue(null);
LiveData<List<ProductEntity>> products = ((YourBaseApp) application).getRepository()
.loadProducts();
observableProducts.addSource(products, observableProducts::setValue);
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
public Factory(#NonNull Application application) {
mApplication = application;
}
#Override
public <T extends ViewModel> T create(Class<T> modelClass) {
//noinspection unchecked
return (T) new ProductListViewModel(mApplication);
}
}
public LiveData<List<ProductEntity>> getProductList() {
return observableProducts;
}
}
Then in your activity/fragment onCreate you may call such a sample function and start observing your data:
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
// Binding is of type ProductListLayoutBinding
// you need to declare it on tope of your fragment
binding = DataBindingUtil.inflate(inflater, R.layout.product_list_layout, container, false);
// your other stuff if needed..
productAdapter = new ProductAdapter(/*...Your parameters if any*/);
binding.yourRecylerViewId.setAdapter(productAdapter);
return binding.getRoot();
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//...
//...
// do your normal stuff above
ProductListViewModel.Factory factory = new ProductListViewModel.Factory(
YourBaseApp.getInstance());
final ProductListViewModel viewModel =
new ViewModelProvider(this, factory).get(ProductListViewModel.class);
subscribeUi(viewModel);
}
private void subscribeUi(ProductListViewModel viewModel) {
// Update the list when the data changes
viewModel.getProductList().observe(this, new Observer<List<ProductEntity>>() {
#Override
public void onChanged(#Nullable List<ProductEntity> myProducts) {
if (myProducts != null) {
if (myProducts.size() == 0) {
binding.setIsLoading(true);
} else {
binding.setIsLoading(false);
productAdapter.setProductList(myProducts);
}
} else {
binding.setIsLoading(true);
}
binding.executePendingBindings();
}
});
}
Finally on your adapter:
public void setProductList(final List<? extends Product> inProductList) {
if (productList == null) {
productList = inproductList;
notifyItemRangeInserted(0, productList.size());
} else {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
#Override
public int getOldListSize() {
return productList.size();
}
#Override
public int getNewListSize() {
return inproductList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return productList.get(oldItemPosition).getId() == inproductList.get(newItemPosition).getId();
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
productList newProduct = inproductList.get(newItemPosition);
productList oldProduct = productList.get(oldItemPosition);
return newProduct.getId() == oldProduct.getId()
&& Objects.equals(newProduct.getDefinition(), oldProduct.getDefinition())
//... compare other properties
//...
;
}
});
productList = inproductList;
result.dispatchUpdatesTo(this);
}
}
Hope , this helps.

How to reset recycler when recreate fragment

The problem is that in my tablayout when im switching between tabs my list duplicating. So i need to remove list on onStop() to recreate it then. Or might be other better solution.
I have tried the following solutions
https://code-examples.net/en/q/1c97047
How to reset recyclerView position item views to original state after refreshing adapter
Remove all items from RecyclerView
My code of adapter
public class OnlineUsersAdapter extends RecyclerView.Adapter<OnlineUsersAdapter.OnlineUserViewHolder> {
private List<OnlineUser> onlineUsers = new ArrayList<>();
private OnItemClickListener.OnItemClickCallback onItemClickCallback;
private OnItemClickListener.OnItemClickCallback onChatClickCallback;
private OnItemClickListener.OnItemClickCallback onLikeClickCallback;
private Context context;
public OnlineUsersAdapter(Context context) {
this.onlineUsers = new ArrayList<>();
this.context = context;
}
#NonNull
#Override
public OnlineUsersAdapter.OnlineUserViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
context = parent.getContext();
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_user, parent, false);
return new OnlineUsersAdapter.OnlineUserViewHolder(v);
}
#Override
public void onBindViewHolder(#NonNull OnlineUsersAdapter.OnlineUserViewHolder holder, int position) {
OnlineUser user = onlineUsers.get(position);
Log.d("testList", "rating " + user.getRating() + " uid " + user.getUid());
holder.bind(user, position);
}
#Override
public int getItemCount() {
return onlineUsers.size();
}
class OnlineUserViewHolder extends RecyclerView.ViewHolder {
RelativeLayout container;
ImageView imageView, likeBtn, chatBtn;
TextView name, country;
private LottieAnimationView animationView;
OnlineUserViewHolder(View itemView) {
super(itemView);
context = itemView.getContext();
container = itemView.findViewById(R.id.item_user_container);
imageView = itemView.findViewById(R.id.user_img);
likeBtn = itemView.findViewById(R.id.search_btn_like);
chatBtn = itemView.findViewById(R.id.search_btn_chat);
name = itemView.findViewById(R.id.user_name);
country = itemView.findViewById(R.id.user_country);
animationView = itemView.findViewById(R.id.lottieAnimationView);
}
void bind(OnlineUser user, int position) {
ViewCompat.setTransitionName(imageView, user.getName());
if (FirebaseUtils.isUserExist() && user.getUid() != null) {
new FriendRepository().isLiked(user.getUid(), flag -> {
if (flag) {
likeBtn.setBackground(ContextCompat.getDrawable(context, R.drawable.ic_favorite));
animationView.setVisibility(View.VISIBLE);
} else {
likeBtn.setBackground(ContextCompat.getDrawable(context, R.drawable.heart_outline));
animationView.setVisibility(View.GONE);
}
});
}
if (user.getUid() != null) {
chatBtn.setOnClickListener(new OnItemClickListener(position, onChatClickCallback));
likeBtn.setOnClickListener(new OnItemClickListener(position, onLikeClickCallback));
}
imageView.setOnClickListener(new OnItemClickListener(position, onItemClickCallback));
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
if (user.getImage().equals(Consts.DEFAULT)) {
Glide.with(context).load(context.getResources().getDrawable(R.drawable.default_avatar)).into(imageView);
} else {
Glide.with(context).load(user.getImage()).thumbnail(0.5f).into(imageView);
}
country.setText(user.getCountry());
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(500);
animator.addUpdateListener(valueAnimator ->
animationView.setProgress((Float) valueAnimator.getAnimatedValue()));
if (animationView.getProgress() == 0f) {
animator.start();
} else {
animationView.setProgress(0f);
}
}
}
public OnlineUsersAdapter(OnItemClickListener.OnItemClickCallback onItemClickCallback,
OnItemClickListener.OnItemClickCallback onChatClickCallback,
OnItemClickListener.OnItemClickCallback onLikeClickCallback) {
this.onItemClickCallback = onItemClickCallback;
this.onChatClickCallback = onChatClickCallback;
this.onLikeClickCallback = onLikeClickCallback;
}
public void addUsers(List<OnlineUser> userList) {
int initSize = userList.size();
onlineUsers.addAll(userList);
// notifyItemRangeInserted(onlineUsers.size() - userList.size(), onlineUsers.size());
}
public String getLastItemId() {
return onlineUsers.get(onlineUsers.size() - 1).getUid();
}
public void clearData() {
List<OnlineUser> data = new ArrayList<>();
addUsers(data);
notifyDataSetChanged();
}
My code in fragment
#Override
public void onStop() {
super.onStop();
firstUid = "";
stopDownloadList = false;
List<OnlineUser> list = new ArrayList<>();
mAdapter.addUsers(list);
mAdapter.notifyDataSetChanged();
}
`users are added after callback
#Override
public void addUsers(List<OnlineUser> onlineUsers) {
if (firstUid.equals("")){
firstUid = onlineUsers.get(0).getUid();
}
if (!firstUid.equals("") && onlineUsers.contains(firstUid)){
stopDownloadList = true;
}
if (!stopDownloadList){
mAdapter.addUsers(onlineUsers);
}
setRefreshProgress(false);
isLoading = false;
isMaxData = true;
}
The line mAdapter.addUsers(onlineUsers); from addUsers method gets called twice. Looks like your asynchronous operation gets triggered twice (e. g. from repeating lifecycle methods like onCreate/onCreateView/onViewCreated).
Solution #1: request users a single time
Move your user requesting machinery to onCreate or onAttach. This will save network traffic but could lead to showing outdated data.
Solution #2: replaceUsers
Your clearData calls mAdapter.addUsers(new ArrayList<>()); (btw, take a look at Collections.emptyList()). Looks like you're trying to replace adapter data but appending instead. Replacement method could look like
public void replaceUsers(List<OnlineUser> userList) {
int oldSize = userList.size();
onlineUsers = userList;
notifyItemRangeRemoved(0, oldSize);
notifyItemRangeInserted(0, userList.size);
}
This version still requeses users every time your fragment gets focused but shows fresher data.

RecyclerView is not updated when notifyDataSetChanged is used with MVVM

I am working with MVVM. Main screen shows movie's posters only during debugging (and not during regular run).
The problem is in observation of RecyclerView population. There is Observer in MainActivity. I expect that notifyDataSetChanged method will cause
posters to appear after receiving data from the API, but it doesn't happen.
My cleaned code related to this issue only is available in https://github.com/RayaLevinson/Test
I am missing some important point related to Observer. Please help me! Thank you.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = findViewById(R.id.recycler_view_movie);
mMainActivityViewModal = ViewModelProviders.of(this).get(MainActivityViewModel.class);
mMainActivityViewModal.init();
mMainActivityViewModal.getMovies().observe(this, new Observer<List<Movie>>() {
#Override
public void onChanged(#Nullable List<Movie> movies) {
mAdapter.notifyDataSetChanged();
}
});
initRecyclerView();
}
private void initRecyclerView() {
mAdapter = new RecyclerViewAdapter(this, mMainActivityViewModal.getMovies().getValue());
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
mRecyclerView.setAdapter(mAdapter);
}
MovieRepository.java
public class MovieRepository {
private static final String TAG = "MovieRepository";
private static String mSortBy = "popular";
private static MovieRepository instance;
private List<Movie> movies = new ArrayList<>();
public static MovieRepository getInstance() {
if (instance == null) {
instance = new MovieRepository();
}
return instance;
}
public MutableLiveData<List<Movie>> getMovies() {
setMovies();
MutableLiveData<List<Movie>> data = new MutableLiveData<List<Movie>>();
data.setValue(movies);
return data;
}
private void setMovies() {
Context context = GlobalApplication.getAppContext();
if (NetworkUtils.isNetworkAvailable(context)) {
movies.clear();
new MovieRepository.FetchMoviesTask().execute(mSortBy);
} else {
alertUserAboutNetworkError();
}
}
private void alertUserAboutNetworkError() {
Context context = GlobalApplication.getAppContext();
// Toast.makeText(context, R.string.networkErr, Toast.LENGTH_LONG).show();
}
private class FetchMoviesTask extends AsyncTask<String, Void, List<Movie>> {
#Override
protected List<Movie> doInBackground(String... params) {
if (params.length == 0) {
return null;
}
String sortBy = params[0];
Log.d(TAG, "In doInBackground " + sortBy);
URL moviesRequestUrl = NetworkUtils.buildUrl(sortBy);
try {
String jsonWeatherResponse = NetworkUtils.getResponseFromHttpUrl(moviesRequestUrl);
return MovieJsonUtils.getMoviesDataFromJson(jsonWeatherResponse);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
#Override
protected void onPostExecute(List<Movie> parsedMoviesData) {
if (parsedMoviesData != null) {
for (Movie movie : parsedMoviesData) {
movies.add(movie);
Log.d(TAG, "In onPostExecute " + " movie was added");
}
}
}
}
}
MainActivityViewModel.java
public class MainActivityViewModel extends ViewModel {
private MutableLiveData<List<Movie>> mMovies;
private MovieRepository mMoviewRepository;
public void init() {
if (mMovies != null) {
return;
}
mMoviewRepository = MovieRepository.getInstance();
mMovies = mMoviewRepository.getMovies();
}
public LiveData<List<Movie>> getMovies() {
return mMovies;
}
}
RecyclerViewAdapter.java
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private static final String TAG = "RecyclerViewAdapter";
private final Context mContext;
private List<Movie> mMovies;
public RecyclerViewAdapter(Context mContext, List<Movie> movies) {
this.mMovies = movies;
this.mContext = mContext;
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_list_item, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull final ViewHolder holder, int position) {
Log.d(TAG, "onBindViewHolder called");
Picasso.get()
.load(mMovies.get(holder.getAdapterPosition()).getPosterPath())
.placeholder(R.mipmap.ic_launcher)
.into(holder.image);
}
#Override
public int getItemCount() {
return mMovies.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
final ImageView image;
final LinearLayout parentLayout;
private ViewHolder(#NonNull View itemView) {
super(itemView);
image = itemView.findViewById(R.id.image);
parentLayout = itemView.findViewById(R.id.parent_layout);
}
}
public void update(List<Movie> movies) {
mMovies.clear();
mMovies.addAll(movies);
notifyDataSetChanged();
}
}
Your MovieRepository#getMovies() executes the Livedata.setValue() before the AsyncTask finishes. You can see that in your debug output.
What you have to do is to call postValue() (cause your on not on the mainthread) in your onPostExecute() method. Then you have to call mAdapter.update() from the onChanged() method.
Also I would recommend to refactor your ViewModel a little bit. Remove the call to the repository from your init() method and create a new method that only calls the load function from the repo. So if you later on would like to support things like endless scrolling, this will help you a lot.
Just a matter of opinion, but i like to create my observables inside my ViewModel and not in the Repository and pass it along as parameter. Thats how it could look like:
Activity
#Override
protected void onCreate(Bundle savedInstanceState) {
...
viewModel = ViewModelProviders.of(this).get(YOUR_VIEW_MODEL.class);
viewModel.init();
viewModel.getItemsObservable().observe(this, new Observer<List<Item>>() {
#Override
public void onChanged(#Nullable List<Item> items) {
// Add/replace your existing adapter
adapter.add/replaceItems(items);
// For better performance when adding/updating elements you should call notifyItemRangeInserted()/notifyItemRangeChanged(). For replacing the whole dataset notifyDataSetChanged() is fine
adapter.notifyDataSetChanged();
// Normally i would put those calls inside the adapter and make appropriate methods but for demonstration.
}
});
initRecyclerView();
viewModel.loadItems()
}
ViewModel
public void init(){
repository = Repository.getInstance();
}
public void loadItems(){
repository.loadItems(getItemsObservable());
}
public LiveData<List<Item>> getItemsObservable() {
if (items == null) {
items = new MutableLiveData<>();
}
return items;
}
Repository
public void loadItems(LiveData<List<Item>> liveData){
List<Item> data = remote.getDataAsync(); // get your data asynchronously
liveData.postValue(data); // call this after you got your data, in your case inside the onPostExecute() method
}

How to make Livedata not update whole list

So I've got an adapter, which is holding a list of objects that I'm displaying. I'm using the MVVP approach with LiveData and ViewModel that come with the android architecture framework.
In my fragment, I connect the livedata to the adapter:
viewModel.getAlarms().observe(this, alarms -> {
Timber.d("Updating alarm list");
alarmAdapter.updateAlarms(alarms);
});
And in my adapter, I update the list...
void updateAlarms(List<Alarm> alarms){
this.alarms = alarms;
notifyDataSetChanged();
}
So even if I make a small change on a single item from the list (item update, item create, item delete..), the whole list will update. That messes up all of my animations. Is there a way to prevent that?
I don't want to copy everything, but as requested here's the bigger picture:
Fragment:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel = ViewModelProviders.of(this, factory).get(HomeViewModel.class);
// Get everything..
viewModel.getAlarms().observe(this, alarms -> {
Timber.d("Updating alarm list");
alarmAdapter.updateAlarms(alarms);
});
}
#OnClick(R.id.home_fbtn_add_alarm)
void addAlarm(){
viewModel.createAlarm(new Alarm(13,39));
}
private void onAlarmStatusChanged(int alarmId, boolean isActive){
// TODO Make it so it doesn't update the whole list...
viewModel.setAlarmStatus(alarmId, isActive);
}
private void onAlarmDeleted(int alarmId){
this.showSnackbar(String.format("Alarm %s deleted", alarmId), clContainer);
viewModel.deleteAlarm(alarmId);
}
Adapter:
class AlarmsAdapter extends RecyclerView.Adapter<AlarmsAdapter.AlarmHolder> {
private List<Alarm> alarms;
private BiConsumer<Integer, Boolean> onStatusChange;
private Consumer<Integer> onDelete;
AlarmsAdapter(BiConsumer<Integer, Boolean> onStatusChange, Consumer<Integer> onDelete) {
this.alarms = new ArrayList<>();
this.onStatusChange = onStatusChange;
this.onDelete = onDelete;
}
void updateAlarms(List<Alarm> alarms){
this.alarms = alarms;
notifyDataSetChanged();
}
#NonNull
#Override
public AlarmHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
Context parentContext = parent.getContext();
int alarmLayoutId = R.layout.item_alarm;
View view = LayoutInflater.from(parentContext).inflate(alarmLayoutId, parent, false);
return new AlarmHolder(view);
}
#Override
public void onBindViewHolder(#NonNull AlarmHolder alarmViewHolder, int position) {
Alarm alarm = alarms.get(position);
alarmViewHolder.setAlarm(alarm);
}
#Override
public int getItemCount() {
return alarms == null ? 0 : alarms.size();
}
class AlarmHolder extends RecyclerView.ViewHolder {
#BindView(R.id.item_alarm_tv_time)
TextView tvTime;
#BindView(R.id.item_alarm_tv_repeat)
TextView tvRepeat;
#BindView(R.id.item_alarm_tv_punishments)
TextView tvPunishment;
#BindView(R.id.item_alarm_swt_active)
Switch swtActive;
#BindView(R.id.item_alarm_img_delete)
ImageView imgDelete;
#BindView(R.id.item_alarm_foreground)
ConstraintLayout foreground;
#BindView(R.id.item_alarm_background)
RelativeLayout background;
AlarmHolder(#NonNull View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void setAlarm(Alarm alarm){
Timber.i("Setting alarm: %s", this.getAdapterPosition());
boolean isActive = alarm.getActive();
tvTime.setText(alarm.getTime());
tvRepeat.setText(alarm.getRepetitionDays());
tvPunishment.setText(alarm.getPunishments());
swtActive.setChecked(isActive);
}
private void setStatus(boolean isActive) {
AlphaAnimation animation;
if(!isActive){
animation = new AlphaAnimation(1.0f, 0.3f);
} else {
animation = new AlphaAnimation(0.3f, 1f);
}
animation.setDuration(300);
animation.setFillAfter(true);
this.itemView.startAnimation(animation);
// TODO Make it so it doesn't update the whole list...
}
#OnCheckedChanged(R.id.item_alarm_swt_active)
void onStatusClick(boolean checked) {
onStatusChange.accept(getAdapterPosition(), checked);
setStatus(checked);
}
#OnClick(R.id.item_alarm_img_delete)
void onDeleteClick() {
onDelete.accept(getAdapterPosition());
}
}}
And the LiveData:
public class HomeViewModel extends ViewModel {
private final AlarmRepository alarmRepository;
private LiveData<List<Alarm>> alarms;
public HomeViewModel(AlarmRepository alarmRepository) {
this.alarmRepository = alarmRepository;
}
/**
* Gets the Alarms' Observable...
* #return Alarms' observable
*/
LiveData<List<Alarm>> getAlarms() {
Timber.d("Fetching alarms..");
if(alarms == null) {
Timber.i("No alarms are cached. Going to DB!");
alarms = alarmRepository.getAllAlarms();
}
return alarms;
}
/**
* Deletes the selected
* #param alarmPosition alarm to be deleted
*/
void deleteAlarm(int alarmPosition) {
Timber.d("Deleting alarm %d", alarmPosition);
getAlarmAtPosition(alarmPosition)
.ifPresent(alarmRepository::deleteAlarm);
}
/**
* Changes the status of the selected alarm
* #param alarmPosition The selected alarm
* #param status The new status
*/
void setAlarmStatus(int alarmPosition, boolean status){
Timber.d("Alarm: %d is changing active status to %s", alarmPosition, status);
getAlarmAtPosition(alarmPosition)
.ifPresent(alarm -> alarmRepository.updateStatus(alarm, status));
}
/**
* Gets the alarm at the selected position.
* #param position The position of the alarm
* #return The alarm of the selected position. Else returns empty.
*/
private Optional<Alarm> getAlarmAtPosition(int position){
Optional<List<Alarm>> alarms =
Optional.ofNullable(this.alarms)
.map(LiveData::getValue);
if(!alarms.isPresent()) {
return Optional.empty();
}
try {
return Optional.of(alarms.get().get(position));
} catch (Exception e){
Timber.e(e, "Could not get alarm at position: %d", position);
return Optional.empty();
}
}
/**
* Creates a new alarm. If null, does nothing.
* #param alarm The alarm to be saved in the DB
*/
void createAlarm(Alarm alarm) {
Timber.d("Adding new alarm.");
if(alarm == null) {
Timber.w("Cannot save null alarm");
return;
}
alarmRepository.createAlarm(alarm);
}
}
I would suggest using ListAdapter (part of recyclerView library)
class AlarmsAdapter extends ListAdapter<Alarm , AlarmsAdapter.AlarmHolder> {
public AlarmsAdapter(
#NonNull ItemCallback<Feed> diffCallback) {
super(diffCallback);
}.....
}
pass this to AlarmsAdapter
private final ItemCallback<Alarm > diffCallback = new ItemCallback<Feed>() {
#Override
public boolean areItemsTheSame(#NonNull Alarm oldItem, #NonNull Alarm newItem) {
return oldItem==newItem;
}
#Override
public boolean areContentsTheSame(#NonNull Alarm oldItem, #NonNull Alarm newItem) {
return oldItem==newItem;
}
};

Categories

Resources