1

I have a default layout that holds a bunch of blank CardViews in a RecyclerView list, basically a welcome screen for the user to show them what CardViews look like. The user then launches an input screen for some data and clicks a "Save" button to save the data into a CardView. Once the user clicks Save, the layout should change from the default layout with the blank CardViews to the new, single CardView that contains the user data. Later, if the user deletes all of their CardViews, then the view should switch back to the default blank CardViews.

I'm struggling with how to set the code int the Adapter in the onCreateViewHolder because getItemCount() will already have a positive value for the default (because the RecyclerView list will already have 4 or 5 blank CardViews in it) which would conflict later with the same getItemCount() amount once the user creates 4 or 5 CardViews. Any ideas on how to set a default layout and then switch to a new layout that can then revert back to the default layout if the list is emptied of user-created CardViews?

Below is my failed attempt at laying out a test for two layouts in the Adapter. I realized it would not work because the default layout never had an ItemCount of zero since there are already 4 or 5 blank CardViews:

...
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {

private List<ContactInfo> contactList;

public ContactAdapter(List<ContactInfo> contactList) {
    this.contactList = contactList;
}

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

@Override
public ContactViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
    if(contactList.size()== 0) {
        View itemView = LayoutInflater.
                from(viewGroup.getContext()).
                inflate(R.layout.defaultcard_layout, viewGroup, false);
        return new ContactViewHolder(itemView);
    }
    else {
        View itemView = LayoutInflater.
                from(viewGroup.getContext()).
                inflate(R.layout.singlecard_layout, viewGroup, false);
        return new ContactViewHolder(itemView);
    }
}

revised Adapter and removeItem code:

...
private LayoutInflater mLayoutInflater;
private List<Contact> mContacts;
private OnItemTapListener mOnItemTapListener;

public ListContactsAdapter(Context context, List<Contact> contacts) {
    Context mContext;
    mContext = context;
    mLayoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mContacts = contacts;
}

