Problem Description: I tried using FirebaseRecyclerAdapter. It has become insanely easy to integrate any type of ListView or RecyclerView with any complex views inside its list item. I came across a thing I would like to describe.
I had buttons and other controls in Recycler Item; I set the ClickListeners to Buttons inside ViewHolder itself. Now, I want to use Activity context inside my ViewHolder class. So, I went creating my own class same as FirebaseRecyclerAdapter as below:
TodoViewHolder.java
public class TodoViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.accbListItemHome)
AppCompatCheckBox mAppCompatCheckBox;
@BindView(R.id.actvListItemHome)
AppCompatTextView mAppCompatTextView;
@BindView(R.id.acibListItemHome)
AppCompatImageButton mAppCompatImageButton;
private Context mContext;
public TodoViewHolder(Context context, View itemView) {
super(itemView);
mContext = context;
ButterKnife.bind(this, itemView);
}
public AppCompatCheckBox getmAppCompatCheckBox() {
return mAppCompatCheckBox;
}
public AppCompatTextView getmAppCompatTextView() {
return mAppCompatTextView;
}
public AppCompatImageButton getmAppCompatImageButton() {
return mAppCompatImageButton;
}
@OnCheckedChanged(R.id.accbListItemHome)
void onCheckBoxChange(CompoundButton compoundButton, boolean checked) {
TodoMasterModel todoMasterModel = (TodoMasterModel) compoundButton.getTag();
todoMasterModel.getmTodoModel().setmIsCompleted(checked);
((HomeActivity) mContext).onDoneClick(todoMasterModel, AddEditDialogFragment.ACTION_Edit);
Log.i("Checkbox", todoMasterModel.toString());
}
@OnClick(R.id.actvListItemHome)
void onTextViewClick(View view) {
TodoMasterModel todoMasterModel = (TodoMasterModel) view.getTag();
((HomeActivity) mContext).showAddEditDialog(todoMasterModel, AddEditDialogFragment.ACTION_Edit);
Log.i("TextView", todoMasterModel.toString());
}
@OnClick(R.id.acibListItemHome)
void onImageButtonClick(View view) {
TodoMasterModel todoMasterModel = (TodoMasterModel) view.getTag();
FirebaseDatabase.getInstance().getReference("todos").child(todoMasterModel.getmId()).setValue(todoMasterModel.getmTodoModel());
Log.i("Delete", todoMasterModel.toString());
}
}
And MyFirebaseAdapter.java
public abstract class MyFirebaseAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private Context mContext;
protected int mModelLayout;
Class<T> mModelClass;
Class<VH> mViewHolderClass;
FirebaseArray mSnapshots;
/**
* @param modelClass Firebase will marshall the data at a location into an instance of a class that you provide
* @param modelLayout This is the layout used to represent a single item in the list. You will be responsible for populating an
* instance of the corresponding view with the data from an instance of modelClass.
* @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout.
* @param ref The Firebase location to watch for data changes. Can also be a slice of a location, using some
* combination of <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>
*/
public MyFirebaseAdapter(Context context, Class<T> modelClass, int modelLayout, Class<VH> viewHolderClass, Query ref) {
mContext = context;
mModelClass = modelClass;
mModelLayout = modelLayout;
mViewHolderClass = viewHolderClass;
mSnapshots = new FirebaseArray(ref);
mSnapshots.setOnChangedListener(new FirebaseArray.OnChangedListener() {
@Override
public void onChanged(EventType type, int index, int oldIndex) {
switch (type) {
case Added:
notifyItemInserted(index);
break;
case Changed:
notifyItemChanged(index);
break;
case Removed:
notifyItemRemoved(index);
break;
case Moved:
notifyItemMoved(oldIndex, index);
break;
default:
throw new IllegalStateException("Incomplete case statement");
}
}
});
}
/**
* @param modelClass Firebase will marshall the data at a location into an instance of a class that you provide
* @param modelLayout This is the layout used to represent a single item in the list. You will be responsible for populating an
* instance of the corresponding view with the data from an instance of modelClass.
* @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout.
* @param ref The Firebase location to watch for data changes. Can also be a slice of a location, using some
* combination of <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>
*/
public MyFirebaseAdapter(Context context, Class<T> modelClass, int modelLayout, Class<VH> viewHolderClass, DatabaseReference ref) {
this(context, modelClass, modelLayout, viewHolderClass, (Query) ref);
}
public void cleanup() {
mSnapshots.cleanup();
}
@Override
public int getItemCount() {
return mSnapshots.getCount();
}
public T getItem(int position) {
return parseSnapshot(mSnapshots.getItem(position));
}
/**
* This method parses the DataSnapshot into the requested type. You can override it in subclasses
* to do custom parsing.
*
* @param snapshot the DataSnapshot to extract the model from
* @return the model extracted from the DataSnapshot
*/
protected T parseSnapshot(DataSnapshot snapshot) {
return snapshot.getValue(mModelClass);
}
public DatabaseReference getRef(int position) {
return mSnapshots.getItem(position).getRef();
}
@Override
public long getItemId(int position) {
// http://stackoverflow.com/questions/5100071/whats-the-purpose-of-item-ids-in-android-listview-adapter
return mSnapshots.getItem(position).getKey().hashCode();
}
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
try {
Constructor<VH> constructor = mViewHolderClass.getConstructor(View.class);
return constructor.newInstance(view);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void onBindViewHolder(VH viewHolder, int position) {
T model = getItem(position);
populateViewHolder(viewHolder, model, position);
}
@Override
public int getItemViewType(int position) {
return mModelLayout;
}
/**
* Each time the data at the given Firebase location changes, this method will be called for each item that needs
* to be displayed. The first two arguments correspond to the mLayout and mModelClass given to the constructor of
* this class. The third argument is the item's position in the list.
* <p>
* Your implementation should populate the view using the data contained in the model.
*
* @param viewHolder The view to populate
* @param model The object containing the data used to populate the view
* @param position The position in the list of the view being populated
*/
abstract protected void populateViewHolder(VH viewHolder, T model, int position);
}
But I get following exception:
java.lang.RuntimeException: java.lang.NoSuchMethodException: [class android.view.View] at com.letsnurture.android.firebasedatabasedemo.adapter.MyFirebaseAdapter.onCreateViewHolder(MyFirebaseAdapter.java:120) at android.support.v7.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:5779) at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5003) at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4913) at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2029) at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1414) at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1377) at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:578) at android.support.v7.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:3260) at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3069) at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:3518) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743) at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1732) at android.widget.LinearLayout.onLayout(LinearLayout.java:1497) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.support.design.widget.HeaderScrollingViewBehavior.layoutChild(HeaderScrollingViewBehavior.java:120) at android.support.design.widget.ViewOffsetBehavior.onLayoutChild(ViewOffsetBehavior.java:42) at android.support.design.widget.AppBarLayout$ScrollingViewBehavior.onLayoutChild(AppBarLayout.java:1319) at android.support.design.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:815) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.support.v4.widget.DrawerLayout.onLayout(DrawerLayout.java:1191) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336) at android.widget.FrameLayout.onLayout(FrameLayout.java:273) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1586) at android.widget.LinearLayout.onLayout(LinearLayout.java:1495) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336) at android.widget.FrameLayout.onLayout(FrameLayout.java:273) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1586) at android.widget.LinearLayout.onLayout(LinearLayout.java:1495) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336) at android.widget.FrameLayout.onLayout(FrameLayout.java:273) at com.android.internal.policy.PhoneWindow$DecorView.onLayout(PhoneWindow.java:2678) at android.view.View.layout(View.java:16651) at android.view.ViewGroup.layout(ViewGroup.java:5440) at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2183) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1943) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1119) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6060) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858) at android.vi
The issue is explained on http://stackoverflow.com/questions/38429054/firebaserecycleradapter-pass-activity-context-to-viewholder by @puf
So, thought this might be good if we are able to pass Activity Context to ViewHolders.
@chintansoni202 I don't think passing an Activity context to the RecyclerAdapter is the right option in most cases, you have a few options here:
ViewHolder you can do this.itemView.getContext() which will give you the View's context which you can cast to an Activity if you're confident that's where the view is displayed.Activity, you should create an interface that your Activity implements and add that as a constructor argument to the RecyclerAdapter. You can then bind that to your ViewHolder in onCreateViewHolder or similar.Ok @samtstern Can you provide some code snippet demonstrating your points #1, #2 ? For point #1, I am confused if its Fragment and not Activity. For point #2, I am aware of creating interfaces but how would I pass the interface from Activity or Fragment to ViewHolder via my FirebaseRecyclerAdapter ? If you could provide some Github Gist or small code snippet would be much more helpful.
@chintansoni202 here's a very basic example of how you could use an interface to define this behavior and then pass it from your Activity to the Adapter to the ViewHolder:
https://gist.github.com/samtstern/e08eec46b363d536f5c3c61f8696862a
I am going to close this issue now as we don't plan to add this feature to the library. Good luck!
@samtstern Thank you so much sir for your Gist. Now, I got your point what you were trying to explain.
Most helpful comment
@chintansoni202 I don't think passing an Activity context to the RecyclerAdapter is the right option in most cases, you have a few options here:
ViewHolderyou can dothis.itemView.getContext()which will give you theView's context which you can cast to anActivityif you're confident that's where the view is displayed.Activity, you should create an interface that yourActivityimplements and add that as a constructor argument to theRecyclerAdapter. You can then bind that to yourViewHolderinonCreateViewHolderor similar.