material-components / material-components-android

Modular and customizable Material Design UI components for Android
Apache License 2.0
16.24k stars 3.05k forks source link

[AutoCompleteTextView] Drop down items fail to show once fragment is destroyed and re-created #2171

Closed vipulvishnoi06 closed 2 years ago

vipulvishnoi06 commented 3 years ago

Description:

My app is a single activity with multiple parent and nested fragments. In one of the child fragments I use viewpager2 component to show different fragments depending on the tab selected.

For the sake of simplicity, let's say I have a TextInputLayout + AutoCompleteTextView control on tab1. There are a total of 8 tabs and other tabs have a combination of other controls - TextViews, EditTexts, etc.

Steps on how to reproduce:

  1. On first creation of tab1, I can see all the drop down items on the AutoCompleteTextView control.
  2. I navigate to tab2, tab3, tab4... so on... until at some point fragment manager decides to destroy tab1 and onDestroyView is called on tab1 Fragment
  3. Now, if I navigate back to tab1, onCreateView is called and I cannot see all the items in AutoCompleteTextView control anymore, except the previously selected one.

Sample code:

view_shared_spinner.xml:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/sp_name"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:minWidth="240dp">

        <AutoCompleteTextView
            android:id="@+id/sp_value"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="none"
            tools:ignore="LabelFor" />

    </com.google.android.material.textfield.TextInputLayout>

</merge>

SpinnerSharedView.kt:

class SpinnerSharedView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    private var binding: ViewSharedSpinnerBinding
    private var currentSelectedItemPosition: Int = -1

    init {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        binding = ViewSharedSpinnerBinding.inflate(inflater, this)
        binding.spName.hint = "my spinner"
    }

    fun getSelectedItemPosition(): Int {
        return currentSelectedItemPosition
    }

    // This method is used to attach a view to the 'Register' data model object. It is called in onViewCreated method of the fragment. This is how user updates the data object
    fun attach(
        reg: Register?, items: Array<String>, callback: (Register) -> Unit
    ) {
        reg?.let { // Register is just a data model class

            val adapter: ArrayAdapter<String> = ArrayAdapter<String>(context, R.layout.list_item_spinner, items)
            binding.spValue.setAdapter(adapter)
            // setOnItemSelectedListener doesn't work on AutoCompleteTextView, instead we should use
            // setOnItemClickListener
            binding.spValue.setOnItemClickListener { _, _, position, _ ->
                if (currentSelectedItemPosition != position) {
                    currentSelectedItemPosition = position
                    it.value = position.toDouble() 
                    callback.invoke(reg)
                }
                clearFocus()
            }
        }
    }

    // This method is called when ViewModel observer sees changes to the 'Register' data model object.  This is how updates to the data object are passed to the view
    fun update(reg: RegisterDec?, map: Map<Int, Int>? = null) {
        reg?.let {
            val tmp = reg.value.toInt()
            val mappedValue = if (map == null) tmp
            else map[tmp] ?: 0
            currentSelectedItemPosition = mappedValue
            binding.spValue.setText(binding.spValue.adapter.getItem(mappedValue).toString(), false)
        }
    }

Tab1 layout:

<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="8dp"
    android:layout_marginBottom="8dp"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

            <!--Other controls omitted-->

            <!--Control Mode-->
            <com.nvent.android.elexant5010imanager.view.shared.SpinnerSharedView
                android:id="@+id/mode"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:name="@string/str_mode"
                app:name_append="" />

        </LinearLayout>

</HorizontalScrollView>
class Tab1: Fragment() {

    private var _binding: FragmentTab1Binding? = null
    private val binding get() = _binding!!

    private val viewModel: Tab1ViewModel by activityViewModels {
        InjectorUtils.provideTab1ViewModelFactory()
    }

    private val controlModeItems =
        arrayOf("N/A", "TS1", "TS2", "TS3", "TS4", "TS5", "TS6", "TS7", "TS8", "Average", "Lowest")

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        _binding = FragmentTab1Binding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        attachUI(viewModel.myDataModel)
        observeUI()
    }

    private fun attachUI(d: myDataModel) {
        binding.mode.attach(d.controlMode, controlModeItems, ::editItemCallback)
    }

    private fun observeUI() {
        viewModel.myDataModelLive.observe(viewLifecycleOwner) {
            it?.let {
                binding.mode.update(it.controlMode)
            }
        }
    }

    private fun editItemCallback(reg: Register) {
        viewModel.update(reg)
    }
}

Version Info: Andnrod API 11 Navigation version: 2.3.5 Fragment version: 1.3.2 Material design version: 1.2.1 also tried with 1.3.0

drchen commented 2 years ago

Duplicate of #1464