EDIT: The real problem was that the submitList() was not triggering the onCreateViewHolder() in the recyclerView.
Solution:
I see people keep having issues with this. The android DiffUtils does not work with same instances of a List, if the same instance of the List is passed, the Adapter will ignore it, EVEN IF its contents are different, which means a new List is required on each submission.
So if a component of yours is changing a list's inner state, better create a copy of it new ArrayList(toSubmit);
before submission.
Question
The MutableLiveData
inside the ViewModel
is updating, but the .observe()
that notifies the adapter is not triggered.
I am trying to display data from a Firebase
database, and I wanted to use an HandlerThread
that references a child and adds a childEventListener
to make things easier among fragments.
But this problem persists even without the HandlerThread, even if I place the whole firebase reference inside the Fragment, the ViewModel still does not triggers the .observe()
public class CategoryListViewModel2 extends ViewModel {
private static final String TAG = "CategoryListViewModel";
private MutableLiveData<List<CategoryListItem>> mData;
private ValueSetter mValueSetter;
public CategoryListViewModel2() {
mValueSetter = new ValueSetter();
mData = new MutableLiveData<>();
}
public MutableLiveData<List<CategoryListItem>> getData() {
return mData;
}
public void getCategories(DataSnapshot snapshot) {
Log.d(TAG, "getCategories: ");
mValueSetter.populateCategories(snapshot, mData);
Log.d(TAG, "getCategories: checkSize is: " + checkSize());
if (checkSize()) {
Log.d(TAG, "getCategories: mData is: " + mData.getValue().get(5));
}
}
private boolean checkSize() {
return mData.getValue().size() > 5;
}
}
The checksize()
method is telling me the MutableLiveData
is indeed increasing in size.
The ValueSetter
is a class that gets the snapShot
places it in a List and then sets the new value of the MutableLiveData
the code for the ValueStter is:
public class ValueSetter {
private static final String TAG = "ValueSetter";
private List<CategoryListItem> items;
public ValueSetter() {
items = new ArrayList<>();
}
public void populateCategories(DataSnapshot snapshot, MutableLiveData<List<CategoryListItem>> mData) {
CategoryListItem item = snapshot.getValue(CategoryListItem.class);
items.add(item);
mData.setValue(items);
}
My Fragment retrieves the initialized HandlerThread
from the Main Activity and then proceeds with the usual: Databindings
, sets the RecyclerView
, creates de Adapter and sets it, Then the ViewModel
via factory so that its constructor gets called, then the ViewModel
submits the list, and just in case it even notifies the adapter, but even this doesn't work...
public class CatalogFragment extends Fragment {
private static final String TAG = "CatalogFragmentRecycler";
private DatabaseHandlerThread mDatabaseHandlerThread;
private CategoryListViewModel2 mViewModel2;
public CatalogFragment() {
Log.d(TAG, "CatalogFragment: ");
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
Log.d(TAG, "onAttach: ");
if (context instanceof MainActivity) {
mDatabaseHandlerThread =((MainActivity)context).getDatabaseHandlerThread();
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentCatalogBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_catalog,container,false);
binding.categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
binding.categoriesRecyclerView.setHasFixedSize(true);
final CategoriesAdapter adapter = new CategoriesAdapter();
binding.categoriesRecyclerView.setAdapter(adapter);
ViewModelFactory factory = new ViewModelFactory();
mViewModel2 = ViewModelProviders.of(this, factory).get(CategoryListViewModel2.class);
mViewModel2.getData().observe(this, categoryListItems -> {
adapter.submitList(categoryListItems);
adapter.notifyDataSetChanged();
binding.categoriesRecyclerView.smoothScrollToPosition(adapter.getItemCount());
});
return binding.getRoot();
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart: ");
mDatabaseHandlerThread.sendMsg("C", dataSnapshot -> mViewModel2.getCategories(dataSnapshot));
}
The smoothScrollToPosition() is the only snippet of code that is somewhat solving my issue with the recyclerView and its adapter not being notified, the problem is that this snippet only solves the issue on the first start of the app, but if a child is added the recyclerView refuses to update.
If I remove the smoothScrollToPosition() line of code, the recyclerView shows nothing, and it only populates the list after I touch it literally, with my finger.
On the onStart, you can see my message sent, which has the reference of the child "C", and the ViewModel that takes care of the data snapshot.
Because Ive done this before, I know that in order for the viewmodel to be able to change the view, it must be done in the same thread that created the view, that means that the onChildEventListener runs my custom listener on the Ui Thread with the runOnUiThread().
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "handleMessage: ");
while (!transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
Log.d(TAG, "handleMessage: ");
mReference = mReference.child(child);
Log.d(TAG, "handleMessage: reference is: " +
mReference.toString());
transfer = false;
mReference.addChildEventListener(firebaseListener());
transfer = true;
//mReference.removeEventListener(firebaseListener());
}
This is the handler inside My HandlerThread
, the removeEventListener
snippet is commented so that events are still being listened if children are added.
In the case where the entire Database reference with the Listener is placed inside the fragment, the ViewModel refuses to trigger the .observe(). when I use the same : mViewModel2.getCategories(dataSnapshot)
from inside the ChildEventListener()
.
public class CategoriesAdapter extends ListAdapter<CategoryListItem,
CategoriesAdapter.CategoriesHolder> {
private static final String TAG = "CategoriesAdapter";
private LayoutInflater mLayoutInflater;
public CategoriesAdapter() {
super(DIFF_CALLBACK);
}
private static final DiffUtil.ItemCallback<CategoryListItem>
DIFF_CALLBACK = new DiffUtil.ItemCallback<CategoryListItem>() {
@Override
public boolean areItemsTheSame(@NonNull CategoryListItem oldItem,
@NonNull CategoryListItem newItem) {
return oldItem.getS() == newItem.getS();
}
@SuppressLint("DiffUtilEquals")
@Override
public boolean areContentsTheSame(@NonNull CategoryListItem
oldItem, @NonNull CategoryListItem newItem) {
Log.d(TAG, "areContentsTheSame: " + newItem.toString());
return oldItem.getS().equals(newItem.getS());
}
};
@NonNull
@Override
public CategoriesHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
Log.d(TAG, "onCreateViewHolder: ");
if (mLayoutInflater == null) {
mLayoutInflater = LayoutInflater.from(parent.getContext());
Log.d(TAG, "onCreateViewHolder: inflater is: " +
mLayoutInflater.toString());
}
Log.d(TAG, "onCreateViewHolder: inflater is: " + mLayoutInflater);
CategoryListItemBinding binding =
DataBindingUtil.inflate(mLayoutInflater,
R.layout.category_list_item,
parent,
false);
return new CategoriesHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull CategoriesHolder holder, int
position) {
CategoryListItem currentitem = getItem(position);
holder.setData(currentitem);
}
class CategoriesHolder extends RecyclerView.ViewHolder {
private CategoryListItemBinding binding;
public CategoriesHolder(@NonNull CategoryListItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
void setData (CategoryListItem item) {
binding.setCategoryListItem(item);
}
}
}
the S
in getS()
is the only field in the snapshot retrieved at the position.