基于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
相关产品推荐
相关产品推荐