public void removeItem(Contact contact, int position) {
    mContacts.remove(contact);
    if (mContacts.size()==0) {
        // if no more contacts in list,
        // we rebuild from scratch
        mContacts.clear();
        notifyDataSetChanged();
    } else {
        // else we just need to remove
        // one item
        mContacts.remove(position);
        notifyItemRemoved(position);
    }
}
AJW
  • 1,578
  • 3
  • 36
  • 77
  • You should implement method getItemViewType. This should return 2 as value, since you need to handle two different kind of views. Then you should be able to determine type for each list element and use it for building layout. See [here](http://stackoverflow.com/questions/34848401/divide-elements-on-groups-in-recyclerview) for an example of implementation. – andrea.petreri Jan 22 '16 at 00:27
  • Ok, I'll take a look. – AJW Jan 22 '16 at 00:31
  • Ok, let me know if this might work: set up a unique ViewType for the default layout. Then when user clicks Save to create a new CardView, then use the second layout. Later, if the user deletes all of the CardViews in the second layout, then getItemCount() will be zero for that ViewType and code will switch to the default layout and refresh the view. – AJW Jan 22 '16 at 00:41
  • I support @thetonrifles suggestion. Since the two layouts differ in their itemCount ( first you have something like 4, 5 empty cards, and then just one single card with some information on it ), you should make a distinction between the two states by implementing two view types. Associate each view type with a different ViewHolder, like EmptyCardViewHolder for the first card state and then something with a better name for the BigCardViewHolder. Notice that in the code that you posted, the 'int i' parameter of `onCreateViewHolder` is actually a viewType using which you'll decide which ViewHolde – Rany Albeg Wein Jan 22 '16 at 01:41
  • ...you should return. – Rany Albeg Wein Jan 22 '16 at 01:42
  • @Rany Albeg Wein Ok, I will try that. Does the onBindViewHolder method also get affected by the two viewTypes or just the onCreateViewHolder method? – AJW Jan 22 '16 at 01:58
  • @AJW Also onBindViewHolder. That's because, depending on view type you will fill the view in a different way. – andrea.petreri Jan 22 '16 at 07:00
  • @AJW I provided you a possible implementation in answer. – andrea.petreri Jan 22 '16 at 07:17
  • removeItem error came from "mContacts.remove(position);". Corrected code is "mContacts.remove(contact);. – AJW Feb 21 '16 at 02:54

1 Answers1

2

This is approach you could follow:

  1. 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_EMPTY = 0;
        public static final int TYPE_CONTACT = 1;
    
        abstract public int getType();
    } 
    
  2. Define a class for each of your List element type:

    public class EmptyItem {
    
        @Override
        public int getType() {
            return TYPE_EMPTY;
        }
    
    }
    
    public class ContactItem {
    
        private ContactInfo contact;
    
        // here getters and setters 
        // for title and so on, built 
        // using contact
    
        public ContactItem(ContactInfo info) {
            this.contact = info;
        }
    
        @Override
        public int getType() {
            return TYPE_CONTACT;
        }
    
    }
    
  3. Create your list. In logic below I'm just ensuring you will always have at least 5 elements. In case you have less than 5 contacts, empty layout will be displayed. Every time you modify your contactList from the outside Activity, such modification will be available even in mContactList because the adapter keeps a reference to the same List managed in the Activity (see Adapter constructor). In case for example you add a new contact, after you just need to invoke updateContactList method for having your UI updated.

    List<ContactInfo> mContactList;
    List<ListItem> mItems;
    
    public ContactsAdapter(List<ContactInfo> contactList) {
        mContactList = contactList;
        mItems = buildContactsList(mContactList);       
    }
    
    // Method for building ui list.
    private List<ContactItem> buildContactsList(List<ContactInfo> contactList) {
        List<ContactItem> list = new ArrayList<>();
        for (ContactInfo contact : contactList) {
            list.add(ContactItem(contact));
        }
        if (list.size() < 5) {
            for (int i=list.size(); i<5; i++) {
                list.add(EmptyItem());
            }
        }
    }
    
    // Method for updating contact list, providing
    // a new one. Everything to be build from scratch.
    public void updateContactsList() {
        mItems.clear();
        mItems.addAll(buildContactsList(mContactList));
        notifyDataSetChanged();
    }
    
  4. Define an adapter for your RecyclerView, working on List defined at point 3. 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 empty and contact items. Adapter methods should take care of this accordingly:

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == ListItem.TYPE_EMPTY) {
            View itemView = mLayoutInflater.inflate(R.layout.defaultcard_layout, parent, false);
            return new EmptyViewHolder(itemView);
        } else {
            View itemView = mLayoutInflater.inflate(R.layout.singlecard_layout, parent, false);
            return new ContactViewHolder(itemView);
        }
    }
    
    
    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
        int type = getItemViewType(position);
        if (type == ListItem.TYPE_EMPTY) {
            EmptyItem header = (EmptyItem) mItems.get(position);
            EmptyViewHolder holder = (EmptyViewHolder) viewHolder;
            // your logic here... probably nothing to do since it's empty
        } else {            
            ContactItem event = (ContactItem) mItems.get(position);
            ContactViewHolder holder = (ContactViewHolder) viewHolder;
            // your logic here
        }
    }
    

In case of updates on contactList you should of course update mItems accordingly by cleaning it, filling it again with same logic reported at point 3 and then notifyDataSetChanged on the Adapter.

