4

I'm trying to convert my TableLayout to a RecyclerView. The TableLayout is fine so far as it goes, but some of the tables have lots of rows, and are really slow to inflate, and I think that a RecyclerView would be the more efficient model to use (and would allow easy access to searching/filtering functionality).

The issue I'm having is that my TableLayout is defined as a set of custom views or compound controls, with a few standard views thrown in, like so:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    style="@style/MySection"
    android:id="@+id/section_cape" >

    <TableLayout style="@style/MyTable"
        android:id="@+id/table_cape" >

        <com.example.myapp.CompoundSwitch
            android:id="@+id/cape"
            custom:switch_label_tag="label_show"
            custom:switch_label_text="@string/label_show"
            custom:switch_indented="false"
            android:visibility="visible" />

        <TextView
            style="@style/MyTextView"
            android:id="@+id/info_capeNotAvailable"
            android:text="@string/info_notAvailable" />

        <com.example.myapp.CompoundSpinner
            android:id="@+id/capeProvider"
            custom:spinner_label_tag="label_variableProvider"
            custom:spinner_label_text="@string/label_provider"
            custom:spinner_indented="false"
            android:visibility="visible" />

        <com.example.myapp.CompoundSlider
            android:id="@+id/capeTransition"
            custom:slider_label_tag="label_providerTransition"
            custom:slider_label_text="@string/label_transition"
            custom:slider_indented="true"
            custom:slider_range="false"
            android:visibility="gone" />

        ... etc ...

Each of the custom views is defined as a TableRow.

I'm struggling to understand what to inflate in the onCreateViewHolder() method of my Adapter. Of course, each custom view does have a layout, but for some of these custom views, there are several layouts and a different layout is used depending on what custom attributes are defined in the xml.

So how do I map my rows of custom views (each with its own set of attributes) into the RecyclerView structure?

My current thinking is that I would have to move each row (custom view) statement into its own separate layout xml file, and then inflate the appropriate one of these in the onCreateViewHolder() method (based on viewType) but I'm not sure that this is the correct approach... and there would be a lot of very small layout files... seems unnecessarily unwieldy.

