news 2026/10/10 17:19:42

Android城市选择器实现指南:数据模型、索引列表与避坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android城市选择器实现指南:数据模型、索引列表与避坑实践

简介:一款仿美团界面的Android城市选择器组件资源包,面向需要在Android应用中快速集成城市选择功能的开发者,可解决城市列表展示、热门城市排序、定位获取以及选择结果回调等常见需求,省去从零搭建的时间和成本。组件基于高德地图引擎实现位置定位,内置简洁API与模块化结构,支持自定义城市数据和UI样式,适配电商、旅游、社交、生活服务等多种应用场景。资源包共285个文件,其中164个class为编译产物,31个java源代码便于阅读与修改,40个xml用于界面布局和配置,22个png为界面素材,另有gradle、jar等工程配置文件,整体仅908KB,结构紧凑便于集成。已有929人学习下载。压缩包内提供完整可运行的模块代码、调用示例及接入说明,可参考其高德地图初始化、城市数据组织、选择器弹窗交互等实现思路,帮助快速理解并二次扩展,对初中级开发者尤其友好。

1. 城市选择器不是单个 View:它要解决的三件事与两套典型形态

新 App 验收前一天,产品和你说:收货地址页那个城市选择器,现在打开要卡两秒,连按 C 字母还定位不到长春。这种场面在 Android 开发里不算少见——城市选择器看着只是一个弹窗,背后却同时压着三千多个行政区划数据的加载、中文转拼音与索引跳转、省市区三级联动、定位失败降级四件事。它不是一个能用 TextView 堆出来的控件,而是一套需要提前设计数据模型和交互链路的小型业务模块。这篇文章按一线落地顺序讲清楚:数据怎么组织、列表和滚轮怎么选、搜索定位怎么凑齐、哪些坑值得提前焊死,适合正在做地址模块、切城市页或者任何要省市区联动的同学照着复现。

2. 数据先行:省市区三级 JSON 的组织、解析与内存缓存怎么做

城市选择器最容易翻车的地方不在 UI,而在数据。很多团队第一版直接把后端接口返回的省市区列表塞进 Spinner,结果下拉数据不全、顺序错乱、两级城市直接变空列表。这里的关键是先确定数据模型,再谈界面。

2.1 省市区三级数据结构:用递归 Region 而不是三层硬编码类

常见做法是维护一份全国省市区数据,放在 assets 下的 JSON 文件里。先看数据结构怎么设计。如果把省、市、区各写一个 Kotlin 类,比如 Province、City、District,初看直观,但遇到直辖市和特别行政区就会很难受:北京市下面没有“市”这一级,或者“北京市 - 北京市 - 朝阳区”这种两级结构会让联动逻辑写出一堆 if 特判。

我一般会用一个递归的Region类统一表达省、市、区,核心是children列表:

