4
votes

Every action in list item is handled from RecyclerView.ViewHolder Actually, i want to pass Activity context to ViewHolder in FirebaseRecyclerAdapter so I tried to add Constructor to this ViewHolder as below:

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

I created my class and modified FirebaseRecyclerAdapter for my purpose as below:

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(mContext, 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);
}

Error is coming because of this line:

return constructor.newInstance(mContext, view);

But I am getting NoSuchMethod exception. I knew I am doing it wrong. But i am not getting the way to do it. Can anyone help me with it ?

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

3
return constructor.newInstance(mContext, view); try return constructor.newInstance((Activity)mContext, view);Vishal Patoliya ツ
@VishalPatoliya I don't think we need any casting.Chintan Soni

3 Answers

4
votes

Your ViewHolder subclass needs a constructor with just a View parameter. From the FirebaseUI documentation:

public static class ChatHolder extends RecyclerView.ViewHolder {
    View mView;

    public ChatHolder(View itemView) {
        super(itemView);
        mView = itemView;
    }

The reason is in this code, which only searches for a single signature. Adding code to search for a ViewHolder(Context, View) constructor might be a good addition. Can you add a feature request to the FirebaseUI github repo for it?

Update: the feature request on Github for those looking to +1 it. :-)

0
votes

Passing an Activity context to Adapter isn't always the right way to do things. You should rather create an interface that your Activity implements and pass that to your Adapter. Another popular way is to use pubsub pattern to raise events and subscribe to them when something happens with libraries like EventBus.

0
votes

Solution:

I created an interface OnTodoActionListener.java as below:

public interface OnTodoActionListener {
    void onCheckBoxChange(CompoundButton compoundButton, boolean checked);
    void onTextViewClick(View view);
    void onImageButtonClick(View view);
}

Then, I created an object of this interface in Activity as:

private OnTodoActionListener mOnTodoActionListener;

mOnTodoActionListener = new OnTodoActionListener() {
        @Override
        public void onCheckBoxChange(CompoundButton compoundButton, boolean checked) {
            TodoMasterModel todoMasterModel = (TodoMasterModel) compoundButton.getTag();
            todoMasterModel.getmTodoModel().setmIsCompleted(checked);
            onDoneClick(todoMasterModel, AddEditDialogFragment.ACTION_Edit);
            Log.i("Checkbox", todoMasterModel.toString());
        }

        @Override
        public void onTextViewClick(View view) {
            TodoMasterModel todoMasterModel = (TodoMasterModel) view.getTag();
            showAddEditDialog(todoMasterModel, AddEditDialogFragment.ACTION_Edit);
            Log.i("TextView", todoMasterModel.toString());
        }

        @Override
        public void onImageButtonClick(View view) {
            TodoMasterModel todoMasterModel = (TodoMasterModel) view.getTag();
            mDatabaseReferenceTodo.child(todoMasterModel.getmId()).setValue(null);
            Log.i("Delete", todoMasterModel.toString());
        }
    };

I made a setter method in my TodoViewHolder and used its reference as below:

private OnTodoActionListener mOnTodoActionListener;

public void setListener(OnTodoActionListener onTodoActionListener) {
    mOnTodoActionListener = onTodoActionListener;
}

@OnCheckedChanged(R.id.accbListItemHome)
void onCheckBoxChange(CompoundButton compoundButton, boolean checked) {
    mOnTodoActionListener.onCheckBoxChange(compoundButton, checked);
}

@OnClick(R.id.actvListItemHome)
void onTextViewClick(View view) {
    mOnTodoActionListener.onTextViewClick(view);
}

@OnClick(R.id.acibListItemHome)
void onImageButtonClick(View view) {
    mOnTodoActionListener.onImageButtonClick(view);
}

And lastly, I set the listener created in Activity as:

FirebaseRecyclerAdapter mHomeRecyclerAdapter = new FirebaseRecyclerAdapter<TodoModel, TodoViewHolder>(TodoModel.class, R.layout.list_item_home, TodoViewHolder.class, mDatabaseReferenceTodo) {
        @Override
        public void populateViewHolder(TodoViewHolder todoViewHolder, TodoModel todoModel, int position) {

            todoViewHolder.setListener(mOnTodoActionListener);

            TodoMasterModel todoMasterModel = new TodoMasterModel(getRef(position).getKey(), todoModel);

            todoViewHolder.getmAppCompatCheckBox().setTag(todoMasterModel);
            todoViewHolder.getmAppCompatTextView().setTag(todoMasterModel);
            todoViewHolder.getmAppCompatImageButton().setTag(todoMasterModel);

            todoViewHolder.getmAppCompatCheckBox().setChecked(todoModel.ismIsCompleted());
            todoViewHolder.getmAppCompatTextView().setText(todoModel.getmTodoContent());

        }
    };
    mRecyclerView.setAdapter(mHomeRecyclerAdapter);

And Finally, I am now able to create a fully working demo of "Todo App" using Firebase Realtime Database. I will post the link to Github once I upload it. Thanks.

Update: You can have my demo of Todo App created using firebase database. Here's link: Firebase Database Demo