drmrbrewer
  • 11,491
  • 21
  • 85
  • 181
  • 1
    This is hard to visualize. Is each custom view a single _TableRow_? Is the XML you posted the primary layout that just goes on and on? Why so many layout files? I think your approach is sound, but you need to consider extracting the parameters of each layout file into the adapter so you can inflate a standard layout (or layouts) and programmatically feed it the information to customize it. For instance, you could extract the type and associated info from each custom view (_TableRow_) into a table accessible to the adapter and inflate the appropriate view based upon the adapter table. – Cheticamp Jun 01 '21 at 13:42
  • @Cheticamp yes, each custom view is a single `TableRow`. The XML I posted is one of several different "sections" that are currently inflated on demand and added to the main view... the table for each section doesn't goes on and on but for some sections there are lots of rows (slow to inflate that section). Are you effectively suggesting not having these individual custom views, and instead just selecting and inflating the appropriate layout directly from the adapter, based on the adapter table... effectively moving the inflation logic from the custom views to the adapter? – drmrbrewer Jun 01 '21 at 22:16
  • 1
    So the XML you posted would be a "section" or an item in a RecyclerView. I was thinking that the TableRows would be the sections. I think that you should go ahead and inflate these "sections" as is. You may want to examine them to see if they can be simplified by reducing layout depth/complexity. For instance, TableLayout is a LinearLayout, so why would it be wrapped in another LinearLayout? Once inflated, though, the RecyclerView will reuse the layout, so it won't have to be inflated a second time but will be reused. (I am assuming that section layouts can occur more than once.) – Cheticamp Jun 01 '21 at 23:07
  • @Cheticamp in order to avoid putting too much detail into the Question, I omitted to say that in fact I am *already* treating each "section" as an item or row in a `RecyclerView`. That's working well. Each `RecyclerView` row is actually a `TextView` (section heading) and a `LinearLayout` container (initially holding nothing)... when the section heading is clicked, the full section layout is inflated add added into the row's container... that full section layout is what you see in the original Question. – drmrbrewer Jun 02 '21 at 10:27
  • Trouble is, some sections (i.e. what you see in the Question) have a LOT of "rows" themselves. They take a long time to inflate when the user clicks on the section heading. So I'm trying to apply the `RecyclerView` treatment to each section too, so that each row (or sub-row) is only inflated as and when the user scrolls down. Maybe there's a better way... I've read about nested `RecyclerView`s being poor for performance, but I haven't seen an explanation as to why... each nested `RecyclerView` would be given a fixed height so that not all rows are populated initially. – drmrbrewer Jun 02 '21 at 10:31
  • Your other questions: Q: "see if they can be simplified by reducing layout depth/complexity. For instance, `TableLayout` is a `LinearLayout`, so why would it be wrapped in another `LinearLayout`?" A: some sections have stuff above and below the `TableLayout` so I am just being consistent by wrapping them all in a `LinearLayout`... I like to be consistent so that I can more easily search/replace later should I want to change something structural across all layouts. – drmrbrewer Jun 02 '21 at 13:58
  • Q2: "I am assuming that section layouts can occur more than once"... A2: actually, each section is unique... so I guess that the recycling part of `RecyclerView` isn't much use... I did start by using a `ListView` but discovered some horrible issues with it (e.g. `EditText` doesn't play well inside a `ListView`... plus I like that `RecyclerView` forces the `ViewHolder` model and appears to be under more active development. – drmrbrewer Jun 02 '21 at 14:01
  • 1
    Still hard to visualize. Maybe a RecyclerView isn't what you need. Don't know if this will help, but take a look at [AsyncLayoutInflater](https://developer.android.com/reference/androidx/asynclayoutinflater/view/AsyncLayoutInflater). You may be able to inflate your layouts in the background before they are called upon. – Cheticamp Jun 02 '21 at 14:17
  • That seems like a very good suggestion, and may be just what I need... rather than each section being inflated at the point of demand (user clicking on section heading), where the user will notice the inflation delay, they can be inflated in advance and shown immediately *if* the section heading is clicked. Sounds a lot more straightforward. Thanks for the suggestion. – drmrbrewer Jun 02 '21 at 14:28
  • @Cheticamp I forgot, the other reason for using a `RecyclerView` for each section was to access the "in built" filtering functionality of the `RecyclerView`, because ultimately I'm aiming to be able to filter out certain rows of each section. I've got filtering working nicely on the sections themselves, and would ideally like to extend the filtering into the section content too. – drmrbrewer Jun 02 '21 at 18:55
  • @Cheticamp I tried `AsyncLayoutInflater` but unfortunately this makes the scrolling stutter badly, at least until everything settles down. Even though layouts are being inflated in the background, I suppose it's possible that CPU for background processing will be causing delays to the UI? I'm certainly *not* getting any "Failed to inflate resource in the background! Retrying on the UI thread" log messages from `AsyncLayoutInflater` (which I saw at first), so it seems like layouts *are* actually being inflated in the background and yet still causing problems for the UI. – drmrbrewer Jun 03 '21 at 07:25
  • 1
    Well, I am sorry to hear that. Seems to me that there is deep nesting of your layouts that will just take time to inflate. The last thing I will suggest is to convert your top-level layouts to _ConstraintLayout_. You should be able to make your layout much flatter and faster to inflate. Maybe try this if the other answers don't pan out. – Cheticamp Jun 03 '21 at 13:20

3 Answers3

1

My current thinking is that I would have to move each row (custom view) statement into its own separate layout xml file, and then inflate the appropriate one of these in the onCreateViewHolder() method (based on viewType) but I'm not sure that this is the correct approach... and there would be a lot of very small layout files... seems unnecessarily unwieldy.

You're right, but with some small correction. It seems like you want to set different custom attributes for many custom Views. So, you don't need to create a lot of small layout files. Instead, you can move these attributes into the code.

  1. At first we define class RowDescriptor which will hold layoutId, factory of ViewHolder instances and the values of your custom attributes.
class RowDescriptor {
  int layoutId;
  RowViewHolder.Factory factory;
  Map<String, Object> values = new HashMap<>();

  public int getLayoutId() { return layoutId; }

  public RowViewHolder.Factory getFactory() { return factory; }

  RowDescriptor(int layoutId, RowViewHolder.Factory factory) {
    this.layoutId = layoutId;
    this.factory = factory;
  }

  public <T> T getValue(String key) {
    return (T) values.get(key);
  }

  public <T> RowDescriptor putValue(String key, T value) {
    values.put(key, value);
    return this;
  }
}
class RowViewHolder extends RecyclerView.ViewHolder {
  RowViewHolder(View itemView) {
    super(itemView);
  }

  void onBind(RowDescriptor row) {
    // for inheritors
  }

  interface Factory {
    RowViewHolder newInstance(View itemView);
  }
}
  1. Then do the trick inside the Adapter implementation. Method onCreateViewHolder() receive only int viewType, and we must convert it to layoutId and create corresponding ViewHolder. So, take a look at the method getItemViewType(), all the magic happens there.
class RowsAdapter extends RecyclerView.Adapter<RowViewHolder> {
  List<? extends RowDescriptor> rows;
  SparseIntArray layoutIdToViewType = new SparseIntArray();
  SparseIntArray viewTypeToLayoutId = new SparseIntArray();
  SparseArray<RowViewHolder.Factory> viewTypeToFactory = new SparseArray<>();

  void setData(List<RowDescriptor> rows) {
    this.rows = rows;
    notifyDataSetChanged();
  }

  int getItemCount() {
    return rows != null ? rows.size() : 0;
  }

  int getItemViewType(int position) {
    RowDescriptor row = rows.get(position);

    int viewType = layoutIdToViewType.get(row.getLayoutId());
    if (viewType == 0) {
      viewType = layoutIdToViewType.size() + 1;

      layoutIdToViewType.put(row.getLayoutId(), viewType);
      viewTypeToLayoutId.put(viewType, row.getLayoutId());
      viewTypeToFactory.put(viewType, row.getFactory());
    }

    return viewType;
  }

  RowViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext())
      .inflate(viewTypeToLayoutId.get(viewType), parent, false);

    return viewTypeToFactory.get(viewType).newInstance(view);
  }

  void onBindViewHolder(RowViewHolder holder, int position) {
    holder.onBind(rows.get(position));
  }
}
  1. Then create needed layouts and corresponding ViewHolder classes. IMPORTANT: same ViewHolder class may be used with different layouts, but same layout can not be used with different ViewHolder classes!

