Divide elements on groups in RecyclerView
Asked Answered
S

3

56

I need to divide elements in RecyclerView on groups with titles (like in the Inbox app on the picture below) so help me please to figure out what approach would be better for my case: 1) I can use Heterogenous layouts for it but it is not so convenient to insert new elements in groups (because I need check if elements of the same group is already added or I need to add new divider). So in this case I'll wrap all operations with such data structure into a separate class.

2) Theoretically I can wrap each group in its own RecyclerView with label is it a good idea?

Inbox app

Scoles answered 18/1, 2016 at 6:23 Comment(2)
It is not a matter of grouping elements at ui level using multiple recyclerview, since you will have problems with scrolling. You can use just one recyclerview with two element types, one for headers and the other one for items.Homograft
@thetonrifles You're right UI should be as simple as possible, it seems I forgot it when I was asking the question, thanks!Scoles
H
118

For example you can:

  1. Use a TreeMap<Date,List<Event>> for splitting elements by date. This will be a collection for keeping your business objects. Of course if you already have a similar structure you can keep it. It's just important to have something for easily building list of items for populating UI with right elements order.

  2. Define a dedicated abstract type for List items (e.g. ListItem) to wrap your business objects. Its implementation could be something like this:

    public abstract class ListItem {
    
        public static final int TYPE_HEADER = 0;
        public static final int TYPE_EVENT = 1;
    
        abstract public int getType();
    } 
    
  3. Define a class for each of your List element type (here I added just two types but you can use many as you need):

    public class HeaderItem extends ListItem {
    
        private Date date;
    
        // here getters and setters 
        // for title and so on, built
        // using date
    
        @Override
        public int getType() {
            return TYPE_HEADER;
        }
    
    }
    
    public class EventItem extends ListItem {
    
        private Event event;
    
        // here getters and setters 
        // for title and so on, built 
        // using event
    
        @Override
        public int getType() {
            return TYPE_EVENT;
        }
    
    }
    
  4. Create a List as follows (where mEventsMap is map build at point 1):

    List<ListItem> mItems;
    // ...
    mItems = new ArrayList<>();
    for (Date date : mEventsMap.keySet()) {
        HeaderItem header = new HeaderItem();
        header.setDate(date); 
        mItems.add(header);
        for (Event event : mEventsMap.get(date)) {
            EventItem item = new EventItem();
            item.setEvent(event);
            mItems.add(item);
        }
    }
    
  5. Define an adapter for your RecyclerView, working on List defined at point 4. Here what is important is to override getItemViewType method as follows:

    @Override
    public int getItemViewType(int position) {
        return mItems.get(position).getType();
    }
    

    Then you need to have two layouts and ViewHolder for header and event items. Adapter methods should take care of this accordingly:

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == ListItem.TYPE_HEADER) {
            View itemView = mLayoutInflater.inflate(R.layout.view_list_item_header, parent, false);
            return new HeaderViewHolder(itemView);
        } else {
            View itemView = mLayoutInflater.inflate(R.layout.view_list_item_event, parent, false);
            return new EventViewHolder(itemView);
        }
    }
    
    
    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
        int type = getItemViewType(position);
        if (type == ListItem.TYPE_HEADER) {
            HeaderItem header = (HeaderItem) mItems.get(position);
            HeaderViewHolder holder = (HeaderViewHolder) viewHolder;
            // your logic here
        } else {            
            EventItem event = (EventItem) mItems.get(position);
            EventViewHolder holder = (EventViewHolder) viewHolder;
            // your logic here
        }
    }
    

Here it is a repository on GitHub providing an implementation of the approach explained above.

