11

I have a VerticalGridView that is using a RecyclerView.Adapter to populate the elements. I have discovered that the onBindViewHolder() method does not get called if the potential element is off of the viewport. Unfortunately, this is causing a NullPointerException from a different method because I am catching a TextView reference in the onBindViewHolder() method and passing it to an outside variable for later manipulation.

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    final ViewHolder viewHolder = (ViewHolder) holder;
    viewHolder.txtCategoryName.setText(categories.get(position).getStrCategory());
    categories.get(position).setTxtViewReference(viewHolder.txtCategoryDefectTotal);
    viewHolder.categoryBoxRoot.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            for(CategoryListItem catItem : categories){
                if(catItem.getStrCategory().equals(viewHolder.txtCategoryName.getText())){
                    int index = Defects.getInstance().getCategories().indexOf(catItem) + 1;
                    MainInterface.grids.get(index).bringToFront();
                    MainInterface.grids.get(index).setVisibility(View.VISIBLE);
                    for(VerticalGridView grid : MainInterface.grids){
                        int gridIndex = MainInterface.grids.indexOf(grid);
                        if(gridIndex != index){
                            grid.setVisibility(View.INVISIBLE);
                        }
                    }
                    break;
                }
            }

        }
    });

From what I understand, the reference to the TextView gets created when the Viewholder object is instantiated.

public class ViewHolder extends RecyclerView.ViewHolder {

    public TextView txtCategoryName;
    public TextView txtCategoryDefectTotal;
    public View categoryBoxRoot;

    public ViewHolder(View itemView) {
        super(itemView);
        txtCategoryName = (TextView) itemView.findViewById(R.id.textViewCategoryName);
        txtCategoryDefectTotal = (TextView) itemView.findViewById(R.id.textViewCategoryTotalDefects);
        categoryBoxRoot = itemView.findViewById(R.id.root_category_box);
    }
}

Is there a way to force onBindViewHolder() to be called on all elements at least one time when the Adapter is instantiated?

I attempted the suggestions here without any success.

I understand that forcing onBindViewHolder() on all elements would work against the whole purpose of the RecycleView.Adapter. Thus, I am open to any other suggestions on how I can catch that TextView reference.

As a temporary fix to this problem, I am able to use a try catch block around the method that generates the NullPointerException. However, I am concerned that the lack of the reference means I could introduce errors in the future.

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
bergler77
  • 362
  • 1
  • 3
  • 13
  • Use your OnClickListener on the ViewHolder Class itself, not `onBindViewHolder` - you should not be creating a new onclicklistener for every row. http://stackoverflow.com/a/30285361/4252352 – Mark Jun 30 '16 at 18:48
  • That is a nice optimization however that does not help me with the problem at hand. I need the TextView reference so I can update it from an external method. – bergler77 Jun 30 '16 at 18:57
  • 2
    Why not update the data source and call `notifyDataSetChanged();` or `notifyItemChanged(position);` (which is a less expensive operation) on your Adapter. – Mark Jun 30 '16 at 19:01
  • I tried that. The adapter still does not call the onBindViewHolder method for items off of the screen. It will only get called once I scroll down. Normally this is not an issue, but I'm manipulating the data elsewhere. I attempted moving the method to the ViewHolder constructor. It appears that the ViewHolders are only constructed when they are first needed. Since, my view is offscreen, it doesn't get called. – bergler77 Jun 30 '16 at 19:10
  • 1
    So let me get this right you are trying to get 'data' from a row that is off screen? Why not just get it from the data source that the adapter uses directly? Or if that's not the case keeping a database of information/data synchronised? – Mark Jun 30 '16 at 19:20
  • Perhaps I went about what I'm doing wrong. Basically what I'm trying to do is update all the TextViews in a grid when I hit a button. It doesn't need to happen when the view itself is clicked on. Instead, it needs to happen when I switch from one gridView to another. I have a data object that is separate from the Adapter. Thus, I thought I could just snag the TextView reference created from the Adapter and store it in my data object. But I can't store what isn't created. In this instance, I can probably use try catch without worrying about that particular data reference. – bergler77 Jun 30 '16 at 19:31
  • @Mark Keen You were right with notifyDataSetChanged(). See my answer for details. – bergler77 Jul 01 '16 at 17:57

5 Answers5

5

