Skip to content

Feature Request : Ability to pass Context to ViewHolder in FirebaseRecyclerAdapter #216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
iChintanSoni opened this issue Jul 18, 2016 · 4 comments

Comments

@iChintanSoni
Copy link

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.

@samtstern
Copy link
Contributor

@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:

  1. Within the 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.
  2. Instead of calling methods directly on your 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.

@iChintanSoni
Copy link
Author

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.

@samtstern
Copy link
Contributor

@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!

@iChintanSoni
Copy link
Author

@samtstern Thank you so much sir for your Gist. Now, I got your point what you were trying to explain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants