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