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.