The easiest solution for this problem is to scroll down to the bottom of the grid and then back up to the top. This cannot be done in the onCreate() method though because the grid is not technically visible at that time. Instead, it should be called in the onResume() method of the activity.

@Override
public void onResume(){
    super.onResume();
    VerticalGridView defectGrid = grids.get(0);
    RecyclerView.Adapter adapter = defectGrid.getAdapter();
    defectGrid.smoothScrollToPosition(adapter.getItemCount()-1);
    defectGrid.smoothScrollToPosition(0);
}

This was a good attempt at a solution but unfortunately it still does not work. While it does get the reference to make the method work, it does NOT necessarily have the right reference. I found that the reference can end up pointing at a different TextView as the RecycleView.Adapter reuses views to display them in different areas.

SOLUTION:

Mark Keen was right when he said to use notifyDataSetChanged(). I got it to work by fixing my onBindViewHolder() method to work properly.

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    final ViewHolder viewHolder = (ViewHolder) holder;
    viewHolder.txtCategoryName.setText(categories.get(position).getStrCategory());
    viewHolder.txtCategoryDefectTotal.setText(String.valueOf(categories.get(position).getTotalDefectsInCategory()));
}

I also changed my data object so it holds an int value instead of a reference to the TextView since the above proved that reference was invalid. Finally, I added a call to my Adapter when I pressed my custom back button.

grids.get(0).getAdapter().notifyDataSetChanged();

Thank you everyone who contributed!

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
bergler77
  • 362
  • 1
  • 3
  • 13
2

Just reset the Adapter like this

RecyclerView.Adapter adapter = recList.getAdapter();
recList.setAdapter(null);
recList.setAdapter(adapter);

recList should be you RecyclerView.

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
Schroooom
  • 29
  • 2
1

wrap your RecyclerView in a NestedScrollView that will force all the items to be bound in advance.

<androidx.core.widget.NestedScrollView
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvGrid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:nestedScrollingEnabled="false" />
</androidx.core.widget.NestedScrollView>
Rahul Tiwari
  • 6,851
  • 3
  • 49
  • 78
0

Understand Flow

public class ThemeGridAdapter extends RecyclerView.Adapter<ThemeGridAdapter.ViewHolder> {
    private OnItemClickedListener listener;
    private List<Theme> mDataset;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView title;
        public TextView subtitle;
        //        public SimpleDraweeView image;
        public ImageView img;
        public ViewGroup container;

        public ViewHolder(RelativeLayout v) {
            super(v);
            container = v;
            title = (TextView) v.findViewById(R.id.theme_name_text);
            subtitle = (TextView) v.findViewById(R.id.theme_name_type);
//            image = (SimpleDraweeView) v.findViewById(R.id.cell_img);
            img = (ImageView) v.findViewById(R.id.cell_img);
        }
    }

    public ThemeGridAdapter(List<Theme> dataset) {
        mDataset = dataset;
    }

    @Override
    public ThemeGridAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                          int viewType) {
        RelativeLayout v = (RelativeLayout) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.cell, parent, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, final int position) {
        holder.title.setText(mDataset.get(position).getTitle());
        holder.subtitle.setText(mDataset.get(position).getAuthor());
//        Uri uri = Uri.parse(mDataset.get(position).getUrl());
        holder.img.setImageResource(mDataset.get(position).getImage());
//        holder.image.setImageURI(uri);
        holder.container.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener != null)
                    listener.onItemClicked(holder.getAdapterPosition());
            }
        });

    }

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

    public void setListener(OnItemClickedListener listener) {
        this.listener = listener;
    }
}
Sohail Zahid
  • 8,099
  • 2
  • 25
  • 41
  • 1
    This does not help me. I am generating a reference to a TextView(probably not a safe operation) in the onBindViewHolder method. That method does not get called for anything outside the bounds of my viewport. So, when I perform an operation on the TextView that is offscreen, I will get a NullPointerException. – bergler77 Jun 30 '16 at 18:36
0

