55

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

Leo
  • 1,683
  • 2
  • 20
  • 25
  • 1
    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. – andrea.petreri Jan 18 '16 at 07:45
  • @thetonrifles You're right UI should be as simple as possible, it seems I forgot it when I was asking the question, thanks! – Leo Jan 20 '16 at 05:57

3 Answers3

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.

andrea.petreri
  • 4,137
  • 2
  • 22
  • 21
  • 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 exist – Leo Jan 18 '16 at 15:51
  • @Leo 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. – andrea.petreri Jan 18 '16 at 16:08
  • 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 changed – Leo Jan 18 '16 at 16:35
  • 4
    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: http://stackoverflow.com/questions/31273203/what-is-the-best-practice-to-group-items-into-cardview/34692798 – Csabi Jul 13 '16 at 06:09
  • 1
    @Csabi 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. – andrea.petreri Jul 13 '16 at 07:36
  • @thetonrifles don't HeaderItem and EventItem need to extend the abstract ListItem class? – Eric H Oct 05 '16 at 00:43
  • @EricH you're right! Interesting that nobody including me noticed this issue :) btw I fixed it. Thanks a lot! – andrea.petreri Oct 05 '16 at 07:53
  • guys can you please share a link to the full source code because it really tricky on using these snippets only.. – kinsley kajiva Jan 01 '17 at 18:41
  • @kinsleykajiva I have [this public repo on GitHub](https://github.com/thetonrifles/android-recycler-grid) 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). – andrea.petreri Jan 04 '17 at 21:02
  • 1
    @kinsleykajiva I had some time this evening. [Here](https://github.com/thetonrifles/stackoverflow/tree/so-34848401) is a branch with a sample project reporting same example of the answer. Hope this could help. – andrea.petreri Jan 04 '17 at 22:23
  • @kinsleykajiva glad it was helpful :) ... of course if you have questions let me know! – andrea.petreri Jan 06 '17 at 20:37
  • @thetonrifles ok its fine. – kinsley kajiva Jan 06 '17 at 20:58
  • This will not work for a horizontal layout. They will not be divided into rows. Headers and content will appear in same row. – Daniel Viglione Oct 17 '17 at 21:37
  • @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? – andrea.petreri Oct 17 '17 at 21:43
  • @thetonrifles exactly like this: https://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. – Daniel Viglione Oct 17 '17 at 22:55
  • @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?). – andrea.petreri Oct 18 '17 at 09:37
  • @MHSFisher glad it could be helpful for you :) – andrea.petreri Dec 10 '17 at 13:24
  • @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 : https://stackoverflow.com/questions/53474711/airbnb-epoxy-for-gmail-like-dashboard-interface – 1nullpointer Nov 26 '18 at 05:57
  • @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 . – 1nullpointer Dec 06 '18 at 11:59
  • Can you please add an example something like this where they use DiffUtil https://github.com/googlesamples/android-architecture-components/blob/master/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java#L44 – 1nullpointer Dec 06 '18 at 12:03
  • @1nullpointer 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`. – andrea.petreri Dec 06 '18 at 20:31
  • @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 objects – 1nullpointer Dec 07 '18 at 06:10
  • @1nullpointer `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 `ListItem`s 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. – andrea.petreri Dec 07 '18 at 16:17
  • can we use interface instead of the abstract class in the above example? – Shikhar Oct 14 '19 at 06:15
  • @ShikharJaiswal yes, you can do that as well – andrea.petreri Jan 13 '20 at 10:23
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.

Maks
  • 7,562
  • 6
  • 43
  • 65
j2esu
  • 1,647
  • 1
  • 16
  • 26
  • 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! – Leo Jan 18 '16 at 15:54
  • 1
    @Leo i've updated an answer to show the way of doing this using just google libs. take a look :) – j2esu Jan 18 '16 at 18:47
  • 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 menu – Arjun Jan 12 '17 at 16:27
  • Is it possible to use the library you've written for a number of groups that is not fixed? – Marcos Guimaraes Sep 17 '17 at 22:44
  • Hey! What is {paint.setTextSize(..);..} ? Where it was called? – AlexS Dec 09 '19 at 05:14
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:

bytesculptor
  • 411
  • 1
  • 4
  • 21