You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于TextInputLayout和AutoCompleteTextView实现省市县联动自定义Spinner

联动选择实现方案(国家→州→地区)

需求说明

使用TextInputLayout+AutoCompleteTextView实现三级联动选择控件,选中国家后通过Retrofit在后台线程请求州数据,选中州后再请求地区数据;技术栈为Kotlin+Retrofit,使用自定义Adapter适配数据。

完整布局代码

<!-- 国家选择控件 -->
<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/selectCountryLayout"
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="10dp"
    android:layout_marginEnd="10dp"
    android:hint="@string/select_country">

    <AutoCompleteTextView
        android:id="@+id/selectCountry"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>

<!-- 州/省选择控件 -->
<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/selectStateLayout"
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="10dp"
    android:layout_marginEnd="10dp"
    android:hint="@string/select_state"
    android:enabled="false">

    <AutoCompleteTextView
        android:id="@+id/selectState"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>

<!-- 地区选择控件 -->
<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/selectDistrictLayout"
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="10dp"
    android:layout_marginEnd="10dp"
    android:hint="@string/select_district"
    android:enabled="false">

    <AutoCompleteTextView
        android:id="@+id/selectDistrict"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>

实现步骤

1. 定义数据模型

根据接口返回结构创建对应数据类:

data class Country(
    val id: Int,
    val name: String
)

data class State(
    val id: Int,
    val name: String,
    val countryId: Int
)

data class District(
    val id: Int,
    val name: String,
    val stateId: Int
)

2. Retrofit API接口定义

使用协程挂起函数实现异步网络请求:

interface LocationApi {
    // 获取所有国家列表
    @GET("countries")
    suspend fun getCountries(): List<Country>

    // 根据国家ID获取州列表
    @GET("states")
    suspend fun getStates(@Query("countryId") countryId: Int): List<State>

    // 根据州ID获取地区列表
    @GET("districts")
    suspend fun getDistricts(@Query("stateId") stateId: Int): List<District>
}

// 初始化Retrofit单例
val retrofit = Retrofit.Builder()
    .baseUrl("https://your-api-domain.com/api/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val locationApi = retrofit.create(LocationApi::class.java)

3. 自定义通用Adapter

支持泛型,可复用在三个选择控件上:

class LocationAdapter<T>(
    context: Context,
    private val resource: Int,
    items: List<T>
) : ArrayAdapter<T>(context, resource, items) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view = super.getView(position, convertView, parent)
        val textView = view.findViewById<TextView>(android.R.id.text1)
        val item = getItem(position)
        textView.text = when (item) {
            is Country -> item.name
            is State -> item.name
            is District -> item.name
            else -> ""
        }
        return view
    }

    override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
        return getView(position, convertView, parent)
    }
}

4. 联动逻辑实现(Activity/Fragment内)

使用Kotlin协程处理后台请求,更新UI状态:

class LocationPickerActivity : AppCompatActivity() {
    private lateinit var selectCountry: AutoCompleteTextView
    private lateinit var selectState: AutoCompleteTextView
    private lateinit var selectDistrict: AutoCompleteTextView

    private lateinit var countryAdapter: LocationAdapter<Country>
    private lateinit var stateAdapter: LocationAdapter<State>
    private lateinit var districtAdapter: LocationAdapter<District>

    private val coroutineScope = CoroutineScope(Dispatchers.Main + Job())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_location_picker)

        initViews()
        loadCountries()
        setupCountrySelectionListener()
    }

    private fun initViews() {
        selectCountry = findViewById(R.id.selectCountry)
        selectState = findViewById(R.id.selectState)
        selectDistrict = findViewById(R.id.selectDistrict)

        countryAdapter = LocationAdapter(this, android.R.layout.simple_spinner_item, emptyList())
        stateAdapter = LocationAdapter(this, android.R.layout.simple_spinner_item, emptyList())
        districtAdapter = LocationAdapter(this, android.R.layout.simple_spinner_item, emptyList())

        selectCountry.setAdapter(countryAdapter)
        selectState.setAdapter(stateAdapter)
        selectDistrict.setAdapter(districtAdapter)
    }

    private fun loadCountries() {
        coroutineScope.launch {
            try {
                val countries = locationApi.getCountries()
                countryAdapter.clear()
                countryAdapter.addAll(countries)
                countryAdapter.notifyDataSetChanged()
            } catch (e: Exception) {
                Toast.makeText(this@LocationPickerActivity, "加载国家失败", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun setupCountrySelectionListener() {
        selectCountry.setOnItemClickListener { _, _, position, _ ->
            val selectedCountry = countryAdapter.getItem(position) ?: return@setOnItemClickListener
            
            selectStateLayout.isEnabled = true
            selectState.setText("", false)
            selectDistrictLayout.isEnabled = false
            selectDistrict.setText("", false)

            coroutineScope.launch {
                try {
                    val states = locationApi.getStates(selectedCountry.id)
                    stateAdapter.clear()
                    stateAdapter.addAll(states)
                    stateAdapter.notifyDataSetChanged()
                    setupStateSelectionListener()
                } catch (e: Exception) {
                    Toast.makeText(this@LocationPickerActivity, "加载州失败", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun setupStateSelectionListener() {
        selectState.setOnItemClickListener { _, _, position, _ ->
            val selectedState = stateAdapter.getItem(position) ?: return@setOnItemClickListener
            
            selectDistrictLayout.isEnabled = true
            selectDistrict.setText("", false)

            coroutineScope.launch {
                try {
                    val districts = locationApi.getDistricts(selectedState.id)
                    districtAdapter.clear()
                    districtAdapter.addAll(districts)
                    districtAdapter.notifyDataSetChanged()
                } catch (e: Exception) {
                    Toast.makeText(this@LocationPickerActivity, "加载地区失败", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        coroutineScope.cancel()
    }
}

关键注意事项

  • 协程生命周期管理:页面销毁时取消协程任务,避免内存泄漏。
  • 控件状态重置:选择上级选项时,清空下级控件内容并重置启用状态,防止数据混乱。
  • 异常处理:每个网络请求都需捕获异常,给用户明确的错误提示。
  • Adapter复用:泛型Adapter减少重复代码,统一数据显示逻辑。

内容的提问来源于stack exchange,提问作者Raja Mali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 14:55:00