Problems with implementing contextual action mode in recyclerview fragment

air picture air · Jun 13, 2015 · Viewed 7.8k times · Source

I hacked together my first app from a bunch of tutorials. With the help of one of them I implemented a RecyclerView inside a fragment which is used by the main activity. Now I found another tutorial which I want to use to implement multi-selection with the contextual action mode. The big problem is, that the tutorial isn't using a fragment. I tried to rewrite the code to fit in a fragment. I'm now at a point where I don't get any errors in Android Studio and the app runs, but when I actually do the LongClick to select an item, it crashes with a NullPointerException:

java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.view.ActionMode.setTitle(java.lang.CharSequence)' on a null object reference

I'm totally lost as to why this happens and with the whole multi-selection stuff in general.

I think the relevant parts of each involved classes are:

Adapter.java:

public class Adapter extends SelectableAdapter<Adapter.ViewHolder> {

private ArrayList<Meal> items = new ArrayList<>();

private Context context;
private ViewHolder.ClickListener clickListener;

public Adapter(ViewHolder.ClickListener clickListener) {
    super();
    this.clickListener = clickListener;

}

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

@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
    Meal currentMeal = items.get(position);


    holder.meal_typeicon.setImageResource(currentMeal.getBadgeIcon());
    holder.meal_price.setText(currentMeal.getPriceOutput());

    holder.meal_name.setText(isSelected(position) ? "SELECTED" : "NOT SELECTED");


}

public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
    @SuppressWarnings("unused")
    private static final String TAG = ViewHolder.class.getSimpleName();

    private RelativeLayout meal_item;

    private ClickListener listener;

    private TextView meal_name;
    private TextView meal_price;
    private TextView meal_contents;
    private TextView meal_contents_spelledout;
    private ImageView meal_typeicon;

    public ViewHolder(View itemView, ClickListener listener) {
        super(itemView);
        meal_item = (RelativeLayout) itemView.findViewById(R.id.meal_item);

        this.listener = listener;

        itemView.setOnClickListener(this);
        itemView.setOnLongClickListener(this);


        meal_name = (TextView) itemView.findViewById(R.id.meal_name);
        meal_price = (TextView) itemView.findViewById(R.id.meal_price);
        meal_contents = (TextView) itemView.findViewById(R.id.meal_contents);
        meal_contents_spelledout = (TextView) itemView.findViewById(R.id.meal_contents_spelledout);
        meal_typeicon = (ImageView) itemView.findViewById(R.id.meal_typeicon);
    }

    @Override
    public void onClick(View v) {
        if (listener != null) {
            listener.onItemClicked(getAdapterPosition());
        }
    }

    @Override
    public boolean onLongClick(View v) {
        if (listener != null) {
            return listener.onItemLongClicked(getAdapterPosition());
        }
        return false;
    }

    public interface ClickListener {
        void onItemClicked(int position);

        boolean onItemLongClicked(int position);
    }
 }

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

public void setMealList(ArrayList<Meal> listMeals) {
    this.items = listMeals;
    notifyDataSetChanged();
    notifyItemRangeChanged(0, listMeals.size());
}
}

SelectableAdapter.java:

public abstract class SelectableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
@SuppressWarnings("unused")
private static final String TAG = SelectableAdapter.class.getSimpleName();

private SparseBooleanArray selectedItems;


public SelectableAdapter() {
    selectedItems = new SparseBooleanArray();
}


/**
 * Indicates if the item at position position is selected
 * @param position Position of the item to check
 * @return true if the item is selected, false otherwise
 */
public boolean isSelected(int position) {
    return getSelectedItems().contains(position);
}


/**
 * Toggle the selection status of the item at a given position
 * @param position Position of the item to toggle the selection status for
 */
public void toggleSelection(int position) {

    if (selectedItems.get(position, false)) {
        selectedItems.delete(position);
    } else {
        selectedItems.put(position, true);
    }
    notifyItemChanged(position);
}

/**
 * Clear the selection status for all items
 */
public void clearSelection() {
    List<Integer> selection = getSelectedItems();
    selectedItems.clear();
    for (Integer i : selection) {
        notifyItemChanged(i);
    }
}

/**
 * Count the selected items
 * @return Selected items count
 */
public int getSelectedItemCount() {
    return selectedItems.size();
}

/**
 * Indicates the list of selected items
 * @return List of selected items ids
 */
public List<Integer> getSelectedItems() {
    List<Integer> items = new ArrayList<>(selectedItems.size());
    for (int i = 0; i < selectedItems.size(); ++i) {
        items.add(selectedItems.keyAt(i));
    }
    return items;
}
}

FragmentToday.java:

public class FragmentToday extends Fragment implements     Adapter.ViewHolder.ClickListener{
private RequestQueue requestQueue;
public ArrayList<Meal> listMeals = new ArrayList<>();
private TextView textVolleyError;
private MealSorter mSorter = new MealSorter();
private Adapter adapter;
private ActionModeCallback actionModeCallback = new ActionModeCallback();
private ActionMode actionMode;

public static FragmentToday newInstance() {
    return new FragmentToday();
}

public FragmentToday() {
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    VolleySingleton volleySingleton = VolleySingleton.getInstance();
    requestQueue = volleySingleton.getRequestQueue();
    sendJsonRequest();

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_today, container, false);
    textVolleyError = (TextView) view.findViewById(R.id.textVolleyError);

    adapter = new Adapter(this);

    RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
    recyclerView.setAdapter(adapter);
    recyclerView.setItemAnimator(new DefaultItemAnimator());
    recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

    sendJsonRequest();
    return view;
}

@Override
public void onItemClicked(int position) {
    if (actionMode != null) {
        toggleSelection(position);
    }
}

@Override
public boolean onItemLongClicked(int position) {
    if (actionMode != null) {
        ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
    }

    toggleSelection(position);

    return true;
}

private void toggleSelection(int position) {
    adapter.toggleSelection(position);
    int count = adapter.getSelectedItemCount();

    if (count == 0) {
        actionMode.finish();
    } else {
        actionMode.setTitle(String.valueOf(count));
        actionMode.invalidate();
    }
}

private class ActionModeCallback implements ActionMode.Callback {
    @SuppressWarnings("unused")
    private final String TAG = ActionModeCallback.class.getSimpleName();

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        mode.getMenuInflater().inflate (R.menu.menu_cam, menu);
        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return false;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        switch (item.getItemId()) {
            case R.id.favorite:
                // TODO: actually remove items
                Log.d(TAG, "menu_remove");
                mode.finish();
                return true;

            default:
                return false;
        }
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        adapter.clearSelection();
        actionMode = null;
    }
}

}

Answer

cinfwatd picture cinfwatd · Jun 15, 2015

The problem seem to be from your onItemLongClicked method of the FragmentToday.java class. It should have been:

@Override
public boolean onItemLongClicked(int position) {
    if (actionMode == null) {
        actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
    }

    toggleSelection(position);

    return true;
}

instead of:

@Override
public boolean onItemLongClicked(int position) {
    if (actionMode != null) {
        ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
    }

    toggleSelection(position);

    return true;
}