andrea.petreri
  • 4,137
  • 2
  • 22
  • 21
  • Very cool, thank you. I will try to implement this approach over the next few nights when I have some spare time and will then check back. – AJW Jan 25 '16 at 02:34
  • Ok, but point 3 may not fit. When user reaches RecyclerView screen for the first time they see the default layout which is five blank cards (using CardView). After they click "+" on the toolbar they create a new, custom card and click "Save". I then want to show only their new, single card on the RecyclerView screen (the second layout). User can continue to add cards and the layout will update with the new cards. If the user deletes all of their cards, I would like them to be sent back to the first screen they saw (the default layout with five blank cards). Please advise. – AJW Jan 26 '16 at 03:36
  • So, the first layout will always have five blank cards as the default. Five fixed cards. The second layout starts with one user-created card and then the number of cards can increase or decrease, but if the user deletes all of their cards, (my thought: itemcount reaches zero for that viewType?) then the RecyclerView screen would go back to showing the default screen with the five blank cards. – AJW Jan 26 '16 at 03:44
  • I should have said use RecyclerView's "getItemCount ()" rather than "itemcount" above. – AJW Jan 26 '16 at 06:19
  • @AJW I've updated code at point 3, showing a possible implementation for adapter. Keeping in the adapter a reference to the contactList, ensures you will always have it up to date. When from outside you add or delete an item, after you just need to invoke `updateContactsList()` on the adapter and the ui will automatically update. Of course there are even other options for doing this. This is just a possible approach. Let me know if everything is clear or if I missed something from your comments. – andrea.petreri Jan 26 '16 at 07:43
  • I will give it a try and let you know...thanks for your help with this! – AJW Jan 26 '16 at 23:41
  • @AJW Glad if I could help! In case you need additional clarifications or sample code, just let me know. – andrea.petreri Jan 26 '16 at 23:42
  • One issue in point 3. I don't understand the sample related to "EmptyItem". Why would the list of items be added to if the list size is <5? Since the 5 cards are fixed in the default layout I never want that layout to change. So I think the "EmptyItem" type would never be added to. Only the ContactItem type would be updated when the user keeps adding new cards to the list. If the user started deleting cards in the list, eventually the list for the ContactItems woud reach zero and then I want the UI screen to revert back to the default layout to show the 5 blank cards again.Please advise. – AJW Jan 26 '16 at 23:57
  • @AJW Maybe problem here is that I didn't understood very well the final outcome you want to get. My understanding is that you want to always show at least 5 elements. In case you have less than 5 you fill the remaining space with empty items (that's why I put condition <5). In case you want to show 5 empty items just in case there are no contacts, you just need to turn condition into list.size()==0. When you say that I should just add contact items this is true... but if you look at updateContactsList method, you'll see that it builds mItems from scratch when an update is made. – andrea.petreri Jan 27 '16 at 06:45
  • @AJW I've added more details at point 3 regarding UI update when editing contact list. Hope this could help. – andrea.petreri Jan 27 '16 at 06:49
  • @AJW [Here on GitHub](https://github.com/thetonrifles/android-recycler-view) I've put a sample application implementing list you asked. In particular when launching the application, choose List Layout option (not Grid, that is another example). The sub-package with code you are interested in is [here](https://github.com/thetonrifles/android-recycler-view/tree/master/app/src/main/java/com/thetonrifles/recyclerviewsample/list). – andrea.petreri Jan 27 '16 at 07:29
  • Ok I will give it a try and then report back. – AJW Jan 27 '16 at 22:39
  • Ok a few questions: In Adapter, will "mItems.clear()" cause a slowdown when list has hundreds/thousands of items? is there any way to avoid clearing the list each time an item is added (add and update rather than clear and addAll)? Also, in Adapter, why test that mContacts is both !=null and size is >0? if one is true doesn't that mean the other must be also be true? Thanks! – AJW Jan 28 '16 at 04:36
  • @AJW Yes, it is possible to optimize insertion of new elements for a matter of performances. Of course logic for updating list depends on your requirements (e.g. you can add elements on bottom or keep the list sorted and insert new element in a specific place). In this case you will just add a single item and invoke notifyItemInserted. If you look at repository I shared with you, there is another list implementing sorting (package sort). There the list is updated just by inserting new items, without rebuilding list from scratch. – andrea.petreri Jan 28 '16 at 06:21
  • @AJW Yes, condition mContacts!=null is useless in this case, but in general if your list is not null it doesn't mean that its size is >0, so it's better to check if list is null or not before invoking a method on it. – andrea.petreri Jan 28 '16 at 06:25
  • @AJW I've updated activity and (mainly) adapter code in [repo](https://github.com/thetonrifles/android-recycler-view/tree/master/app/src/main/java/com/thetonrifles/recyclerviewsample/list), for avoiding to re-create the list from scratch when adding / removing items. Hope this could help! – andrea.petreri Jan 28 '16 at 07:12
  • Ok making good progress. Questions: does OnItemTapListener handle both mouse clicks and touches? Is Contact class just to hold the strings in each Item? What does the "public Boolean equals (Object o)..." code do? – AJW Jan 29 '16 at 02:41
  • @AJW `equals` method is used in this case by `List` for implementing comparisons in method. When you do things like `mContacts.remove(contact);`, contact to be removed is the one _equals_ to the input parameter. In this example `Contact` is just a POJO class, including params and getters/setters method for accessing them. Of course it is possible to implement even additional operations, depending on your needs. What do you mean with _mouse clicks_? – andrea.petreri Jan 29 '16 at 06:25
  • Mouse clicks are clicks from a pointing device. Mouse is usually connected to a personal computer. Looking good here for the layouts. How do I add scrollToPosition(0) to the "addItem" code in the Adapter? I want to insert a new item at the top of the list and then have the View show the top of that list. Do I need a reference to the ListContactsActivity that is the RecyclerView? – AJW Feb 02 '16 at 03:51
  • @AJW If you want to insert item on top of the list and scroll to that position you should do a couple of things. In `ContactsAdapter` you should rework `addItem` logic for inserting new contact on top (now it is inserted on bottom). In `ListContactsActivity` you should use `scrollToPosition` method after `mContactsAdapter.addItem(contact)` (so you don't need a reference to the RecyclerView inside Adapter. – andrea.petreri Feb 02 '16 at 06:54
  • Hi, struggling with one remaining CardView issue. I think the error has something to do with the adapter. Would appreciate if you could take a look and provide any thoughts...see this post which lists out the current code you had helped from above: http://stackoverflow.com/questions/35472971/android-why-is-cardview-not-updating-with-new-user-input. Also: http://stackoverflow.com/questions/35497770/toast-gets-intent-data-why-doesnt-cardview-in-recyclerview – AJW Feb 20 '16 at 18:42
  • @AJW ok... I'm going to check – andrea.petreri Feb 20 '16 at 19:07
  • The second link lists out my current Adapter code so is the best one to look at for the issue I'm trying to solve. – AJW Feb 20 '16 at 19:13
  • @AJW ok... I've wrote you a comment on first question link. My feeling is that there is something wrong with adapter notification when inserting new item. You can try with approach I suggested. In case it doesn't work try first of all to replace `notifyItemInserted(0)` with `notifyDataSetChanged`. In case this works it means problem is exactly with notification method and it's just a matter of finding the proper position of item to notify... of course I can help you in finding it :) – andrea.petreri Feb 20 '16 at 19:16
  • Ok, I changed to notifyDataSetChanged() in the addItem method and it worked! – AJW Feb 20 '16 at 19:23
  • @AJW Ok great... so it means problem is just with notification (I mean, your data is correct). `notifyItemInserted(0)` doesn't work? – andrea.petreri Feb 20 '16 at 19:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/104057/discussion-between-thetonrifles-and-ajw). – andrea.petreri Feb 20 '16 at 19:24
  • I switched to your simpler GitHub for the adapter. One issue is the removeItem method is crashing the app when I click on a CardView to remove it. The exception says IndexOutOfBoundsException: Invalid index 1, size is 1. I will show the removeItem code above in my original question. Any ideas here? – AJW Feb 20 '16 at 23:57