res/layout/row1.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal">

  <TextView
    android:id="@+id/some_value"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1" />

  <TextView
    android:id="@+id/label"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1" />
</LinearLayout>
class FirstRowViewHolder extends RowViewHolder {
  TextView someValueView;
  TextView labelView;

  FirstRowViewHolder(View itemView) {
    super(itemView);
    someValueView = itemView.findViewById(R.id.some_value);
    labelView = itemView.findViewById(R.id.label);
  }

  void onBind(RowDescriptor row) {
    Integer someValue = row.getValue("some_value");
    someValueView.setText(String.valueOf(someValue));

    String label = row.getValue("label");
    labelView.setText(label);
  }
}

res/layout/row2.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal">

  <TextView
    android:id="@+id/another_value"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1" />

  <TextView
    android:id="@+id/description"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1" />
</LinearLayout>
class SecondRowViewHolder extends RowViewHolder {
  TextView anotherValueView;
  TextView descriptionView;

  SecondRowViewHolder(View itemView) {
    super(itemView);
    anotherValueView = itemView.findViewById(R.id.another_value);
    descriptionView = itemView.findViewById(R.id.description);
  }

  void onBind(RowDescriptor row) {
    Integer anotherValue = row.getValue("another_value");
    anotherValueView.setText(String.valueOf(anotherValue));

    String description = row.getValue("description");
    descriptionView.setText(description);
  }
}
  1. Eventually, link everything together.
class MainActivity extends AppCompatActivity {
  void onCreate(Bundle state) {
    super.onCreate(state);

    List<RowDescriptor> rows = new ArrayList<>();

    rows.add(new RowDescriptor(R.layout.row1, FirstRowViewHolder::new)
        .putValue("some_value", 42)
        .putValue("label", "hello"));

    rows.add(new RowDescriptor(R.layout.row2, SecondRowViewHolder::new)
        .putValue("another_value", 24)
        .putValue("description", "world"));

    RowsAdapter adapter = new RowsAdapter();
    adapter.setData(rows);

    RecyclerView recyclerView = new RecyclerView(this);
    recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
    recyclerView.setAdapter(adapter);

    setContentView(recyclerView);
  }
}

You can improve this solution by avoiding usage of Map for attribute values. Instead, you can create one more class hierarchy for entity, which will hold attribute values for each of your custom rows.

Compilable and workable project is here.

