Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting EditTexts Values From RecyclerView

I am building an app where user is required to fill some data in order to post something, so a fragment consists of EditText, radio buttons and Spinner along with RecyclerView which dynamically renders a number of child layout containing TextView and EditText.

So when user select category from Spinner, some properties which are related to that category are displayed in RecyclerView and user can optionally fill some of them.

I have tried to implement this functionality using callback and TextWatcher but I don't get the values I want.

CallBack

interface PropertiesCallback {
    fun addProp(position: Int, title: String, value: String)
}

Adapter

class PropertiesAdapter(private val propertiesCallback: PropertiesCallback)
    : RecyclerView.Adapter<PropertiesAdapter.ViewHolder>() {

    private var list = listOf<CategoriesAndSubcategoriesQuery.Property>()

    fun setData(listOfProps: List<CategoriesAndSubcategoriesQuery.Property>) {
        this.list = listOfProps
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.z_property_input, parent, false)
        return ViewHolder(view)
    }

    override fun getItemCount(): Int = list.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(list[position], position)
    }

    inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        private val label: TextView = view.findViewById(R.id.label)
        private val input: EditText = view.findViewById(R.id.input)

        fun bind(prop: CategoriesAndSubcategoriesQuery.Property, position: Int) {
            label.text = prop.title()

            prop.hint()?.let { input.hint = prop.hint() }

            input.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {}

                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                    propertiesCallback.addProp(position, prop.title(), input.text.toString())
                }
            })
        }
    }
}

In Fragment

private var propertiesList = mutableListOf<CategoriesAndSubcategoriesQuery.Property>()
private var propertiesInputList = mutableListOf<ProductPropertiesInput>()



  private fun setUpSubcategorySpinner() {
        subcategoriesAdapter = ArrayAdapter(
                [email protected]!!,
                android.R.layout.simple_spinner_item,
                subcategoriesList
        )
        //Subcategories
        subcategoriesAdapter.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line)

        subcategory_spinner.adapter = subcategoriesAdapter

        subcategory_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
                subcategoryId = subcategoriesList[position].id()

                //Adding properties
                subcategoriesList[position].properties()?.let {
                    //Clear previous properties data of another subcategory.
                    propertiesInputList.clear()
                    propertiesList.clear()

                    propertiesList.addAll(it)
                    propertiesAdapter.setData(propertiesList)
                    propertiesAdapter.notifyDataSetChanged()
                }
            }

            override fun onNothingSelected(parent: AdapterView<*>) {}
        }
    }

overide

 override fun addProp(position: Int, title: String, value: String) {
        val prop = ProductPropertiesInput
                .builder()
                .title(title)
                .value(value)
                .build()

        propertiesInputList.add(prop)

        //Log.d(TAG, "prop: ${prop.title()} : ${prop.value()}")
    }

submit fun

    private fun submitProduct() {
        //Initializing properties.
        val properties: Any

        //The keys needed in final list.
        val propertyKeys = propertiesList.map { it.title() }

        //Removing objects which keys are not needed.
        propertiesInputList.removeAll { it.title() !in propertyKeys }

        Log.d(TAG, "propertiesInputList: $propertiesInputList")

        //Removing duplicate and assign result in properties var.
        properties = propertiesInputList
                .distinctBy { it.title() }

        Log.d(TAG, "properties: $properties")

        for (prop in properties) {
        Log.d(TAG, "properties , title: ${prop.title()}, value: ${prop.value()} ")
    }
}

Above codes is intended to work as. When user types a value in one of the EditText in RecyclerView the value will be taken to fragment and added to an object which takes title and value and then added to propertiesInputList.

Problem 1: propertiesInputList will have so many duplicates objects with the same title and I thought the best solution was using distinctBy.

Problem 2: When user fills a number of EditText which are related to let's say category1 and changes his mind and select another category from Spinner. The previous values which are not part of new chosen category remain in propertiesInputList list. So I thought the best solution was to clear propertiesInputList and using removeAll with the titles related to category to filter unwanted objects.