Homograft answered 18/1, 2016 at 8:16 Comment(25)
At the moment I have almost the same solution (but without explicit map to list transformation), it works but I think (probably I'm wrong) that it is not so easy to support the solution and maybe some best practices already existScoles
@Scoles With this approach probably the main problem would be to keep code clean in case of many view types. In such case, a possibility could be to encapsulate each ViewHolder and corresponding binding logic into a dedicated class, keeping adapter implementation lighter.Homograft
Good advice, thanks. Fortunately in my case I have only two types of items (divider and item with data) :) but who knows probably it will be changedScoles
My question qould be, how do you wrap the items into cards? The OP asked about card-container-grouped UI having headers. How do you group the event-items into 'card' (or whatever it is on the UI, having shadows) with one recyclerview? I guess the answer is to wrap each row into cardviews: #31273703Expatriate
@Expatriate Of course there are different approaches (something is described in answer of question you linked). A possible approach here is that instead of having just two view types (header and item) we should have four view types (header, top item, middle item, bottom item). That's because layout changes for each item depending on position. Then you can adapt cardview as reported in the answer you linked or proceed by using some 9-patch background already including shadow for example. Core concept is that you need to have multiple view types for rendering different kind of elements.Homograft
@thetonrifles don't HeaderItem and EventItem need to extend the abstract ListItem class?Neural
@EricH you're right! Interesting that nobody including me noticed this issue :) btw I fixed it. Thanks a lot!Homograft
guys can you please share a link to the full source code because it really tricky on using these snippets only..Baseless
@kinsleykajiva I have this public repo on GitHub that uses this approach but with a GridLayoutManager. Hope this could be already helpful. If not I can create another repo exactly with answer case but I need some time (hopefully I can work on this during weekend).Homograft
@kinsleykajiva I had some time this evening. Here is a branch with a sample project reporting same example of the answer. Hope this could help.Homograft
@kinsleykajiva glad it was helpful :) ... of course if you have questions let me know!Homograft
@thetonrifles ok its fine.Baseless
This will not work for a horizontal layout. They will not be divided into rows. Headers and content will appear in same row.Acceptor
@Donato I don't get the point of your comment. Layout you are proposing (that I assume is a LinearLayoutManager with HORIZONTAL orientation) is for displaying items on the same row. As a side note, the approach will work in any case, but for dividing items on the same row with group of columns (this easy switch is one of the main reason why LayoutManager has been introduced). Could you provide more details about the final outcome you want to achieve?Homograft
@thetonrifles exactly like this: ibb.co/gd7R0m. It has a header. Then below the header is a row of images with info that can you use your hand to swip left and right. Below that is another header. And again below it is another row of images with info that you can swipe.Acceptor
@Donato this seems to me the same case. The only difference is that one of your items type contains a RecyclerView as well. Probably, if your header is on top of every horizontal RecyclerView you can think about creating a custom view wrapping the RecyclerView header and in this way you won't need to use different view types at all. Also, depending on the scenario, you can decide to use a ScrollView for the whole layout instead of a vertical RecyclerView. This strictly depends on how many horizontal RecyclerView you are planning to include (dynamic or static layout?).Homograft
@MHSFisher glad it could be helpful for you :)Homograft
@thetonrifles I have the same issue, is this solution still relevant .I was checking airbnb epoxy . Is that better or would it be an overkill for this requirement. Please advice . Link if that is supposed to a different queston : #53475211Lavone
@thetonrifles In this process , how do we identify and notify when there is a change in the list . I use Livedata with around items shifting , and seems to be slow. I was checking a solution with DiffUtil to compare and update . But am unable to understand on how to compare different types of items when we have 2 different kinds of items in the list .Lavone
Can you please add an example something like this where they use DiffUtil github.com/googlesamples/android-architecture-components/blob/…Lavone
@Lavone I can provide an example. Actually DiffUtil requires to define a DiffUtil.Callback where you need to implement methods areItemsTheSame and areContentsTheSame. Since you are handling different types my expectation is that there you will first of all make a check on type. Notice that Such kind of implementation doesn't require LiveData. Displaying data in a RecyclerView is not related on how you retrieve the data. The data update can be dispatched in many other ways and this will not have impact on logic you use for updating the RecyclerView.Homograft
@thetonrifles got it .Livedata seems to be a different usecase altogether. So for those 2 methods , how do you normally compare since they are completely different objectsLavone
@Lavone areItemsTheSame can be implemented relying on equals method (in the implementation I assume you check the item type with instanceof or by comparing their Class). In areContentsTheSame you can rely on the fact that ListItems in the example have a type. If their type is different it means that content is surely different. If type is same you can compare visible contents after casting.Homograft
can we use interface instead of the abstract class in the above example?Loth
@ShikharJaiswal yes, you can do that as wellHomograft
C
12

