-1

I am fetching data from Cloud Firestore and displaying in a recyclerview using MVVM architecture along with LiveData. When I observe the changes in viewmodel and notify the adapter about the dataset change, the changed data isn't updating in the recyclerview.

Do you see anything wrong in the code?

UPDATE: I have updated code as suggested my the first answer. But still no success. I observed that even after the repository fetches the data from firestore, it doesn't updates the viewmodel about it. I have added the repository class too. Do you see any problem there?

HomeActivity.java

public class HomeActivity extends AppCompatActivity {

    RecyclerView recyclerView;
    StandardPlansAdapter adapter;
    HomeActivityViewModel viewModel;

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.recyclerViewHome);

        viewModel = new ViewModelProvider(this).get(HomeActivityViewModel.class);
        adapter = new StandardPlansAdapter(viewModel.getStandardPlans().getValue());
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(adapter);

        viewModel.getStandardPlans().observe(this, plans -> {
            adapter.setStandardPlans(plans);
            adapter.notifyDataSetChanged();
        });
    }
}

StandardPlansAdapter.java

public class StandardPlansAdapter extends RecyclerView.Adapter<StandardPlansAdapter.StandardPlansViewHolder> {

    private ArrayList<Plan> standardPlans;

    public StandardPlansAdapter(ArrayList<Plan> standardPlans) {
        this.standardPlans = standardPlans;
    }

    @NonNull
    @Override
    public StandardPlansViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.home_rv_layout, parent, false);
        return new StandardPlansViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull StandardPlansViewHolder holder, int position) {
        Plan plan = standardPlans.get(position);
        holder.textView.setText(plan.getName());
    }

    @Override
    public int getItemCount() {
        return standardPlans.size();
    }

    class StandardPlansViewHolder extends RecyclerView.ViewHolder {

        TextView textView;

        public StandardPlansViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tvPlanName);
        }
    }

    public void setStandardPlans(ArrayList<Plan> standardPlans) {
        this.standardPlans = standardPlans;
    }

}

HomeActivityViewModel.java

public class HomeActivityViewModel extends ViewModel {

    private StandardPlansRepository repository;
    private MutableLiveData<ArrayList<Plan>> standardPlans;

    public HomeActivityViewModel() {
        super();
        repository = StandardPlansRepository.getInstance();
        standardPlans = repository.getStandardPlans();
    }

    public LiveData<ArrayList<Plan>> getStandardPlans() {
        return standardPlans;
    }
}

StandardPlansRepository.java

public class StandardPlansRepository {

    private static StandardPlansRepository instance;
    private ArrayList<Plan> standardPlans = new ArrayList<>();

    public static StandardPlansRepository getInstance() {
        if (instance == null) {
            instance = new StandardPlansRepository();
        }
        return instance;
    }

    public MutableLiveData<ArrayList<Plan>> getStandardPlans() {
        setStandardPlans();
        MutableLiveData<ArrayList<Plan>> plans = new MutableLiveData<>();
        plans.setValue(standardPlans);
        return plans;
    }

    private void setStandardPlans() {
        documentReference.get().addOnSuccessListener(documentSnapshot -> {
            if (documentSnapshot.exists()) {
                StandardPlans plans = documentSnapshot.toObject(StandardPlans.class);
                if (plans != null) {
                    standardPlans = plans.getStandard_plans();
                }
            } else {
                Log.e("rahul", "Document doesn't exist");
            }
        }).addOnFailureListener(e -> {
            Log.e("rahul", e.getMessage());
            e.printStackTrace();
        });
    }
}
Rahul
  • 143
  • 1
  • 17
  • use the newer ListAdapter class from recyclerview and simplify your code – denvercoder9 Jan 04 '20 at 12:01
  • @sonnet I googled about ListAdapter and found that I actually don't need ListAdapter because in my case, the list view will not allow users to add or delete any element. Also, the data in the list will be seldom updated. If there is any other benefit of using ListAdapter then please let me know – Rahul Jan 05 '20 at 08:11

