6

I have a RecyclerView in which in each object of the RecyclerView I have a checkbox. Each time one of the checkboxes is pressed I would like to insert data on the Adapter. One time I finished (press a button on the Fragment that sets the Adapter to the RecyclerView) the data have to be returned.

My code(simplified) of the Adapter of the RecyclerView is:

public List<CarItem> data; 
public class MyCustomAdapter extends RecyclerView.Adapter<MyCustomAdapter.MyCustomViewHolder>  {

    public MyCustomAdapter(List<CarItem> data) {
        this.data=data;
    }

    public MyCustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view= LayoutInflater.from(parent.getContext()).inflate(R.layout_car_item, parent, false);
        return new MyCustomViewHolder(view);
    }

    public void onBindViewHolder(final MyCustomViewHolder holder, final int position) {
        holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                  @Override
                  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                       //Here I add the data that I want to store
                       holder.getItem();
                  }
            });
    }

    public class MyCustomViewHolder extends RecyclerView.ViewHolder{
          public MyCustomViewHolder (View itemView) {
              super(itemView);
              //Here I make reference to all the elements of the layout of the Adapter
          }

          public void getItem(){
              Log.d("prove", "" + data.size());
          }
    }
}

I have created getItem() method to get the data inside MyCustomViewHolder but I do not know how to return the data to the Fragment in which I sets the Adapter to the RecyclerView.

Is there some way to return data from the Adapter of the RecyclerView to the Fragment in which I set it?

Thanks in advance!

Francisco Romero
  • 12,787
  • 22
  • 92
  • 167
  • If you simplify less the getItem() method, perhaps I understand better your question. Are you retrieving data from the database here? – albodelu Aug 12 '16 at 15:21
  • @Ardock Yes, I am retrieving the data from a database. – Francisco Romero Aug 13 '16 at 14:45
  • I understand now and I think that load data from a database and don't resort to any extra data structure to save the state than the viewholders, its not the best approach but I'll try to improve my response to match your requirements. I would create an extra list to save the retrieved data or use the caritems list. Are you discarding this option for any reason? – albodelu Aug 13 '16 at 15:44
  • @Ardock Because the structure of the project was done by another person and I am trying to get the data that is shown on the list back when each of the items on the list are checked (press on the checkbox). I have to modify the code as less as possible. – Francisco Romero Aug 13 '16 at 16:02
  • Are you retrieving carItems from the database then? If you simplify less the getItem() method, I'll improve my response. – albodelu Aug 13 '16 at 16:15
  • @Ardock Yes, I am retrieving carItems from the database. I think I cannot simplify more than a Log on the console the content of `getItem()` method. I have created it on a try to get the data back on the Class in which I set the adapter to the recycler view. – Francisco Romero Aug 13 '16 at 16:24
  • Ok, thanks, I was asking for more information not less, my english.... And this line `public List data;` is not inside the adapter? – albodelu Aug 13 '16 at 16:28
  • @Ardock each `CarItem` is just an Object with "name" and "description" attributes. – Francisco Romero Aug 13 '16 at 16:59

3 Answers3

7

Updated response:

Alternative using the CarItems list:

@OnClick(...)
public void onButtonClicked() {
    int size = ((MyCustomAdapter) mRecyclerView.getAdapter()).getItemCount();
    for (int i = 0; i < size; i++) {
        if (((MyCustomAdapter) mRecyclerView.getAdapter()).isItemChecked(i)) {
            // Get each selected item
            CarItem carItem = (MyCustomAdapter) mRecyclerView.getAdapter()).getItem(i);
            // Do something with the item like save it to a selected items array.
        }
    }
}

You also can add a getCheckedItems() method to the adapter:

public List<CarItem> getCheckedItems() {
    List<CarItem> checkedItems = new ArrayList<>();
    for (int i = 0; i < getItemCount(); i++) {
        if (isItemChecked(i)) {
            checkedItems.add(getItem(i));
        }
    }
    return checkedItems;
}

