Search code examples
androidandroid-roomandroid-livedataandroid-architecture-componentsandroid-diffutils

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?


Solution

  • 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.