But now I get only the first letter user types. If user types shoes I get s. So it seems distinctBy returns the first object but I want to get exactly last word user typed and if the user typed and erased everything I want blank.

Is there a better solution to handle this? Like looping recyclerView only when user press submit instead of TextWatcher? Or which part should I fix to make this work?

like image 970
Nux Avatar asked May 22 '19 22:05

Nux


2 Answers

I don't completely understand what you are trying to achieve here. EditTexts inside a RecyclerView is generally not a good idea for following reasons.

  1. When the recyclerView is scrolled, you would want to preserve the text added by the user for that particular field/item and show it correctly when the user scrolls back.
  2. When you add a TextWatcher to an EditText, you also need to remove it when the view is recycled or the view holder is bound again. Otherwise, you will end up with multiple listeners and things will go wrong.

For the other question that you have,

But now I get only the first letter user types. If user types shoes I get s

That's by design. TextWatcher would emit event every time a character is entered. So you would get s, sh, sho, shoe, shoes. So you can not take an action on this data because the user is still adding something to that field.


So,

  • You don't know when the user has stopped adding the text to the EditText (or whether user is done). You could use something like debounce but that is complicated. You should give a button to the user. Take the value when the user taps the button.

  • I am assuming you have multiple edittexts in the RecyclerView. So you would need to store the values for each edittext because the recyclerview will re-use the views and you'll lose the data. You could do that in your adapter's onViewRecycled callback. Keep a map of id -> string where you store this data and retrieve when the view holder is bound.

  • You could also use a TextWatcher but you would have detach it before attaching a new one or in onViewRecycled.

Update:

If I had something like this, I would use a ScrollView with a vertical LinearLayout (for simplicity) and add EditText based on the requirements. If you want to add TextWatcher, you'd need some kind of stable id.

class EditTextContainer : LinearLayout {

    private val views = mutableListOf<EditText>()
    private val textWatchers = hashMapOf<Int, TextWatcher>()

    ... constructor and bunch of stuff

    fun updateViews(items: List<Item>, textCallback: (id, text) -> Unit) {
        // Remove text watchers
        views.forEach { view ->
             view.removeTextWatcher(textWatchers[view.id])
        }

        // More views than required
        while (views.size > items.size) {
            val view = views.removeAt(views.size-1)
            removeView(view)
        }

        // Less views than required
        while (view.size < items.size) {
            val view = createView()
            view.id = View.generateViewId()
            addView(view, createParams()) // Add this view to the container
            views.add(view)
        }

        // Update the views
        items.forEachIndexed { index, item ->
            val editText = views[item]
            // Update your edittext.
             addTextWatcher(editText, item.id, textCallback)
        }
    }

     private fun createView(): EditText {
         // Create new view using inflater or just constructor and return
     }

     private fun createParams(): LayoutParams {
         // Create layout params for the new view
     }

     private fun addTextWatcher(view, itemId, textCallback) {
         val watcher = create text watcher where it invokes textCallback with itemId
         view.addTextWatcher(watcher)
         textWatchers[view.id] = watcher
     }
}
like image 180
Froyo Avatar answered Oct 17 '22 20:10

Froyo


Your inputs are less to identify the issue. I guess you are making some data collection application with the list of edit text. There is a an issue when you were using the edit text in recycler list. When you scroll down the bottom edit text in the recycler view will be filled with already filled edit text value, even though you user is not filled.

As a work around You can create some sparse array any data structure which will best suitable for you, that can map you position and value like mPropertyValue[] = new String [LIST_SIZE]. , assuming that position of ur list item matches with index of array.

Try updating the index with the value of text watcher mPropertyValue[POSITION] = YOUR_EDIT_TEXT_VALUE

When you want to initialize your edit text use the value by mPropertyValue[POSITION]

You can always make sure that your edit text will be having the right value by this .

like image 23
user3135923 Avatar answered Oct 17 '22 20:10

user3135923