and use it from the RecyclerView like this:

((MyCustomAdapter) mRecyclerView.getAdapter()).getCheckedItems();

Is there some way to return data from the Adapter of the RecyclerView to the Fragment in which I set it?

In general to manipulate the data from your fragment:

Add the methods below to the adapter and call them from the fragment via the recyclerView casting it to your custom adapter: ((MyCustomAdapter) mRecyclerView.getAdapter()).getItem(...);

public void setListItems(List<CarItem> data) {
    this.data = data;
}

public List getListItems() {
    return data;
}

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

public CarItem getItem(int position) {
    return data.get(position);
}

public void setItem(CarItem item, int position) {
    data.set(position, item);
}

I have a RecyclerView in which in each object of the RecyclerView I have a checkbox. Each time one of the checkboxes is pressed I would like to insert data on the Adapter.

Manage multichoice state and saved state

Read this explanation and check this sample app for multichoice.

This library also extends adapter for selection using SparseBooleanArray to save selected items.

Or use this to save the checked state and use it later when button is pressed to only access to the data of the checked items:

public class MyCustomAdapter extends RecyclerView.Adapter<MyCustomAdapter.MyCustomViewHolder>  {
    private ArrayList<CarItem> data;
    private SparseBooleanArray checkedState = new SparseBooleanArray();

    public void setCheckedState(int position, boolean checked) {
        checkedState.append(position, checked);
    }

    public boolean isItemChecked(int position) {
        return checkedState.get(position);
    }

    public SparseBooleanArray getCheckedState() {
        return checkedState;
    }

    public void onBindViewHolder(final MyCustomViewHolder holder, final int position) {
        holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                  @Override
                  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                       setCheckedState(holder.getAdapterPosition(), isChecked);
                       //Here I add the data that I want to store
                       if (isChecked) {
                           data.set(holder.getAdapterPosition(), holder.getItem());
                       } else {
                           data.set(holder.getAdapterPosition(), null);
                       }

              }
        });
}

Reason to use adapter position on this related Google I/O 2016 video:

enter image description here


I have created getItem() method to get the data inside MyCustomViewHolder but I do not know how to return the data to the Fragment in which I sets the Adapter to the RecyclerView.

Set/get data to/from the view holder

Also these methods can be useful to get the data, perhaps using the setTag()/getTag() mechanism.

viewHolder.itemView.setTag(yourNewData);

/**
 * Returns this view's tag.
 *
 * @return the Object stored in this view as a tag, or {@code null} if not
 *         set
 *
 * @see #setTag(Object)
 * @see #getTag(int)
 */
@ViewDebug.ExportedProperty
public Object getTag() {
    return mTag;
}

/**
 * Sets the tag associated with this view. A tag can be used to mark
 * a view in its hierarchy and does not have to be unique within the
 * hierarchy. Tags can also be used to store data within a view without
 * resorting to another data structure.
 *
 * @param tag an Object to tag the view with
 *
 * @see #getTag()
 * @see #setTag(int, Object)
 */
public void setTag(final Object tag) {
    mTag = tag;
}

One time I finished (press a button on the Fragment that sets the Adapter to the RecyclerView) the data have to be returned.

I would need extra info about what are you trying to do when the data is returned and the reason you set the adapter to the recyclerview another time here.

findViewHolderForAdapterPosition(int)

/**
 * Return the ViewHolder for the item in the given position of the data set. Unlike
 * {@link #findViewHolderForLayoutPosition(int)} this method takes into account any pending
 * adapter changes that may not be reflected to the layout yet. On the other hand, if
 * {@link Adapter#notifyDataSetChanged()} has been called but the new layout has not been
 * calculated yet, this method will return <code>null</code> since the new positions of views
 * are unknown until the layout is calculated.
 * <p>
 * This method checks only the children of RecyclerView. If the item at the given
 * <code>position</code> is not laid out, it <em>will not</em> create a new one.
 *
 * @param position The position of the item in the data set of the adapter
 * @return The ViewHolder at <code>position</code> or null if there is no such item
 */
