Lubron
Lubron

Reputation: 129

Refactoring RecyclerView with multi ViewHolder from ButterKnife using ViewBinding

Wanting to switch to version 8.0 of AGP for my Android project I had to refactor a lot of java classes where the developer before me used ButterKnife to bind variables to layout elements. The last class I encounter is a RecyclerView that uses multiple ViewHolder in in a hierarchical way like this:

     public class ViewHolder extends RecyclerView.ViewHolder {

        ViewHolder(View v) {
            super(v);
        }
    }

    public class MessageViewHolder extends ViewHolder {

        @BindView(R.id.message_text_list_item)
        LinearLayout mMessageLayout;

        @BindView(R.id.message_custom_widget)
        LinearLayout mCustomWidget;

        @Nullable
        @BindView(R.id.message_textview_response_layout)
        LinearLayout mMessageAcknoledgmentLayout;

        @BindView(R.id.message_textview_sender_name)
        TextView mSender;

        @BindView(R.id.message_textview_text)
        TextView mText;

        @BindView(R.id.message_textview_time)
        TextView mTime;

        @Nullable
        @BindView(R.id.message_textview_status)
        TextView mStatus;

        @Nullable
        @BindView(R.id.message_imageview_ack_status)
        ImageView mAckStatus;

        private long id;

        private MessageStatus mMessageStatus;

        MessageViewHolder(View v) {
            super(v);
            ButterKnife.bind(this, v);

            mCustomWidget.setVisibility(GONE);
            mSender.setVisibility(GONE);
            if(mMessageAcknoledgmentLayout != null) {

                mMessageAcknoledgmentLayout.setVisibility(GONE);
            }
        }

        @Optional
        @OnClick(R.id.message_textview_ack)
        void onMessageAckClick() {

            easyLog.info("[onMessageAckClick] Ho cliccato sull'ack del messaggio con id -> "+ id);
            showMessageAckConfirmDialog(id, true);
        }

        @Optional
        @OnClick(R.id.message_textview_nack)
        void onMessageNackClick() {

            easyLog.info("[onMessageAckClick] Ho cliccato sul nack del messaggio con id -> "+ id);
            showMessageAckConfirmDialog(id, false);
        }
    }

    public class SeparatorViewHolder extends ViewHolder {

        @BindView(R.id.separator_date)
        TextView mDate;

        SeparatorViewHolder(View v) {
            super(v);
            ButterKnife.bind(this, v);
        }
    }

    public class AudioViewHolder extends MessageViewHolder {

        AudioControlView mAudioControlView;

        AudioViewHolder(View v, int audioLayout) {
            super(v);

            View view = inflater.inflate(audioLayout, null);
            mAudioControlView = view.findViewById(R.id.player_view);
            mCustomWidget.addView(view);
            mCustomWidget.setVisibility(View.VISIBLE);
        }
    }

    public class ImageViewHolder extends MessageViewHolder {

        MessageImageView mMessageImageView;

        ImageView mImageView;

        ImageViewHolder(View v, int imageLayout) {
            super(v);

            View view = inflater.inflate(imageLayout, null);
            mMessageImageView = view.findViewById(R.id.message_image);
            mImageView = mMessageImageView.getImageView();
            mCustomWidget.addView(mMessageImageView);
            mCustomWidget.setVisibility(View.VISIBLE);
        }
    }

I know how to use ViewBinding for RecyclerView with a single ViewHolder, but I can't figure out how to handle this particular situation, anyone has some hints or solutions?

Thanks in advance!

EDIT: I forgot important infos: I had some problem understanding which bindings I have to pass and use because in the onCreateViewHolder function a layout is passed (list_item_text_****) but in the ViewHolders the layout on which @BindView is used is another one, not the same, for example

list_item_text_left.xml

<it.ingeniars.etmlib.ui.view.MessageTextViewLeft
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

</it.ingeniars.etmlib.ui.view.MessageTextViewLeft>