You can try to use the library I've wrote to solve this problem in my project. Gradle dependency (needs jcenter repo included):

dependencies {
    //your other dependencies
    compile 'su.j2e:rv-joiner:1.0.3'//latest version by now
}

Then, in your situation, you can do smth like this:

//init your RecyclerView as usual
RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
rv.setLayoutManager(new LinearLayoutManager(this));

//construct a joiner
RvJoiner rvJoiner = new RvJoiner();
rvJoiner.add(new JoinableLayout(R.layout.today));
YourAdapter todayAdapter = new YourAdapter();
rvJoiner.add(new JoinableAdapter(todayAdapter));
rvJoiner.add(new JoinableLayout(R.layout.yesterday));
YourAdapter yesterdayAdapter = new YourAdapter();
rvJoiner.add(new JoinableAdapter(yesterdayAdapter));

//set join adapter to your RecyclerView
rv.setAdapter(rvJoiner.getAdapter());

When you need to add item, add it to appropriate adapter, like:

if (timeIsToday) {
    todayAdapter.addItem(item);//or other func you've written
} else if (timeIsYesterday) {
    yesterdayAdapter.addItem(item);
}

If you need to add new group to recycler view dynamically, you can use this methods:

rvJoiner.add(new JoinableLayout(R.layout.tomorrow));
YourAdapter tomorrowAdapter = new YourAdapter();
rvJoiner.add(new JoinableAdapter(tomorrowAdapter));

You can check this link for more library description. I can't say that it's surely the best way to achieve you goal, but it helps me sometimes.

UPD:

I've found the way to do this without using external libraries. Use RecyclerView.ItemDecoration class. For example, to group items by 3 item in group you can do this:

recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {

        private int textSize = 50;
        private int groupSpacing = 100;
        private int itemsInGroup = 3;

        private Paint paint = new Paint();
        {
            paint.setTextSize(textSize);
        }

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            for (int i = 0; i < parent.getChildCount(); i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildAdapterPosition(view);
                if (position % itemsInGroup == 0) {
                    c.drawText("Group " + (position / itemsInGroup + 1), view.getLeft(),
                            view.getTop() - groupSpacing / 2 + textSize / 3, paint);
                }
            }
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            if (parent.getChildAdapterPosition(view) % itemsInGroup == 0) {
                outRect.set(0, groupSpacing, 0, 0);
            }
        }
    });

Hope it helps.

Chumley answered 18/1, 2016 at 9:7 Comment(5)
It is not a big project and I'm new in the android development so I want to figure out as many as possible base things and best practices so I'll try to use google android libraries as long as possible in the project. But probably later I'll look for such library. Thanks!Scoles
@Scoles i've updated an answer to show the way of doing this using just google libs. take a look :)Chumley
The updated answer is very creative ! Way to use the new ItemDecoration, it's got support for older versions of android as well, I've used this code to generate a menu that looks like an iOS menuEserine
Is it possible to use the library you've written for a number of groups that is not fixed?Benitabenites
Hey! What is {paint.setTextSize(..);..} ? Where it was called?Hirschfeld
H
3

This question is from back in 2016, in the meantime (2020) there are different libraries available for grouping recycler views. The most popular ones:

Hume answered 14/6, 2020 at 21:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.