public ViewHolder findViewHolderForAdapterPosition(int position) {
    if (mDataSetHasChangedAfterLayout) {
        return null;
    }
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder != null && !holder.isRemoved() && getAdapterPositionFor(holder) == position) {
            return holder;
        }
    }
    return null;
}

@OnClick(...)
public void onButtonClicked() {
    int size = ((MyCustomAdapter) mRecyclerView.getAdapter()).getItemCount();
    for (int i = 0; i < size; i++) {
        ViewHolder vh = (MyCustomViewHolder) findViewHolderForAdapterPosition(i);
        if (vh != null && ((MyCustomAdapter) mRecyclerView.getAdapter()).isItemChecked(i)) {
            Object obj = vh.itemView.getTag();
            // Manipulate the data contained in this view holder
            // but perhaps would be better to save the data in the CarItem
            // and don't manipulate the VH from outside the adapter
        }
    }
}
Community
  • 1
  • 1
albodelu
  • 7,931
  • 7
  • 41
  • 84
  • 1
    Thank you very much for your expanded answer!. I got confused because I thought `onBindViewHolder` method was independent but finally I got it putting `getItem()` method outside of the `onBindViewHolder` method and put it inside `MyCustomAdapter`. Of course, I had to create a setter method that I use inside `onBindViewHolder` to put the element before retrieve it from my `Fragment`. Your second section make me clarify my mind. – Francisco Romero Aug 16 '16 at 10:00
0

The easiest way is to use EventBus https://github.com/greenrobot/EventBus

Just register it in your fragment and set @Subscribe method. Build simply class and pass data from adapter to fragment using EventBus.getDefault().post(new YourClass(yourData));

Another way is use intarfaces.

Caution!

When you will use checkbox in your adapter, you must remember old value from checkbox, and set in when list will be scrolled. Then you may be interested SparseBooleanArray

Mateusz
  • 22
  • 3
  • 2
    I haven't used **EventBus**, but I have used **Otto** and things are happening asynchronously with it. And I can get REALLY messy. Thus, be careful when using buses. I prefer using interfaces instead. – Todor Kostov Aug 12 '16 at 14:25
-2

Do not use a RecyclerView. It is not designed to be used for such purposes. RecyclerView and ListView are useful when you want to show read-only data as their rows are recycled. It is dangerous to get an input from the user using these views because when the rows are recycled after scrolling it is possible to lose data that has already been entered. Use LinearLayout instead. This way you will have a direct access to the views in it. If the raws you want to display are not fixed count, you can dynamically add Views to the container.

Kiril Aleksandrov
  • 2,601
  • 20
  • 27
  • I am using `RecyclerView` because I do not know how much items would be there as I am retrieving them from a database. I do not know if it is possible with a `LinearLayout`. – Francisco Romero Aug 12 '16 at 12:48
  • @Kiril, Got any souces on that? I can imagine a RecyclerView/Listview would be very usefull for some purposes. – Robin Dijkhof Aug 12 '16 at 12:49
  • It is possible. You just need inflate the view (the row) and put it into the `LinearLayout` as a child `View`. – Kiril Aleksandrov Aug 12 '16 at 12:49
  • Error404 I consider RecyclerView multi-purpose and this answer incorrect. I never used it for your checked requirements and I only used it for read-only cases (so I don't downvote this) but if you follow this approach without knowing the number of items, you'll lose the recycling benefits and i think all the possible issues can be managed. Recyclerview wold not exist otherwise. – albodelu Aug 12 '16 at 17:16
  • @adrock, I've tried this and it works. To keep the scrolling functionality one should put the ListView in a ScrollView (this is valid for other ViewGroup containers too) – Kiril Aleksandrov Aug 12 '16 at 17:19