tianzhuqiao / form

Form: form builder in Kotlin for Android
Apache License 2.0
15 stars 6 forks source link

CustomView State not saved #6

Open WaheedAbbas opened 2 years ago

WaheedAbbas commented 2 years ago

I created a custom view that has a custom input field. When I scroll, the value of one input field ends up filling some other random input field. The values keep switching to a different input field filling other boxes as well with the same value every time I scroll up and down. This does not seem to be the case with FormItemText() or other existing input form items. Can you please have a look into this?

class FormClosedQuestionViewHolder(inflater: LayoutInflater, resource: Int, parent: ViewGroup) :
    FormViewHolder(inflater, resource, parent) {
    private var questionTextView : TextView? = null
    private var priceInputLayout : TextInputLayout? = null
    private var controlsView : SegmentedGroup? = null
    private var actionYesRadioBtn : RadioButton? = null
    init {
        questionTextView = itemView.findViewById(R.id.question_text)
        priceInputLayout = itemView.findViewById(R.id.beverage_price_input_layout)
        controlsView = itemView.findViewById(R.id.actions_toggle_layout)
        actionYesRadioBtn = itemView.findViewById(R.id.action_yes_radio_btn)
    }

    override fun bind(s: FormItem, listener: FormItemCallback?) {
        super.bind(s, listener)

        if (s is FormItemClosedQuestion) {
             questionTextView?.text = s.question
             priceInputLayout?.editText?.setText(s.price)
             Log.d("FormClosedQuestionViewHolder", "bind(): ${s.question} has ${s.price}")
            actionYesRadioBtn?.setOnCheckedChangeListener { compoundButton, isChecked ->
                if(isChecked) {
                    priceInputLayout?.visibility = View.VISIBLE
                } else {
                    priceInputLayout?.visibility = View.GONE
                }
            }
            priceInputLayout?.editText?.addTextChangedListener(object : TextWatcher {
                override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {

                }

                override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                   s.price = p0.toString()
                   listener?.onValueChanged(s)
                }

                override fun afterTextChanged(p0: Editable?) {
                }

            })

        }
    }
}
open class FormItemClosedQuestion : FormItem() {
    var question: String = ""
    var price : String = ""
}

fun <T : FormItemClosedQuestion> T.question(question: String) = apply {
    this.question = question
}
fun <T : FormItemClosedQuestion> T.price(price: String) = apply {
    this.price = price
}

MainActivity:

private val beveragesSection : FormItemSection by lazy { FormItemSection().title(getString(R.string.beverages)).titleColor(Color.BLACK).apply {
        enableCollapse = true
        +FormItemClosedQuestion().question("Pepsi 10x150 ML Can").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        }.tag("pepsi_10_150_can")
        +FormItemClosedQuestion().question("Pepsi 10x140 ML Can").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        }
        +FormItemClosedQuestion().question("Pepsi 10x130 ML Can").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        }
        +FormItemClosedQuestion().question("Pepsi 10x120 ML Can").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        }
}

BeforeScrolling

AfterScrolling

tianzhuqiao commented 2 years ago

Hi @WaheedAbbas, that looks weird, could you send a minimal project showing that issue?

WaheedAbbas commented 2 years ago

I created a separate project to explain the issue. The view basically has a question title, and Yes/No radio buttons, hitting yes would show an input field.

MainActivity:

class MainActivity : FormActivity() {

    private val TAG = "MainActivity"
    override fun initForm() {

        adapter?.registerViewHolder(
            FormItemClosedQuestion::class.java,
            R.layout.availablity_and_price_layout,
            FormClosedQuestionViewHolder::class.java
        )
        adapter?.apply {
            +questionsSection
        }
    }

    private val questionsSection: FormItemSection = FormItemSection().apply {
        title = "Questions Section"
        enableCollapse = true
        add(FormItemClosedQuestion().question("Question Price 1").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        })
        add(FormItemClosedQuestion().question("Question Price 2").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        })
        add(FormItemClosedQuestion().question("Question Price 3").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        })
        add(FormItemClosedQuestion().question("Question Price 4").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        })
        add(FormItemClosedQuestion().question("Question Price 5").onValueChanged {
            Log.d(TAG, "Price: ${it.price}")
        })

    }

}

FormClosedQuestionViewHolder:

class FormClosedQuestionViewHolder(inflater: LayoutInflater, resource: Int, parent: ViewGroup) :
    FormViewHolder(inflater, resource, parent) {
    private var questionTextView : TextView? = null
    private var priceIEdt : EditText? = null
     private var actionYesRadioBtn : RadioButton? = null
    init {
        questionTextView = itemView.findViewById(R.id.question_text)
        priceIEdt = itemView.findViewById(R.id.price_edt)
        actionYesRadioBtn = itemView.findViewById(R.id.action_yes_radio_btn)
    }

    override fun bind(s: FormItem, listener: FormItemCallback?) {
        super.bind(s, listener)

        if (s is FormItemClosedQuestion) {
            questionTextView?.text = s.question
            priceIEdt?.setText(s.price)
            Log.d("FormClosedQuestionViewHolder", "bind(): ${s.question} has ${s.price}")
            actionYesRadioBtn?.setOnCheckedChangeListener { compoundButton, isChecked ->
                if(isChecked) {
                    priceIEdt?.visibility = View.VISIBLE
                } else {
                    priceIEdt?.visibility = View.GONE
                }
            }
            priceIEdt?.addTextChangedListener(object : TextWatcher {
                override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {

                }

                override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                    s.price = p0.toString()
                    listener?.onValueChanged(s)
                }

                override fun afterTextChanged(p0: Editable?) {
                }

            })

        }
    }
}
open class FormItemClosedQuestion : FormItem() {
    var question: String = ""
    var price : String = ""
}

fun <T : FormItemClosedQuestion> T.question(question: String) = apply {
    this.question = question
}
fun <T : FormItemClosedQuestion> T.price(price: String) = apply {
    this.price = price
}

availablity_and_price_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:layout_margin="10dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <TextView
        android:id="@+id/question_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Toblerone Stick Chocolate Honey and Almond 100 ML"
        />
    <RadioGroup
        android:id="@+id/actions_toggle_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginHorizontal="20dp"
        android:layout_marginVertical="5dp"
        android:elevation="5dp"
        android:orientation="horizontal">

        <RadioButton
            android:id="@+id/action_yes_radio_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Yes" />

        <RadioButton
            android:id="@+id/action_no_radio_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:buttonTint="@color/teal_200"
            android:checked="true"
            android:text="No" />
    </RadioGroup>

    <EditText
        android:id="@+id/price_edt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="numberDecimal"
        android:padding="10dp"
        android:imeOptions="actionDone"
        android:visibility="gone"
        android:hint="Price" />

</LinearLayout>

While creating the example project I found out that the issue only happens when you increase the number of elements. When you test the app with 5 FormItems in the section, it would work fine but it would break when you increase them to 15.

tianzhuqiao commented 2 years ago

Thanks @WaheedAbbas, could you email me the minimal project( ben.qiao@gmail.com)?

WaheedAbbas commented 2 years ago

I have shared the project with you using OneDrive Link since Gmail was blocking it for security reasons. Let me know if you get it. Thanks. :) @tianzhuqiao

tianzhuqiao commented 2 years ago

@WaheedAbbas , the issue is in the way to call addTextChangedListener; it shall be called in init (as it only needs to be set once), not in bind. bind will be called many times, e.g., if in bind, when re-use the cell, priceIEdt?.setText(s.price) may trigger the onTextChanged (set by previous item, so s is also from the previous item).

init {
        questionTextView = itemView.findViewById(R.id.question_text)
        priceIEdt = itemView.findViewById(R.id.price_edt)
        actionYesRadioBtn = itemView.findViewById(R.id.action_yes_radio_btn)

        actionYesRadioBtn?.setOnCheckedChangeListener { compoundButton, isChecked ->
            if(isChecked) {
                priceIEdt?.visibility = View.VISIBLE
            } else {
                priceIEdt?.visibility = View.GONE
            }
        }
        priceIEdt?.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            }

            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                (item as? FormItemClosedQuestion)?.let {
                    it.price = p0.toString()
                    listener?.onValueChanged(it)
                }
            }

            override fun afterTextChanged(p0: Editable?) {
            }
        })
    }
WaheedAbbas commented 2 years ago

Thank you so much. @tianzhuqiao That solved the issue for me. I also had to add a property in FormItem class to set and get value for the radio buttons. FormItemClosedQuestion:

open class FormItemClosedQuestion : FormItem() {
    var question: String = ""
    var price : String = ""
    var isYes = false
}

Then get in bind method.

 override fun bind(s: FormItem, listener: FormItemCallback?) {
        super.bind(s, listener)

        if (s is FormItemClosedQuestion) {
            questionTextView?.text = s.question
            priceIEdt?.setText(s.price)
            actionYesRadioBtn?.isChecked = s.isYes
            actionNoRadioBtn?.isChecked = !s.isYes

            Log.d("FormClosedQuestionViewHolder", "bind(): ${s.question} has ${s.price}")

        }
    }

Now it works perfectly fine. Thanks again for such an awesome library.