data class Region( val name: String, // 名称,如"广东省" val code: String? = null, // 行政区划代码,后端回传时要用 val children: List<Region>? = null // 下级区域,没有下级就是叶子节点 )

对应的 JSON 文件组织方式是这样:

[ { "name": "广东省", "code": "440000", "children": [ { "name": "深圳市", "code": "440300", "children": [ { "name": "南山区", "code": "440305" }, { "name": "福田区", "code": "440304" } ] }, { "name": "广州市", "code": "440100", "children": [ { "name": "天河区", "code": "440106" } ] } ] }, { "name": "北京市", "code": "110000", "children": [ { "name": "北京市", "code": "110100", "children": [ { "name": "朝阳区", "code": "110105" } ] } ] } ]

选择递归模型而不是省市区三张平表,原因是它天然兼容两类真实数据:一类是广东这种标准的三级结构,另一类是北京、天津、港澳这种只有两级的行政区。联动界面拿到一个Region,如果children不为空就继续展开下一级;如果为空就直接把整条链路回传给业务方,不需要任何特判。code字段必须保留,因为最终提交订单、设置收货地址时后端要的是行政区划代码,不是中文名。真实业务里这份 JSON 文件压缩后通常也就一两百 KB,对 APK 体积没有任何可见压力,完全没必要为它专门建一个 Room 数据库。

2.2 异步加载与解析:assets 文件不能在主线程读

确定好结构后,下一步是加载和解析。assets 文件读取属于磁盘 IO,Gson 反射解析在低端机上跑一次也要几十毫秒,放主线程会造成进入页面时掉帧甚至 ANR。我通常把加载逻辑放在一个单例 Provider 里,用协程切到 IO 线程,并把解析结果缓存在内存中:

object CityProvider { private var root: List<Region>? = null suspend fun load(context: Context): List<Region> { root?.let { return it } // 内存缓存命中 return withContext(Dispatchers.IO) { val input = context.assets.open("region.json") val json = input.bufferedReader().use { it.readText() } val type = object : TypeToken<List<Region>>() {}.type Gson().fromJson<List<Region>>(json, type).also { root = it } } } }

这段代码的逻辑说明:root是进程级缓存,第二次进入页面直接返回,避免重复解析同一份 JSON;withContext(Dispatchers.IO)保证文件读取和 JSON 解析都不占用主线程;bufferedReader().use { it.readText() }会在读取结束后自动关闭流,防止 assets 句柄泄漏。参数说明里有两个值得注意的点:一是TypeToken必须写成List<Region>的匿名内部类形式,否则 Gson 泛型擦除后拿不到真实类型;二是use块的作用域要覆盖整个读取过程,不能只包open那一步就提前释放流。

内存缓存放单例有一个隐患:进程被杀重建时单例会被清空,这没问题;但如果你的 APK 里同时存在多套数据源,比如测试环境和正式环境的 region.json 不同,单例缓存会把第一次加载的数据带到第二次页面,造成“换环境不生效”的假象。解决方式是把CityProvider的控制权交给你项目里的依赖注入容器,或者干脆在Activity的ViewModel里持有加载结果,按页面生命周期管理缓存。数据量本身不大,即使每次进页面重新解析,也就几十毫秒到一两百毫秒的成本,优先保证逻辑简单和可测试。

2.3 要不要上数据库或网络下发:按团队能力决定

assets 内置 JSON 不是唯一方案,这里把常见的三种数据源做个对比,方便你按项目情况选:

方案优点缺点适用场景
assets 内置 JSON离线可用、启动快、无接口依赖城市区划调整后需要发版更新大部分国内业务,够用
Room / SQLite支持增量更新、按名称索引查询快开发成本高、首次建库耗时超大数据量、复杂查询场景
接口下发区划调整即时生效、可灰度依赖网络、有失败态、要设计降级强运营、行政区划频繁变化的业务

我见过最稳妥的组合是:assets 内置一份完整 JSON 作为兜底,同时提供一个轻量接口做增量更新,服务端返回“省市区版本号”,客户端本地缓存的版本号低于服务端时再拉新数据。这个方案成本主要在接口设计,UI 层的解析逻辑完全不用动。如果你们团队只有两三个人且没有服务端资源,老实把数据放 assets 里,等真有区划调整时再发个版本,比为了更新去做一套增量协议更划算。

3. 列表还是滚轮:选型对比与带索引列表的 RecyclerView 落地实现

数据层就绪后,界面形态的选择会成为第一个争论点。产品可能直接丢来一句“参考 iOS 的滚轮”,但 Android 上滚轮和字母索引列表是两套完全不同的交互范式,各自有适配场景和实现成本,需要先想清楚再动手。

3.1 滚轮联动和字母索引列表的适用场景

省市区三级滚轮的核心价值是“结构化选择”,用户从省开始逐级缩小范围,适合收货地址、户籍所在地这类必须选完整三级结构的表单。缺点是操作步骤多,选完一个省还要再拨两个滚轮,如果用户目标是“深圳市南山区”,他得至少滑动三次。

字母索引列表的价值是“目标明确时的快速选择”,右侧一划直接跳到拼音首字母,配合搜索框,从打开到选中可能只要几秒。它更适合切换城市、选择出发地、绑定常驻城市这类只选一层、不强制省市区串成完整链路的场景。商用 App 里你看到的大部分“城市选择器”其实是字母索引列表加搜索框,滚轮反而更多出现在表单里的“省市区”三联动控件中。

这两个方案不冲突。如果一个页面既要快速选城市,又需要省市区完整回传,可以按下级联动的思路把它们串起来:先用字母索引列表选中省,再进入该省的城市列表,这样既保留了快速检索,又天然完成了三级选择。但要注意:不要在一个页面里同时放滚轮和索引列表,用户会迷惑到底该用哪个,交互成本反而翻倍。

3.2 带右侧字母索引的城市列表:RecyclerView 落地写法

针对“切换城市”这个最常见的标题需求,我一般选择字母索引列表方案。它的核心由三部分组成:按拼音排序并分组的数据列表、右侧 26 个字母的索引条、以及索引条和列表的联动。先看列表适配器怎么写。城市数据经过预处理后会变成两类行:字母分组标题行和城市行,适配器内部用一个密封类来表示:

sealed class Row { data class Header(val letter: String) : Row() data class City(val region: Region, val pinyin: String, val shortPinyin: String) : Row() } class CityAdapter( private val onCityClick: (Region) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val rows = mutableListOf<Row>() private val indexMap = mutableMapOf<String, Int>() // 字母 -> 列表position fun submit(data: List<Region>) { rows.clear() indexMap.clear() data.groupBy { it.firstLetter() } // 按首字母分组 .toSortedMap() .forEach { (letter, cities) -> indexMap[letter] = rows.size rows.add(Row.Header(letter)) cities.forEach { rows.add(Row.City(it, it.fullPinyin(), it.firstLetter())) } } notifyDataSetChanged() } override fun getItemViewType(position: Int): Int = if (rows[position] is Row.Header) TYPE_HEADER else TYPE_CITY // onCreateViewHolder / onBindViewHolder 按类型 inflate 对应布局并绑定点击 }

这段代码的逻辑说明:submit是数据入口,先把城市列表按首字母分组,toSortedMap保证从 A 到 Z 的展示顺序;indexMap记录每个字母第一次出现的列表 position,索引条滑动时直接用这个映射做跳转;getItemViewType让 RecyclerView 可以复用两种不同的 item 布局,分组标题行显示一个灰色字母背景,城市行显示城市名和拼音。参数说明里最关键的是indexMap的更新时机:每次submit时必须先清空再写入,否则索引条跳转到错误位置很难排查。

右侧索引条的自定义 View 是联动逻辑的另一半,我通常直接复用一个轻量控件,核心是触摸事件处理和回调:

class IndexBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private val letters = ('A'..'Z').toList() var onLetterChange: ((String) -> Unit)? = null override fun onTouchEvent(event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> { val y = event.y.coerceIn(0f, height.toFloat()) val index = (y / height * letters.size).toInt().coerceIn(0, letters.size - 1) onLetterChange?.invoke(letters[index].toString()) return true } } return false } }

逻辑说明:letters固定为 26 个英文字母,滑动时通过event.y占height的比例换算成字母索引;coerceIn两处都是防越界,一处是手指滑出控件顶部或底部时把坐标拉回边界内,另一处是计算出的字母下标不能超过 25;ACTION_UP也回调一次是为了保证手指离开时列表一定停在目标位置,否则快速滑动后突然抬手,MOVE 事件没来得及触发最后一次跳转,列表就会停在半路。参数说明:coerceIn(0f, height.toFloat())这里的下限必须写 0f 而不是 1f,否则首尾两个字母永远无法通过滑动命中的 bug 很难发现。调用方在 Activity 里拿到回调字母后,用indexMap[letter]找到目标 position,再调用layoutManager.scrollToPositionWithOffset(position, 0)完成跳转。

3.3 滚轮联动的实现思路:用 RecyclerView 代替自绘 WheelView

如果你最终选了滚轮方案,不建议从零自绘 WheelView。自绘控件的坑集中在惯性计算和回弹动画,这两块非常容易翻车,而且各厂商 ROM 对触摸事件的处理不一致,会出现“小米上滑得飞快、华为上回弹不到位”的玄学差异。常见做法是用横向排列的三个 RecyclerView,每个 RecyclerView 搭配一个 SnapHelper,让系统 RecyclerView 的惯性滚动去处理滚轮效果,自绘部分只剩下画选中线、放大选中项文字这种纯装饰逻辑。

三级联动的核心是数据源切换:省级 RecyclerView 的选中项变化后,市级 RecyclerView 的所有 item 换成当前省下的children,并把选中位置重置为 0;市级选中项变化后,区级同理。这里有一个必须处理的细节:当某一级的children为空时,应该直接回调整个链路并关闭选择器,而不是让界面停留在空白状态。这就是递归Region模型带来的便利,联动代码里只需要判断item.children.isNullOrEmpty(),不需要针对直辖市写额外条件。如果你用三层独立的 Province/City/District 类,这里的特判会散落到每一级联动逻辑里,维护成本立刻上去。

4. 搜索、热门城市与定位:把选择器做成“能用”的四个交互细节

列表和索引解决了“按字母找”的问题,但真实用户更常做的是输入汉字、全拼或首字母搜索,比如输入“shenzhen”“sz”甚至直接输“深圳”。同时,热门城市和定位城市这两个产品标配交互,也会直接影响选择器的完整度。本章把这些交互拆开讲,每一块都有明确的实现参数和失败降级策略。

4.1 支持中文、全拼、首字母的搜索匹配函数

搜索匹配不能只做contains("城市名")。完整的匹配策略要同时覆盖三种输入:中文名、完整拼音、拼音首字母。匹配函数长这样:

fun matches(region: Region, keyword: String): Boolean { val kw = keyword.trim().lowercase() if (kw.isEmpty()) return true if (region.name.contains(kw)) return true // 中文:直接包含 val fullPinyin = region.fullPinyin().lowercase() // 全拼:shenzhen val shortPinyin = region.shortPinyin().lowercase() // 首字母:sz return fullPinyin.startsWith(kw) || shortPinyin.startsWith(kw) || fullPinyin.contains(kw) }

逻辑说明:先用name.contains(kw)覆盖中文输入,这是最快的一条分支;再用全拼和首字母做前缀匹配。我把fullPinyin.contains(kw)放在最后,是为了兼顾“记忆不全只输入中间几个字母”的情况,但这种模糊匹配会引入更多噪音,如果你们城市列表很长,建议去掉这一条只保留前缀匹配。参数说明里有三个细节:keyword必须 trim 再比较,用户输入带空格时不会误过滤;转小写要放在比较前,否则 “ShenZhen” 会匹配失败;城市名里如果有英文或数字,比如“阿坝”“那曲”这种带声调差异的名字,拼音转换库要先处理声调,否则shànghǎi和shanghai对不上。

搜索结果的展示方式也有讲究。不要用另一个全新的 RecyclerView 去替换主列表,而是复用一个过滤后的数据源,这样搜索结果项点击后的回调逻辑与列表中点击一致。搜索框的防抖也值得做:用户每敲一个字符就过滤一次是没问题的,因为城市列表最多几千条,本地过滤在内存里跑一遍也就是几十毫秒;但如果数据源来自网络接口,必须添加至少 300ms 的防抖再发请求,避免每敲一个字母都触发一次网络流量。

4.2 热门城市:不要动态算,内置固定列表更省心

热门城市的作用是减少 90% 以上用户的点击路径。实现上不需要复杂逻辑,在数据列表顶部固定插入一行“热门城市”分组,里面放六到九个高频城市即可。我通常的写法是在submit时把热门城市列表作为固定前置数据插入:

val hotCities = listOf( Region("北京", "110100"), Region("上海", "310100"), Region("广州", "440100"), Region("深圳", "440300"), Region("杭州", "330100"), Region("成都", "510100") ) fun submit(data: List<Region>) { rows.clear() rows.add(Row.Header("热门")) hotCities.forEach { rows.add(Row.City(it, it.fullPinyin(), it.firstLetter())) } rows.add(Row.Header("定位")) // ... 定位城市,失败时不展示此分组 // ... 再加载 alphabet 数据 }

逻辑说明:热门城市分组固定存在,不参与字母排序,放在列表最前端;点击后的onCityClick回调走的还是同一个Region对象,后续提交逻辑不需要区分热门和普通城市。参数说明:热门城市列表最好由产品经理配置而不是开发写死,因为业务活动经常调整入口城市;但开发侧至少要内置一份默认值,否则接口没下发时热门区域就空白了。这里有一个容易踩的地方:热门城市里“重庆市”的全拼是chongqing,不是zhongqing,在生成拼音时就要解决好,否则热门城市的数据也会被搜索功能带偏。

4.3 定位当前城市:Android 12 的模糊定位已经够用

定位城市的主流交互是:页面打开时申请定位权限,拿到坐标后反查到城市名,展示在“当前定位”这一行。反查城市名一般要接高德或百度定位 SDK,因为它内部已经集成了逆地理编码;如果不想引入第三方 SDK,只靠系统LocationManager拿坐标,还得自己写逆地理编码接口,不划算。这里重点说系统层面的权限适配。

Android 6.0 以后定位权限是动态申请,Android 12(API 31)开始引入了模糊定位权限。对于城市选择器这个场景,根本不需要精确权限,直接用模糊定位就够了,这样既降低用户授权心理门槛,也避免被应用商店审核时问“为什么需要精确定位”。权限申请的核心代码:

private fun requestLocationPermission() { val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { Manifest.permission.ACCESS_COARSE_LOCATION } else { Manifest.permission.ACCESS_FINE_LOCATION } if (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) { locateOnce() } else { requestPermissions(arrayOf(permission), REQUEST_LOCATION) } }

逻辑说明:Android 12 上只申请ACCESS_COARSE_LOCATION,系统会返回一个模糊位置,精度大概在几百米到几公里,对于“定位到城市”已经完全够用;Android 11 及以下仍然申请ACCESS_FINE_LOCATION,因为老版本上精细定位权限更容易被用户信任。参数说明:requestPermissions的请求码REQUEST_LOCATION需要是 Activity 级别的常量,回调里要区分isGranted和shouldShowRequestPermissionRationale两种拒绝情况,前者直接降级为“定位失败,请手动选择”,后者需要引导用户去设置页打开权限。

拿到坐标后切到后台线程做逆地理编码,成功则更新“当前定位”行,失败则隐藏该分组。我一般还会加一个 5 秒超时:定位 SDK 返回慢时不能一直让用户等,超时后直接把当前定位区域隐藏,不阻塞整个列表的展示。

5. 城市选择器避坑清单:多音字、两级城市与索引错位的排查记录

城市选择器看起来只有几百行代码,但实际接入业务后踩过的坑一个接一个。下面这五条是按真实发生频率排序的血泪经验,每一条我都按“现象 → 原因 → 解决”的方式记录下来,方便你直接对照排查。

5.1 多音字把“重庆”排进 Z 组,搜索 C 找不到

现象:索引条上点 C,列表里没有重庆;搜索 “chongqing” 也无结果,但搜索 “zhongqing” 反而能搜到。

原因:默认拼音转换库把“重”读成 zhòng,重庆被转成了zhongqing,首字母变成 Z。这个问题在“长安”(cháng 安)、“厦门”(xià 门)等城市名上同样存在。

解决:声明一个城市名到正确拼音的例外表,在生成全拼和首字母时做后处理。城市数量有限,只需要维护几十个高频例外词即可,比如重庆 -> chongqing、长安 -> changan、厦门 -> xiamen。注意例外表不能只覆盖全拼,首字母也必须同步修正。这个表建议放在CityProvider里,和城市数据一起加载,后续运营要加新城市时不用改代码。

5.2 直辖市和特别行政区只有两级,三级联动出现空白区

现象:滚轮方案里选完“北京市 - 北京市”后,第三个滚轮一片空白;列表方案里点进北京只看到一级城市就结束,不知道到底该不该继续往下点。

原因:数据结构里如果强制省市区三层数组,直辖市没有独立“市辖区”这一层时,第二层直接就是区,联动逻辑拿不到预期层级就渲染空白。

解决:用第二章的递归Region模型后,这里天然不会出错——children为空就该回调,不为空就继续展开。关键在于联动代码里禁止写死“必须选完三层才能确认”,而是以“某个节点没有子节点”为结束条件。如果你们用的还是老的三层硬编码结构,至少要在省选中时判断该省下是否只有一个市级节点且该市级节点直接包含区级数据,遇到这种情况把省级选择结果直接当成市级结果回传。

5.3 索引条快速滑动时列表停在半路,继续滑到 Z 没反应

现象:手指从索引条顶部快速划到底部,RecyclerView 跳到 Y 附近就停了,再往上滑一次才生效。

原因:索引条的ACTION_MOVE事件频率很高,但 MOVE 过程中可能因为触摸事件被系统拦截,导致最后一次跳转没有触发;另外scrollToPositionWithOffset如果目标 position 是分组标题,列表会停在标题位置而不是城市位置,视觉上就差了一个 item。

解决:两个地方要一起改。第一,ACTION_UP时必须再回调一次当前字母,确保抬手瞬间列表一定跳到位。第二,跳转位置不能直接用分组标题的 position,而是用该字母下第一个城市的 position;如果indexMap记录的是 Header 的 position,跳转时要手动加 1。如果列表顶部还有搜索框或热门城市,还要把这部分固定 header 的高度折算进 offset,否则列表总会整体偏移一段距离。

5.4 异步解析完成后 Activity 已销毁,更新列表直接崩

现象:快速进出页面后偶发IllegalStateException或 NPE,日志指向adapter.notifyDataSetChanged()调用处。

原因:CityProvider.load是异步方法,协程结果返回时 Activity 可能已经走完onDestroy,还在用旧 Activity 引用的 RecyclerView 去刷新数据。

解决:把数据加载放在ViewModel里,用viewModelScope启动协程,数据就跟随 ViewModel 生命周期而不是 Activity 生命周期。Activity 里只负责观察liveData结果并更新 UI,当 Activity 销毁时观察者自动移除,不会出现“数据回来了但页面没了”的情况。更早期项目如果还在用单例回调和接口实现,没有 ViewModel,至少要在异步回调里包一层if (activity.isDestroyed) return做防御。

5.5 为读本地城市数据申请存储权限,却被应用商店驳回

现象:assets 里明明放着 region.json,代码里却申请了READ_EXTERNAL_STORAGE,上架审核被驳回,提示“权限与功能不符”。

原因:assets 目录属于应用私有资源,读取它压根不需要任何存储权限。很多新手把“读本地文件”和“申请存储权限”划等号,不知道 assets 是个特殊入口。Android 11 之后存储权限本身还多了分区存储的限制,申请了也未必读得到公共目录数据。

解决:城市数据放 assets 永远是零权限方案。如果一定要做网络下发更新,下载的 JSON 文件放到context.filesDir应用私有目录里,同样不需要申请存储权限。这条不属于功能 bug,但却是最容易在上架环节翻车的合规问题,提前写出来提醒。

6. 验证与进阶:用 ADB 脚本和 UI Automator 守住选择器关键路径

城市选择器这类纯 UI 组件的回归测试,手工点一遍很费时间,而且三个关键链路——索引跳转、搜索命中、杭州点击回调——任何一个断了都不容易在开发自测阶段发现。我交付出城市选择器之前,一定会用 ADB 加 UI Automator 跑一遍核心用例,这是 Android 测试成本最低的验收方式。

先写一个验证脚本,核心是用uiautomator dump导出当前页面的控件树,再用input tap模拟点击。我这里给你一个可持续扩展的最小脚本框架:

# 1. 打开城市选择器页面,包名和 Activity 按你自己的工程替换 adb shell am start -n com.example.app/.CityPickerActivity sleep 1 # 2. 在搜索框输入 "shenzhen",验证深圳出现 adb shell input tap 500 150 adb shell input text "shenzhen" sleep 1 adb shell uiautomator dump /sdcard/search.xml adb shell cat /sdcard/search.xml | grep -q "深圳" && echo "搜索命中" # 3. 清除搜索,模拟右侧索引条从底部滑到顶部 adb shell input keyevent KEYCODE_MOVE_END adb shell input swipe 900 1800 900 300 300 sleep 1 adb shell uiautomator dump /sdcard/index.xml adb shell cat /sdcard/index.xml | grep -q "重庆" && echo "索引跳转命中"

脚本的逻辑说明:input tap的坐标要按真机分辨率换算,不同屏幕密度下同一控件位置差异很大,所以这组脚本适合在固定测试机上跑,不适合跨机型通用;uiautomator dump导出的 XML 里包含当前屏幕所有可访问节点的text和content-desc属性,搜索验证本质上就是检查关键城市名是否出现在节点树里。参数说明里有一个经验值:每个步骤之间的sleep不要小于 1 秒,尤其从input text到uiautomator dump之间,搜索是异步过滤的,dump 太早会拿到旧界面。

跑完脚本后,我还会手动再验证一个自动化容易覆盖不到的点:点击“重庆市”之后,回调里带着的行政区划代码必须是500100而不是500000。自动化能验证 UI 展示了什么,但验证不了回调数据的正确性,这一步我会在点击后打一行日志,确认链路最终上报的 code 和名称一致,再进真机看一次日志。另外,模拟器上跑这种脚本时偶尔会遇到点击落空,多半是动画还没结束就触发了下一次点击,脚本里适当加等待是对的,但等待时间别无脑拉长,间隔太长会让整个验证流程变得迟钝,也掩盖不了真实问题。这条脚本还有一个进阶用法:把它挂到 CI 上,配合构建产物做冒烟测试,城市选择器后续再怎么改,回归成本都控制在一次命令内。

我现在的习惯是:接到任何列表类组件的需求,先把这类验证脚本建好,再开始写 UI。这样开发过程中每改一次布局和联动逻辑,都能在两分钟内确认最关键的路径没断。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 17:18:47

Unet及注意力变体图像分割全流程实战与避坑指南

简介&#xff1a;图像分割中常用的UNet、注意力UNet、残差UNet及两者结合的变体&#xff0c;以可运行工程形式打包&#xff0c;附带ISIC 2017皮肤病变数据集子集。面向深度学习初学者和医疗影像分析研究者&#xff0c;省去自行搭建模型与寻找数据的麻烦&#xff0c;方便直接对比…

作者头像 李华
网站建设 2026/10/10 17:16:03

远离画饼陷阱,普通人的互联网真实赚钱路径与钱源思维

1. 先分清什么在给你画饼这些年见过太多人一头扎进互联网&#xff0c;拿着一堆课程截图和收益截图当救命稻草。我不能说这些全是假的&#xff0c;但可以负责任地讲一句&#xff1a;凡是告诉你“不用技能、不用积累、只要跟对项目就能月入过万”的&#xff0c;大概率是在给你画饼…

作者头像 李华
网站建设 2026/10/10 17:14:07

免费进销存源码实战:onlyit窗体程序部署与二次开发指南

简介&#xff1a;一款面向小型企业和个体经营者的免费进销存管理软件&#xff0c;基于窗体程序开发&#xff0c;提供进货、销售、库存、财务等核心管理功能&#xff0c;并附带OA源码&#xff0c;支持二次开发定制&#xff0c;适用于日常商业运营中的进销存流程优化。资源包共16…

作者头像 李华
网站建设 2026/10/10 17:14:05

GPU内核驱动安全与稳定性实战:内存漏洞、IOCTL攻防与TDR恢复

这一节&#xff0c;我们进入专栏第四部分“高级主题与实战”的4.3节。如果你跟着前面几节走到了现在&#xff0c;应该已经知道KMD&#xff08;Kernel Mode Driver&#xff0c;内核模式驱动&#xff09;的工作范围包含了命令提交、内存管理、电源控制、上下文切换等大量危险地带…

作者头像 李华
网站建设 2026/10/10 17:13:15

Spring Boot汽车维修保养系统设计:从业务流程到毕业设计落地

毕业设计这个圈子流传一句话&#xff1a;管理系统遍地走&#xff0c;但能不能拿到高分&#xff0c;看的是你有没有把"业务"真正装进代码里。今天要拆解的题目是"基于Spring Boot的汽车维修保养服务信息系统"&#xff0c;这个标题看起来平淡&#xff0c;其实…

作者头像 李华
网站建设 2026/10/10 17:11:01

12GB显存跑125B大模型:分层卸载与量化压缩实战

1. 12GB显存跑125B模型&#xff0c;这事到底靠不靠谱先把结论摆在前面&#xff1a;12GB显存的消费级显卡&#xff0c;确实可以运行125B参数级别的大模型&#xff0c;但前提是你得接受"分层卸载量化压缩CPU协同"这套组合拳&#xff0c;而不是指望模型全部塞进显存里。…

作者头像 李华