Reputation: 3552
Items/cells of my multiple view type RecyclerView Adapter get shuffles on the scroll. I go through all below possible solutions but none of them working.
1.
@Override
public long getItemId(int position) {
return mDataset.get(position).hashCode();
or
return mDataset.get(position).getBaseFormElementId();
}
setHasStableIds(true);
Please share if anyone has any solution.
My Adapter Class
public class FormAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private Context mContext;
private List<BaseFormElement> mDataset;
public FormAdapter(Context context) {
mContext = context;
mDataset = new ArrayList<>();
setHasStableIds(true);
}
public List<BaseFormElement> getDataset() {
return mDataset;
}
public OnFormElementValueChangedListener getValueChangeListener() {
return mListener;
}
@Override
public int getItemCount() {
return mDataset.size();
}
@Override
public int getItemViewType(int position) {
return mDataset.get(position).getType();
}
@Override
public long getItemId(int position) {
//return super.getItemId(position);
return mDataset.get(position).getBaseFormElementId();
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// get layout based on header or element type
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v;
switch (viewType) {
case BaseFormElement.TYPE_HEADER:
v = inflater.inflate(R.layout.form_element_header, parent, false);
return new FormElementHeader(v);
case BaseFormElement.TYPE_EDIT_TEXT:
v = inflater.inflate(R.layout.form_element, parent, false);
return new FormElementEditTextViewHolder(v, new FormItemEditTextListener(this));
case BaseFormElement.TYPE_PICKER_DATE:
v = inflater.inflate(R.layout.form_element, parent, false);
return new FormElementPickerDateViewHolder(v, mContext, this);
case BaseFormElement.TYPE_PICKER_TIME:
v = inflater.inflate(R.layout.form_element, parent, false);
return new FormElementPickerTimeViewHolder(v, mContext, this);
case BaseFormElement.TYPE_PICKER_SINGLE:
v = inflater.inflate(R.layout.form_element, parent, false);
return new FormElementPickerSingleViewHolder(v, mContext, this, new FormItemEditTextListener(this));
case BaseFormElement.TYPE_PICKER_MULTI:
v = inflater.inflate(R.layout.formelementlabel, parent, false);
return new FormElementPickerMultiViewHolder(v, mContext, this);
case BaseFormElement.TYPE_IMAGE_REMARKS:
v = inflater.inflate(R.layout.formelementlabel, parent, false);
return new FormElementImageWithRemarksViewHolder(v, mContext, this);
case BaseFormElement.TYPE_SWITCH:
v = inflater.inflate(R.layout.form_element_switch, parent, false);
return new FormElementSwitchViewHolder(v, mContext, this);
case BaseFormElement.TYPE_SEGMENT:
v = inflater.inflate(R.layout.form_element_switch, parent, false);
return new FormElementSwitchViewHolder(v, mContext, this);
case BaseFormElement.TYPE_LABEL:
v = inflater.inflate(R.layout.formelementlabel, parent, false);
return new FormElementLabelViewHolder(v, clicklistner);
case BaseFormElement.TYPE_IMAGE:
v = inflater.inflate(R.layout.formelementlabel, parent, false);
return new FromElementImageViewHolder(v, mContext, this);
case BaseFormElement.TYPE_VIDEO:
v = inflater.inflate(R.layout.formelementlabel, parent, false);
return new FormElementVideoViewHolder(v, mContext, this);
case BaseFormElement.DIALOG_LIST:
v = inflater.inflate(R.layout.form_element_mmv, parent, false);
return new FormElementDialogListViewHolder(v, mContext, this);
case BaseFormElement.TYPE_SLIDER:
v = inflater.inflate(R.layout.formelementlabel, parent, false);
return new FormElementSliderViewHolder(v, mContext, this);
default:
v = inflater.inflate(R.layout.form_element, parent, false);
return new FormElementEditTextViewHolder(v, new FormItemEditTextListener(this));
}
}
@Override
public void onBindViewHolder(BaseViewHolder holder, final int position) {
// gets current object
BaseFormElement currentObject = mDataset.get(position);
holder.bind(position, currentObject, mContext);
}
}
Upvotes: 0
Views: 1457
Reputation: 3349
I have worked on this type of RecyclerView
and here is the DynamicAdapter
class used with multiple view-type
. I hope you get some clarity on how to use it.
public class DynamicAdapter extends RecyclerView.Adapter <BaseViewHolder> {
private static final String TAG = "DynamicAdapter";
private static final int VIEW_EMPTY = 0;
private static final int VIEW_EDIT_TEXT = 1;
private static final int VIEW_SIGNATURE = 2;
private static final int VIEW_UPLOAD = 3;
private static final int VIEW_DATE_TIME = 4;
private static final int VIEW_DATE_RANGE = 5;
private static final int VIEW_DATE = 6;
private static final int VIEW_DESCRIPTION = 7;
private static final int VIEW_UNDEFINED = 8;
ArrayList <FormData> formDataList;
private Context context;
private AdapterListener adapterListener;
public DynamicAdapter(ArrayList <FormData > formDataList) {
this.formDataList = formDataList;
}
public void setAdapterListener(AdapterListener adapterListener) {
this.adapterListener = adapterListener;
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
this.context = parent.getContext();
switch (viewType) {
case VIEW_EDIT_TEXT:
ItemDynamicFormEdittextBinding edittextBinding = ItemDynamicFormEdittextBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new EditTextViewHolder(edittextBinding);
case VIEW_NUMBER:
ItemDynamicFormNumberBinding numberBinding = ItemDynamicFormNumberBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new NumberViewHolder(numberBinding);
case VIEW_EMAIL:
ItemDynamicFormEmailBinding emailBinding = ItemDynamicFormEmailBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new EmailViewHolder(emailBinding);
case VIEW_UPLOAD:
ItemDynamicFormUploadBinding uploadBinding = ItemDynamicFormUploadBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new UploadViewHolder(uploadBinding);
case VIEW_DATE_RANGE:
ItemDynamicFormDateRangeBinding rangeBinding = ItemDynamicFormDateRangeBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new DateRangeViewHolder(rangeBinding);
case VIEW_DATE_TIME:
ItemDynamicFormDateTimeBinding dateTimeBinding = ItemDynamicFormDateTimeBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new DateTimeViewHolder(dateTimeBinding);
case VIEW_DATE:
ItemDynamicFormDateBinding dateBinding = ItemDynamicFormDateBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new DateViewHolder(dateBinding);
case VIEW_DESCRIPTION:
ItemDynamicFormDescriptionBinding descriptionBinding = ItemDynamicFormDescriptionBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new DescriptionViewHolder(descriptionBinding);
case VIEW_UNDEFINED:
ItemDynamicFormUnknownBinding unknownBinding = ItemDynamicFormUnknownBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new UnknownViewHolder(unknownBinding);
default:
ItemDynamicFormEmptyViewBinding emptyViewBinding = ItemDynamicFormEmptyViewBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new EmptyViewHolder(emptyViewBinding);
}
}
@Override
public void onBindViewHolder(@NonNull BaseViewHolder holder, final int position) {
holder.onBind(position);
}
@Override
public int getItemCount() {
return formDataList.size();
}
@Override
public int getItemViewType(int position) {
if (formDataList != null && !formDataList.isEmpty()) {
if (formDataList.get(position) != null &&
formDataList.get(position).getType() != null) {
switch (formDataList.get(position).getType()) {
case TEXT:
return VIEW_EDIT_TEXT;
case CAMERA:
return VIEW_CAMERA;
case TOGGLE:
return VIEW_TOGGLE;
case NUMBER:
return VIEW_NUMBER;
case EMAIL:
return VIEW_EMAIL;
case DATE_RANGE:
return VIEW_DATE_RANGE;
case DATE_TIME:
return VIEW_DATE_TIME;
default:
return VIEW_UNDEFINED;
}
} else {
return VIEW_UNDEFINED;
}
} else {
return VIEW_EMPTY;
}
}
/**
* Class used to handle all the text fields for email,text & number.
*/
private class EditTextViewHolder extends BaseViewHolder {
ItemDynamicFormEdittextBinding mBinding;
EditTextViewHolder(ItemDynamicFormEdittextBinding binding) {
super(binding.getRoot());
Log.e(TAG, "EditTextViewHolder: ------------>>");
this.mBinding = binding;
mBinding.edDynamicFormText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
}
@Override
public void afterTextChanged(Editable editable) {
}
});
}
@Override
public void onBind(int position) {
final FormData formData = formDataList.get(position);
FormEdittextViewModel emptyItemViewModel = new FormEdittextViewModel(formData);
mBinding.setViewModel(emptyItemViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
}
/**
* Class used to handle all the text fields for email, text & number.
*/
private class NumberViewHolder extends BaseViewHolder {
ItemDynamicFormNumberBinding mBinding;
NumberViewHolder(ItemDynamicFormNumberBinding binding) {
super(binding.getRoot());
this.mBinding = binding;
mBinding.edDynamicFormNumber.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
}
@Override
public void afterTextChanged(Editable editable) {
}
});
}
@Override
public void onBind(int position) {
final FormData formData = formDataList.get(position);
mBinding.setViewModel(new FormNumberViewModel(formData));
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
}
/**
* Class used to handle all the text fields for email, text & number.
*/
private class EmailViewHolder extends BaseViewHolder {
ItemDynamicFormEmailBinding mBinding;
EmailViewHolder(ItemDynamicFormEmailBinding binding) {
super(binding.getRoot());
this.mBinding = binding;
mBinding.edDynamicFormEmail.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
}
@Override
public void afterTextChanged(Editable editable) {
}
});
}
@Override
public void onBind(int position) {
final FormData formData = formDataList.get(position);
//val radioButton = mBinding.root.findViewById(R.id.radioButtton) as RadioButton
FormEmailViewModel emptyItemViewModel = new FormEmailViewModel(formData);
mBinding.setViewModel(emptyItemViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
}
/**
* Class used to handle all the text fields for description only.
*/
private class DescriptionViewHolder extends BaseViewHolder {
ItemDynamicFormDescriptionBinding mBinding;
DescriptionViewHolder(ItemDynamicFormDescriptionBinding binding) {
super(binding.getRoot());
this.mBinding = binding;
mBinding.edDynamicFormDesc.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
formDataList.get(getAdapterPosition()).setEnteredValue(charSequence.toString());
}
@Override
public void afterTextChanged(Editable editable) {
}
});
}
@Override
public void onBind(int position) {
final FormData formData = formDataList.get(position);
FormDescriptionViewModel emptyItemViewModel = new FormDescriptionViewModel(formData);
mBinding.setViewModel(emptyItemViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
}
/**
* Class used to select pic form device and upload to server.
*/
private class UploadViewHolder extends BaseViewHolder
implements FormUploadViewModel.UploadListener {
private ItemDynamicFormUploadBinding mBinding;
private UploadViewHolder(ItemDynamicFormUploadBinding mBinding) {
super(mBinding.getRoot());
this.mBinding = mBinding;
}
@Override
public void onBind(int position) {
final FormData data = formDataList.get(position);
FormUploadViewModel uploadViewModel = new FormUploadViewModel(data, this);
mBinding.setViewModel(uploadViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
@Override
public void onUploadClick(@NonNull FormData formData) {
adapterListener.onUploadPic(getAdapterPosition(), formData);
}
}
/**
* Class used to pick date and time.
*/
private class DateTimeViewHolder extends BaseViewHolder
implements FormDateTimeViewModel.DateTimeListener {
ItemDynamicFormDateTimeBinding mBinding;
int mYear;
int mMonth;
int mDay;
int mHour;
int mMinute;
int mSecond;
FormDateTimeViewModel dateTimeViewModel;
FormData data;
DateTimeViewHolder(ItemDynamicFormDateTimeBinding mBinding) {
super(mBinding.getRoot());
this.mBinding = mBinding;
}
@Override
public void onBind(int position) {
data = formDataList.get(position);
dateTimeViewModel = new FormDateTimeViewModel(data, this);
mBinding.setViewModel(dateTimeViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
@Override
public void dateClick() {
Calendar calendar = Calendar.getInstance();
mHour = calendar.get(Calendar.HOUR_OF_DAY);
mMinute = calendar.get(Calendar.MINUTE);
mSecond = 0;
mYear = calendar.get(Calendar.YEAR);
mMonth = calendar.get(Calendar.MONTH);
mDay = calendar.get(Calendar.DAY_OF_MONTH);
CommonUtils.openDatePicker(context, mYear, mMonth, mDay,
0, 0, (view, year, monthOfYear, dayOfMonth) -> {
String selectedDate = dayOfMonth + "-" + (monthOfYear + 1) + "-" + year;
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, monthOfYear);
cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
/*cal.set(Calendar.HOUR_OF_DAY, mHour);
cal.set(Calendar.MINUTE, mMinute);
cal.set(Calendar.SECOND, 0);*/
dateTimeViewModel.getDate().set(DateTimeUtil.getParsedDate(cal.getTimeInMillis()));
data.setEnteredValue(cal.getTimeInMillis() + "");
});
}
@Override
public void timeClick() {
Calendar calendar = Calendar.getInstance();
mHour = calendar.get(Calendar.HOUR_OF_DAY);
mMinute = calendar.get(Calendar.MINUTE);
CommonUtils.openTimePicker(context, mHour, mMinute,
(view, hourOfDay, minute) -> {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, mYear);
cal.set(Calendar.MONTH, mMonth);
cal.set(Calendar.DAY_OF_MONTH, mDay);
cal.set(Calendar.HOUR_OF_DAY, hourOfDay);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 0);
dateTimeViewModel.getTime().set(DateTimeUtil.getParsedTime(cal.getTimeInMillis()));
data.setEnteredValue(cal.getTimeInMillis() + "");
});
}
}
/**
* Class used to pick date from & to .
*/
private class DateRangeViewHolder extends BaseViewHolder
implements FormDateRangeViewModel.DateRangeListener {
ItemDynamicFormDateRangeBinding mBinding;
FormDateRangeViewModel uploadViewModel;
DateRangeViewHolder(ItemDynamicFormDateRangeBinding mBinding) {
super(mBinding.getRoot());
this.mBinding = mBinding;
}
@Override
public void onBind(int position) {
FormData data = formDataList.get(position);
uploadViewModel = new FormDateRangeViewModel(data, this);
mBinding.setViewModel(uploadViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
@Override
public void dateViewClick(@NotNull View view) {
// Get Current Date
Calendar c = Calendar.getInstance();
int mYear = c.get(Calendar.YEAR);
int mMonth = c.get(Calendar.MONTH);
int mDay = c.get(Calendar.DAY_OF_MONTH);
// int mHour = c.get(Calendar.HOUR_OF_DAY);
// int mMin = c.get(Calendar.MINUTE);
CommonUtils.openDatePicker(context, mYear, mMonth, mDay,
c.getTimeInMillis(), 0, (view1, year, monthOfYear, dayOfMonth) -> {
//String selectedDate = dayOfMonth + "-" + (monthOfYear + 1) + "-" + year;
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, monthOfYear);
cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
switch (view.getId()) {
case R.id.tvDateRange1:
uploadViewModel.getDate1().set(DateTimeUtil.getParsedDate(cal.getTimeInMillis()));
formDataList.get(getAdapterPosition()).setMaxRange(cal.getTimeInMillis());
break;
case R.id.tvDateRange2:
uploadViewModel.getDate2().set(DateTimeUtil.getParsedDate(cal.getTimeInMillis()));
formDataList.get(getAdapterPosition()).setMinRange(cal.getTimeInMillis());
break;
}
});
}
}
/**
* Class used to pick date
*/
private class DateViewHolder extends BaseViewHolder
implements FormDateViewModel.DateListener {
ItemDynamicFormDateBinding mBinding;
FormDateViewModel emptyItemViewModel;
int mYear, mMonth, mDay;
DateViewHolder(ItemDynamicFormDateBinding mBinding) {
super(mBinding.getRoot());
this.mBinding = mBinding;
}
@Override
public void onBind(int position) {
FormData data = formDataList.get(position);
emptyItemViewModel = new FormDateViewModel(data, this);
mBinding.setViewModel(emptyItemViewModel);
// Immediate Binding
// When a variable or observable changes, the binding will be scheduled to change before
// the next frame. There are times, however, when binding must be executed immediately.
// To force execution, use the executePendingBindings() method.
mBinding.executePendingBindings();
}
@Override
public void onDateClick() {
Calendar calendar = Calendar.getInstance();
mYear = calendar.get(Calendar.YEAR);
mMonth = calendar.get(Calendar.MONTH);
mDay = calendar.get(Calendar.DAY_OF_MONTH);
CommonUtils.openDatePicker(context, mYear, mMonth, mDay,
0, 0, (view, year, monthOfYear, dayOfMonth) -> {
// String selectedDate = dayOfMonth + "-" + (monthOfYear + 1) + "-" + year;
Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, monthOfYear);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
formDataList.get(getAdapterPosition()).setMaxRange(c.getTimeInMillis());
emptyItemViewModel.getDate().set(DateTimeUtil.getParsedDate(c.getTimeInMillis()));
});
}
}
/**
* If hashMap is empty show empty view
*/
private class EmptyViewHolder extends BaseViewHolder
implements FormEmptyItemViewModel.ClickListener {
private ItemDynamicFormEmptyViewBinding mBinding;
EmptyViewHolder(ItemDynamicFormEmptyViewBinding binding) {
super(binding.getRoot());
this.mBinding = binding;
}
@Override
public void onBind(int position) {
FormEmptyItemViewModel emptyItemViewModel = new FormEmptyItemViewModel(this);
mBinding.setViewModel(emptyItemViewModel);
}
}
/**
* If view type is not handled then show this view
*/
private class UnknownViewHolder extends BaseViewHolder {
UnknownViewHolder(ItemDynamicFormUnknownBinding unknownBinding) {
super(unknownBinding.getRoot());
}
@Override
public void onBind(int position) {
}
}
If you have any doubt let me know, Happy Coding :)
Upvotes: 1
Reputation: 369
You may try:
@Override
public long getItemId(int position) {
return position;
}
Also override:
@Override
public int getItemViewType(int position) {
return position;
}
One way to increase the cache for recyclerview is:
recyclerView.setItemViewCacheSize(20)
You can also get extra space with the help of LinearLayoutManager's method calculateExtraLayoutSpace. I've added links to the docs.
Alternatively you can use:
setIsRecyclable(false);
But it beats the purpose of using RecyclerView.
Upvotes: 0
Reputation: 128
You should not use hashCode()
as an ID as it is not guaranteed to be unique !
My guess is that many of your items are returning the same hash.
Please try implementing a unique ID for each item and use the it in your getItemId()
method.
Upvotes: 0
Reputation: 33
Make the RecyclerView ViewModel non-recyclable. I think the recyclerview is set to refresh by default, therefore, shuffling the items For example
class YourViewModel extends RecyclerView.ViewHolder {
YourViewModel (@NonNull View view) {
super(view);
// Add the line below
this.setIsRecyclable(false);
}
}
Also, you could implement a sorting method using Collection.sort(items) before the items are populated, so even when the view gets recycled, the items still remain sorted (eg. by id).
Upvotes: 0