AirBnb Epoxy - Views are duplicated instead of replaced

1.7k Views Asked by At

I am rendering a form based on JSON response that I fetch from the server.

My use case involves listening to a click from a radio button, toggling the visibility of certain text fields based on the radioButton selection, and refreshing the layout with the visible textView.

The expected output should be to update the same view with the textView now visible, but I'm now seeing the same form twice, first with default state, and second with updated state.

Have I somehow created an entirely new model_ class and passing it to the controller? I just want to change the boolean field of the existing model and update the view.

My Model Class

@EpoxyModelClass(layout = R.layout.layout_panel_input)
abstract class PanelInputModel(
    @EpoxyAttribute var panelInput: PanelInput,
    @EpoxyAttribute var isVisible: Boolean,
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var context: Context,
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var textChangedListener: InputTextChangedListener,
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var radioButtonSelectedListener: RadioButtonSelectedListener,
    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var validationChangedListener: ValidationChangedListener
) : EpoxyModelWithHolder<PanelInputModel.PanelInputHolder>() {

    @EpoxyAttribute var imageList = mutableListOf<ImageInput>()

    override fun bind(holder: PanelInputHolder) {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        generateViews(holder, inflater, panelInput.elements) // Generates textViews, radioButtons, etc, based on ElementType enum inside Panel input
    }

   fun generateRadioButtonView(element: Element) {
        // Created a custom listener and calling its function
        radioButtonSelectedListener.radioButtonSelected(chip.id, chip.text.toString())
   }

  fun generateTextView() {
     // Show/hide textView based on isVisible value
  }

My Controller Class

class FormInputController(
    var context: Context,
    var position: Int, // Fragment Position in PagerAdapter
    var textChangedListener: InputTextChangedListener,
    var radioButtonSelectedListener: RadioButtonSelectedListener,
    var validationChangedListener: ValidationChangedListener
) : TypedEpoxyController<FormInput>() {

    override fun buildModels(data: FormInput?) {
        val panelInputModel = PanelInputModel_(
            data as PanelInput,
            data.isVisible,
            context,
            textChangedListener,
            radioButtonSelectedListener,
            validationChangedListener
        )
        panelInputModel.id(position)
        panelInputModel.addTo(this)
    }
}

My fragment implements the on radio button checked listener, modifies the formInput.isVisible = true and calls formInputController.setData(componentList)

Please help me out on this, thanks!

1

There are 1 best solutions below

0
On

I don't think you are using Epoxy correctly, that's not how it's supposed to be.

  1. First of all, let's start with the Holder: you should not inflate the view inside of bind/unbind, just set your views there. Also, the view is inflated for you from the layout file you are specifying at R.layout.layout_panel_input, so there is no need to inflate at all.

You should copy this into your project: https://github.com/airbnb/epoxy/blob/master/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/KotlinEpoxyHolder.kt

And create your holder in this way:

class PanelInputHolder : KotlinHolder() {

    val textView by bind<TextView>(R.id.your_text_view_id)
    val button by bind<Button>(R.id.your_button_id)
}
  1. Let's move to your model class: you should remove these variables from the constructor as they are going to be a reference for the annotation processor to create the actual class. Also, don't set your layout res from the annotation as that will not be allowed in the future.

Like so:

@EpoxyModelClass
class PanelInputModel : EpoxyModelWithHolder<PanelInputHolder>() {

    @EpoxyAttribute
    lateinit var text: String
    @EpoxyAttribute(DoNotHash)
    lateinit var listener: View.OnClickListener

    override fun getDefaultLayout(): Int {
        return R.layout.layout_panel_input
    }

    override fun bind(holder: PanelInputHolder) {

        // here set your views
        holder.textView.text = text
        holder.textView.setOnClickListener(listener)
    }

    override fun unbind(holder: PanelInputHolder) {

        // here unset your views
        holder.textView.text = null
        holder.textView.setOnClickListener(null)
    }
}
  1. Loop your data inside the controller not inside the model:

class FormInputController : TypedEpoxyController<FormInput>() {
    
    
    override fun buildModels(data: FormInput?) {
    
        data?.let {

            // do your layout as you want, with the models you specify
            // for example a header
        
            PanelInputModel_()
                .id(it.id)
                .text("Hello WOrld!")
                .listener { // do something here }
                .addTo(this)
        
            // generate a model per item
            it.images.forEach {
            
                ImageModel_()
                    .id(it.imageId)
                    .image(it)
                    .addTo(this)
            }
        }
    }
}

When choosing your id, keep in mind that Epoxy will keep track of those and update if the attrs change, so don't use a position, but a unique id that will not get duplicated.