Oleksii K.
  • 5,359
  • 6
  • 44
  • 72
  • Are you some sort of genius? This seems very neat. Need to get my head around it completely but it looks promising! At the moment I have several compound control classes defined (e.g. the `CompoundSwitch` of the original post), and these not only inflate the right layout (depends on `attrs` passed in) but they also have convenient setters and getters etc for setting/querying state of the internal control (differs between different variants of Switch). I guess that no longer fits with the model you're proposing? Or can the `RowViewHolder` kinda take on this role? – drmrbrewer Jun 03 '21 at 18:55
  • I changed your example to something a bit more like my own use case, with each row type having an individual label and an associated control (of various types, e.g. `EditText`, `CheckBox`, `Slider`, `Spinner`)... see here: https://bitbucket.org/drmrbrewer/adapter-view-type-trick/src/master/. Is there a way to retain some of the benefits of the compound control approach, where the entire row is treated as a control element in itself? Or maybe I have to ditch that and just query the underlying control (e.g. checkbox) directly. – drmrbrewer Jun 04 '21 at 08:21
  • Can't help feeling that the following answer to a different question might be of assistance here: https://stackoverflow.com/a/48123513/4070848 – drmrbrewer Jun 04 '21 at 14:00
  • I awarded the bounty to you, as the answer which was most helpful and guided me in the right direction. I modified your suggested approach so that in `onCreateViewHolder()` I am not inflating any layout directly but instead am creating the appropriate custom view, which does the inflation itself. That approach is based on the link in my previous comment. See the bitbucket repo for what I ended up with (note that I also extract the attributes from XML to make migration easier, i.e. migrating from adding my custom views in the XML layout, to adding my custom views programmatically). – drmrbrewer Jun 07 '21 at 08:32
  • 1
    I apologise for late response. I hope you'll find the best solution for your specific requirements. Good luck =) – Oleksii K. Jun 09 '21 at 15:17
0

I created a sample adapter class for you to give you an idea about the usage of different viewType.

Let me know if you have further questions.

class ModelAdapter extends RecyclerView.Adapter {
static final int TYPE_ONE = 1;
static final int TYPE_TWO = 2;
static final int TYPE_THREE = 3;
List<Model> modelList;
Context context;

public ModelAdapter(List<Model> modelList, Context context) {
    this.modelList = modelList;
    this.context = context;

}

@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
    switch (i) {
        case TYPE_TWO:
            return new MyViewHolder2(LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.list_item2, viewGroup, false));
        case TYPE_THREE:
            return new MyViewHolder3(LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.list_item3, viewGroup, false));
        case TYPE_ONE:
        default:
            return new MyViewHolder(LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.list_item, viewGroup, false));
    }

}

@Override
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, int i) {
    Model data = modelList.get(i);
    switch (modelList.get(i).viewType) {
        case TYPE_TWO:
            ((MyViewHolder2) viewHolder).title.setText(data.title);
        case TYPE_THREE:
            ((MyViewHolder3) viewHolder).title.setText(data.title);
        default:
            ((MyViewHolder) viewHolder).title.setText(data.title);
    }

}

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

@Override
public int getItemViewType(int position) {
    return modelList.get(position).viewType;
}

class MyViewHolder extends RecyclerView.ViewHolder {
    TextView title;


    public MyViewHolder(View itemView) {
        super(itemView);

        title = itemView.findViewById(R.id.title_tv);
    }
}

class MyViewHolder2 extends RecyclerView.ViewHolder {
    TextView title;


    public MyViewHolder2(View itemView) {
        super(itemView);

        title = itemView.findViewById(R.id.title_tv2);
    }
}

class MyViewHolder3 extends RecyclerView.ViewHolder {
    TextView title;


    public MyViewHolder3(View itemView) {
        super(itemView);

        title = itemView.findViewById(R.id.title_tv3);
    }
}


}
Dinkar Kumar
  • 2,175
  • 2
  • 12
  • 22
-1

If you have the data ready you can use HashMap using the position and check in the onBindViewHolder() that if there is any item in the HashMap for this

declare in Activity a HashMap like

public static HashMap<Integer , ArrayList<String>> subList = new HashMap<>();

In the RecyclerView

...
    @Override
      public void onBindViewHolder(ContactsAdapter.ViewHolder holder, int position) {
            String data                  = arrayList.get(position);
            RecyclerView recyclerView    = holder.recyclerView;
    
            if(MainActivity.subList.get(position) != null)
            {
                  SubAdapter  subAdapter       = new SubAdapter(context , 
                  MainActivity.subList.get(position));
                  recyclerView.setAdapter(subAdapter);
                  recyclerView.setVisibility(View.VISIBLE);
            }
            else
               recyclerView.setVisibility(View.GONE);

            ....
        }
...
Dharman
  • 30,962
  • 25
  • 85
  • 135
Ahmad
  • 437
  • 1
  • 4
  • 12
  • The problem I'm having, as stated in the original question, mainly relates to what I should be inflating in the `onCreateViewHolder()` method of my `Adapter`. – drmrbrewer May 30 '21 at 19:36