1 Answers1

1

Your Observer is calling notifyDataSetChanged without updating the actual data set, which is the private member standardPlans inside your StandardPlansAdapter

Add a public setStandardPlans method to your adapter:

public class StandardPlansAdapter extends RecyclerView.Adapter<StandardPlansAdapter.StandardPlansViewHolder> {
    public setStandardPlans(ArrayList<Plan> standardPlans) {
        this.standardPlans = standardPlans;
    }
}

And then call the setter before notifying in your Observer callback

viewModel.getStandardPlans().observe(this, (Observer<List<Plan>>) plans -> { 
    adapter.setStandardPlans(plans);   //You update the dataset first
    adapter.notifyDataSetChanged();    //And then notify the adapter of the update
})

EDIT

Also, in your onCreate method I noticed that you are attaching the observer before initializing the adapter. If your observer callback is ever fired before the adapter initialization due to a race condition you'll get a NullPointerExceptionand the Activity will crash since you're dereferencing the adapter in your callback.

adapter = new StandardPlansAdapter(viewModel.getStandardPlans().getValue()); //Initialize adapter first  
viewModel.getStandardPlans().observe(this, (Observer<List<Plan>>) plans -> adapter.notifyDataSetChanged()); //Potential Null pointer otherwise

EDIT2

You seem to have an inaccurate understanding of how LiveData works.

 public MutableLiveData<ArrayList<Plan>> getStandardPlans() {
     setStandardPlans();
     MutableLiveData<ArrayList<Plan>> plans = new MutableLiveData<>();
     plans.setValue(standardPlans);
     return plans;
 }

You cannot just manually create a new instance of LiveData here, set its value and expect the observers to be notified.

  1. You're not supposed to create instances of LiveData and set values on them.
  2. You are NOT updating the livedata value inside your document's onSuccessListener callback. (plans.setValue) How can you possibly expect to be notified of updates if you're not setting the LiveData's value there? And, if this logic is to work, you have to make your plans variable a class variable in your repository, not a local variable in your getStandardPlans method.

In any case you're not supposed to use live data this way

For example, if you were working with Room databases, you'd be getting LiveData on your tables via the Room Library observable queries, not manually initializing a LiveData instance and setting values on it.

Firestore API does not give you LiveData observable queries like the Room library. (It does give you observable queries). So if you wanted to observe those queries via LiveData and ViewModels, you'd have to extend LiveData and attach to Firestore callbacks inside it.

getSavedAddresses extends LiveData

PrashanD
  • 2,643
  • 4
  • 28
  • 58
  • Thanks a lot Prashan. I got your logic. I have a question on that too but will keep that for later. But, even after implementing your changes I got no success. I guess this time the problem is with the repository. It doesn't notify viewmodel of the updated value of the standardPlans. I have updated my question. Can you tell me what;s wrong there? – Rahul Jan 04 '20 at 11:50
  • Thank you so much. I got your point. Well I had to ask you one question which I had kept for later. Where should I place my notifyDataSetChanged(). In the Activity inside the viewmodel observer or in the Adapter inside the setStandardPlans method ? – Rahul Jan 05 '20 at 13:09
  • @Rahul answer is it doesn't matter. It won't change the functionality in any way. Also in an architectural point of view it makes sense to do it either way. If you keep it inside the Observer, that means your `setStandardPlans` in the adapter is just a POJO private field setter. If you move it inside the setter, that also makes sense because that means your setter is responsible for setting the private field's value as well as notifying itself of the change. In that case if I were you I'd rename that method to `setStandardPlansAndNotify` – PrashanD Jan 05 '20 at 13:28
  • Yeah that won't change the functionality but I was thinking about it from an architectural point of view. Whose concern is it to notify about the change? Also, can there be a case when we just have to set the field value and not have to notify the adapter? – Rahul Jan 05 '20 at 14:01
  • If the field value is used for something other than populating the list. – PrashanD Jan 05 '20 at 14:03