widget_text_msg_left.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    style="@style/WidgetTextMessage.Left"
    android:id="@+id/message_text_left_layout">

    <LinearLayout
        android:id="@+id/message_text_left_background"
        style="@style/WidgetTextMessageBackground.Left">

        <LinearLayout
            android:id="@+id/message_text_list_item"
            style="@style/LinearWidgetTextMsg.Left">

            <TextView
                android:id="@+id/message_textview_sender_name"
                style="@style/TextViewSenderMsg.Left"/>

            <LinearLayout
                android:id="@+id/message_custom_widget"
                style="@style/CustomWidgetFrame.Left"/>

            <TextView
                android:id="@+id/message_textview_text"
                style="@style/TextViewTextMsg.Left"/>

            <LinearLayout
                android:id="@+id/message_textview_response_layout"
                style="@style/TextViewAckLayout">

                <Button
                    android:id="@+id/message_textview_nack"
                    style="@style/TextViewNack"/>

                <Button
                    android:id="@+id/message_textview_ack"
                    style="@style/TextViewAck" />

            </LinearLayout>

            <LinearLayout
                style="@style/TextViewStatusLayout">

                <ImageView
                    android:id="@+id/message_imageview_ack_status"
                    style="@style/ImageViewAckStatusMsg.Left"/>

                <TextView
                    android:id="@+id/message_textview_time"
                    style="@style/TextViewTimeMsg.Right"/>

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>

</LinearLayout>
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

        switch(viewType) {
            case TYPE_SEPARATOR:
                return new SeparatorViewHolder(inflater.inflate(R.layout.widget_message_separator, parent, false));

            case TYPE_MESSAGE_TEXT_LEFT:
                return new MessageViewHolder(inflater.inflate(R.layout.list_item_text_left, parent, false));

            case TYPE_MESSAGE_TEXT_WITH_IMAGE_LEFT:
                return new ImageViewHolder(inflater.inflate(R.layout.list_item_text_left, parent, false), R.layout.list_item_image_left);

            case TYPE_MESSAGE_TEXT_WITH_AUDIO_LEFT:
                return new AudioViewHolder(inflater.inflate(R.layout.list_item_text_left, parent, false), R.layout.list_item_audio_left);

            case TYPE_MESSAGE_TEXT_WITH_AUDIO_RIGHT:
                return new AudioViewHolder(inflater.inflate(R.layout.list_item_text_right, parent, false), R.layout.list_item_audio_right);

            case TYPE_MESSAGE_TEXT_RIGHT:
                return new MessageViewHolder(inflater.inflate(R.layout.list_item_text_right, parent, false));

            case TYPE_MESSAGE_TEXT_WITH_IMAGE_RIGHT:
                return new ImageViewHolder(inflater.inflate(R.layout.list_item_text_right, parent, false), R.layout.list_item_image_right);

            case TYPE_MESSAGE_TEXT_WITH_PDF_PREVIEW_IMAGE_LEFT:
                return new ImageViewHolder(inflater.inflate(R.layout.list_item_text_left, parent, false), R.layout.list_item_image_left);

            case TYPE_MESSAGE_TEXT_WITH_PDF_PREVIEW_IMAGE_RIGHT:
                return new ImageViewHolder(inflater.inflate(R.layout.list_item_text_right, parent, false), R.layout.list_item_image_right);

            default:
                return null;
        }
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {

        InboxItem item = getItem(position);
        int itemType = getItemViewType(position);
        switch(itemType) {
            case TYPE_MESSAGE_TEXT_LEFT:
                onBindTextLeftViewHolder((MessageViewHolder) viewHolder, (InboxMessage) item);
                break;

            case TYPE_MESSAGE_TEXT_RIGHT:
                onBindTextRightViewHolder((MessageViewHolder) viewHolder, (InboxMessage) item);
                break;

            case TYPE_MESSAGE_TEXT_WITH_AUDIO_RIGHT:
                onBindTextAudioRightViewHolder((AudioViewHolder) viewHolder, (InboxMessage) item, position);
                break;

            case TYPE_MESSAGE_TEXT_WITH_AUDIO_LEFT:
                onBindTextAudioLeftViewHolder((AudioViewHolder) viewHolder, (InboxMessage) item, position);
                break;

            case TYPE_MESSAGE_TEXT_WITH_IMAGE_RIGHT:
                onBindTextImageRightViewHolder((ImageViewHolder) viewHolder, (InboxMessage) item);
                break;

            case TYPE_MESSAGE_TEXT_WITH_IMAGE_LEFT:
                onBindTextImageLeftViewHolder((ImageViewHolder) viewHolder, (InboxMessage) item);
                break;

            case TYPE_SEPARATOR:
                onBindSeparator((SeparatorViewHolder) viewHolder, (MessageSeparator) item);
                break;

            case TYPE_MESSAGE_TEXT_WITH_PDF_PREVIEW_IMAGE_LEFT:
                onBindTextPDFImageLeftViewHolder((ImageViewHolder) viewHolder, (InboxMessage) item);
                break;

            case TYPE_MESSAGE_TEXT_WITH_PDF_PREVIEW_IMAGE_RIGHT:
                onBindTextPDFImageRightViewHolder((ImageViewHolder) viewHolder, (InboxMessage) item);
                break;
        }
    }