Check the style of binding view

  public class CrimeListFragment extends Fragment {


    private static final String SAVED_SUBTITLE_VISIBLE = "subtitle";

    private static final int REQUEST_CRIME = 1;

    private RecyclerView mRecyclerView;
    private CrimeAdapter mCrimeAdapter;

    private boolean mSubtitleVisible;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_crime_list, container, false);

        mRecyclerView = (RecyclerView) view.findViewById(R.id.crime_recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

        if (savedInstanceState != null){
            mSubtitleVisible = savedInstanceState.getBoolean(SAVED_SUBTITLE_VISIBLE);
        }
        updateUI();


        return view;
    }

    private void updateUI() {

        CrimeLab crimeLab = CrimeLab.get(getActivity());
        List<Crime> crimes = crimeLab.getCrimes();

        if (mCrimeAdapter == null) {
            mCrimeAdapter = new CrimeAdapter(crimes);
            mRecyclerView.setAdapter(mCrimeAdapter);
        } else {
            mCrimeAdapter.setCrimes(crimes);
            mCrimeAdapter.notifyDataSetChanged();
        }
        updateSubtitle();
    }

    private class CrimeHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private TextView mTitleTextView;
        private TextView mDateTextView;
        private CheckBox mSolvedCheckBox;
        private Crime mCrime;


        public CrimeHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(this);

            mTitleTextView = (TextView) itemView.findViewById(R.id.list_item_crime_title_text_view);
            mDateTextView = (TextView) itemView.findViewById(R.id.list_item_crime_date_text_view);
            mSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_crime_solved_checkbox);

        }

        public void bindCrime(Crime crime) {

            mCrime = crime;

            mTitleTextView.setText(mCrime.getTitile());
            mDateTextView.setText(mCrime.getDate().toString());
            mSolvedCheckBox.setChecked(mCrime.isSolved());
        }

        @Override
        public void onClick(View v) {

            /*Toast.makeText(getActivity(),mCrime.getTitile() + "clicked!", Toast.LENGTH_SHORT).show();*/

            /*Intent for calling activity from fragment*/
           /* Intent intent = new Intent(getActivity(), CrimeActivity.class);*/
            /*Intent intent = CrimeActivity.newIntent(getActivity(),mCrime.getId());*/
            Intent intent = CrimePagerActivity.newIntent(getActivity(), mCrime.getId());
            /*startActivityForResult(intent,REQUEST_CRIME);*/
            startActivity(intent);
        }
    }

    private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder> {

        private List<Crime> mCrimes;

        public CrimeAdapter(List<Crime> crimes) {

            mCrimes = crimes;
        }

        @Override
        public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater layoutInflater = LayoutInflater.from(getActivity());

            View view = layoutInflater.inflate(R.layout.list_item_crime, parent, false);
            return new CrimeHolder(view);

        }

        @Override
        public void onBindViewHolder(CrimeHolder holder, int position) {

            Crime crime = mCrimes.get(position);
            /*holder.mTextView.setText(crime.getTitile());*/
            holder.bindCrime(crime);

        }

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

        public void setCrimes(List<Crime> crimes) {

            mCrimes = crimes;
        }
    }


    @Override
    public void onResume() {
        super.onResume();
        updateUI();
    }


    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CRIME) {

        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.fragment_crime_list, menu);

        MenuItem subtitleItem = menu.findItem(R.id.menu_item_show_subtitle);
        if (mSubtitleVisible){
            subtitleItem.setTitle(R.string.hide_subtitle);
        }else {
            subtitleItem.setTitle(R.string.show_subtitle);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getItemId()) {

            case R.id.menu_item_new_crime:

                Crime crime = new Crime();
                CrimeLab.get(getActivity()).addCrime(crime);
                Intent intent = CrimePagerActivity.newIntent(getActivity(), crime.getId());
                startActivity(intent);
                return true;

            case R.id.menu_item_show_subtitle:

                mSubtitleVisible = !mSubtitleVisible;
                getActivity().invalidateOptionsMenu();
                updateSubtitle();
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private void updateSubtitle(){

        CrimeLab crimeLab = CrimeLab.get(getActivity());
        int crimeCount = crimeLab.getCrimes().size();

        String subtitle = getString(R.string.subtitle_format,crimeCount);
        if (!mSubtitleVisible){

            subtitle  = null;
        }
        AppCompatActivity appCompatActivity = (AppCompatActivity)getActivity();
        appCompatActivity.getSupportActionBar().setSubtitle(subtitle);

    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(SAVED_SUBTITLE_VISIBLE,mSubtitleVisible);
    }
}
Ankit Tale
  • 1,924
  • 4
  • 17
  • 30