Converting TableLayout to RecyclerView

Viewed 244

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.

3 Answers

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.

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);
    }
}


}

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);

            ....
        }
...
Related