Upvotes: 0

Views: 115

Answers (1)

Martin Marconcini
Martin Marconcini

Reputation: 27246

There's nothing super special about ViewBinding and ViewHolders:

(all pseudo-code)

One way could be having a small internal base class:

internal sealed class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

And then individual ViewHolders:

internal class AViewHolder(private val binding: ALayoutBinding) : BaseViewHolder(binding.root) {
    fun bind(item: SomeItem) {
        with(binding) {
           ... // use your views and bind them to "some Item" ;)
        }
    }
}

internal class BViewHolder(private val binding: BLayoutBinding) : BaseViewHolder(binding.root) {
    fun bind(item: SomeItem) {
    ...
    }
}

Then you have an Adapter:

internal class YourAdapter() : ListAdapter<YourItems, BaseViewHolder>(...) 

As usual.

Then all you need is to override the correct methods:

  1. override fun getItemViewType(position: Int): Int
  2. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder
  3. override fun onBindViewHolder(holder: BaseViewHolder, position: Int)

For 1, all you really do is

 override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is A -> VIEW_TYPE_A // you make these constants
            is B -> VIEW_TYPE_B

Then for 2:

 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        val inflater = LayoutInflater.from(parent.context)

        return when (viewType) {
            VIEW_TYPE_A -> AViewHolder(ALayoutBinding.inflate(inflater, parent, false))
            VIEW_TYPE_B -> BViewHolder(BLayoutBinding.inflate(inflater, parent, false))

...

And finally...

 override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
        val item = getItem(position)

        when (holder) {
            is AViewHolder -> holder.bind(item as SomeItem) // here you'd cast the objects to suite your view holders if they were different.
            is BViewHolder -> holder.bind(item as SomeItem)
...

And that's really all there is.

Keep in mind, this is one way to do it. I've seen it, I've done it, it works. There may be others or you may want more or less abstractions but at the end of the day, you pass the binding object to your ViewHolder and carry on as usual ;)

Update 1

It's a bit difficult to follow your exact issue, but keep in mind: For views to be available in ViewBinding, they must have an id.

There's no "different magic" with ViewBinding vs. ButterKnife, other than the code generation behind the scenes to generate the "Binding" objects you use, versus ButterKnife's annotation system that did the findViewById for you.

If you have a layout called my_layout_one.xml You should also have a MyLayoutOneBinding. Each binding has an inflate method, so instead of passing the .xml, then calling inflate, then doing findViewById on each view you wanted to reference/use, you can obtain a reference to your *Binding class once, and it will contain all the layout's views references already there (kind of populated at compile time).

So let's take an example:

new MessageViewHolder(inflater.inflate(R.layout.list_item_text_left, parent, false));

The MessageViewHolder is expecting a View, as evidenced by the constructor:

MessageViewHolder(View v)

After the viewHolder receives the View, ButterKnife would be able to access all Views that have an id via the annotation. If you look at list_item_text_left (the one you supplied), I see how this can be confusing, since the Layout references what appears to be a custom View called

it.ingeniars.etmlib.ui.view.MessageTextViewLeft

You didn't post the source code for this View, but based on the ViewHolder's current code you've shown, I'd assume the layout that the view is internally inflating is: widget_text_msg_left.xml

There's got to be a WidgetTextMsgLeftBinding somewhere in your auto-generated code that you could inflate by calling inflate on it.

Because your current view takes a View (not a layout), it then does findViewById on that view (well it currently uses ButterKnife, which kind of abstracts this findView for you, but if you look at the code you supplied, The Message view holder references (using just the top two fields as to make this shorter):

        @BindView(R.id.message_text_list_item)
        LinearLayout mMessageLayout;

        @BindView(R.id.message_custom_widget)
        LinearLayout mCustomWidget;

Both id are LinearLayouts defined in list_item_text_left.xml

So as you can see, there's nothing mysterious about how it's all orchestrated together.

Experiment passing and inspecting (in the IDE) what each binding has, and remember that for a view to be able to be used from a binding, it must contain an id. You've seen the viewBinding docs, so you understand now how it works.

Now think about what exact view each viewHolder is referencing, and that will tell you which Binding class you need to inflate or pass.

Upvotes: 0

Related Questions