1

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.

Delark
  • 1,141
  • 2
  • 9
  • 15

1 Answers1

1

Upon more testing, it seemed that the .observe() was indeed being called, the problem was that the .submtList() was not triggering the onCreateViewHolder()

Not really an answer but after some reading it seems that it has something to do with it not being compatible with something other than Room.

https://stackoverflow.com/a/50062174/11214643

at least that answer managed to access the Diff_Callback, but still no response from the onCreateViewHolder()... I also tried placing a notifyDataSetChaged() inside the overridden submitList() method, but nothing happened.

I also tried placing an adapter.notifyDataSetChanged inside the .observe() Listener but it didn't work...

Finally the real answer was combining both:

adapter.notifyDataSetChanged();
recyclerView.smoothScrollToPosition(adapter.getItemCount())

inside the .observe(), after the submitList().

This solution populates the recyclerView on creation and on child added, now given the few requests for this issue, I guess it has something to do with the version I'm currently using for testing (Marshmallow).

And now the the biggest joke of all... upon reading my own question above, it seems I wrote my question(code), with the correct answer in it, but in my IDE the adapter.notifyDataSetChanged(); was always commented, or I never tried using both at the same time on the 3 days I've been stuck with this problem.

Take care people, I'm going to sleep.

Edit:

Ok some time has passed since my original question and first answer...and I just realized that after all this I learned some things, and Ill try to simplify why everything is basically wrong..

First this:

Parsing(I think this is not the proper term) the snapshots List inside that ValueSetter I think could be done inside the Adapter, I later developed a fori comparator that parses the snapshot depending on its getKey(), or whatever value inside the snapshot...of course at a given child.

In theory you could even parse the value at the XML if you are using DataBind...I dont know how good it would be to do it, but I think the code becomes cleaner if done later than doing it in the ViewModel.

Second:

I later discovered that that semaphore in the handlerTHread was pointless adn didnt worked as intended, what you need is this https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

That will help you develop a re-router that fills a single list with different paths or queries of a Firebase Database.

Also, Ive heard people say that placing logic inside the LiveData is bad, but because I love wrapping useful things (dont know if you can call it strictly encapsulation) all the logic to connect the FirebaseListener is better done inside the LiveData here is a good example https://firebase.googleblog.com/2017/12/using-android-architecture-components.html

BUT Watch out: You need to be extremely careful with 2 things:

Learn that Observers stack, and that is where the hasObserver() method is useful, you will need this when a Frgament gets recreated and calls for a LiveData to fetch info from a new Firebase path are requested again.

Second: FirebaseListeners - as the name of the method says (addEventListener()) are ADDED and not setted, this means that if you add again... you will end with two listeners and you will be downloading data twice...very dangerous... use named variables to define Listeners and do not use anonymous initializers triggered at resumed states.

Also, extremely important. Something not shown on the link I provided of LiveData...

getValue.add(myItem) DOES NOT TRIGGER THE OBSERVER.

it only triggers it on extremely specific circumstances, like if you call it inside a runnable called on the onActive() of the LiveData.

The correct way to...for example, if you want to replace something inside it, is:

private void replace2List(@NonNull DataSnapshot dataSnapshot) {

    if (getValue()!= null) {
        for (DataSnapshot mDataSnapshot: ((List<DataSnapshot>) getValue())
             ) {
            if (dataSnapshot.getKey().equals(mDataSnapshot.getKey()) && dataSnapshot.getValue() != mDataSnapshot.getValue()){
            int i = getValue().indexOf(mDataSnapshot);
                List<DataSnapshot> snapshots = (List<DataSnapshot>)getValue();
                snapshots.set(i,dataSnapshot);
                setValue(snapshots);
            }
        }
    }
}

And Finally the thing that solved the problem that created this post:

inside the Adapter:

@Override
public void submitList(@Nullable List<DataSnapshot> list) {
    Log.d(TAG, "submitList: list is: " + list.toString());

    notifyDataSetChanged();
    super.submitList(list);
}

If you place the notifyDatasetChanged() below the super, it wont work!!

in the parent Fragment of the RecyclerView:

@Override
            public void onChanged(List<DataSnapshot> snapshots) {
                adapter.submitList(snapshots);
                binding.itineraryRecyclerView.smoothScrollToPosition(0);
            }

This is a secondary measure, just in case!! using both your RecyclerView wont fail.

0 used of you want to go to the top of the list...adapter.getItemCount() if you want to go to the last item.

Why does this happen?? I think it has something to do with Fragments within Fragments...

Delark
  • 1,141
  • 2
  • 9
  • 15