简介这是一份面向水利行业信息化管理的安卓源码项目旨在帮助移动端开发者、水利信息化工程师及高校相关专业学生快速理解行业应用的构建方法。资源包为压缩包格式整体大小约一点二八兆代码结构清晰涵盖界面布局、页面管理、数据库操作、网络通信、定位服务、通知提醒、权限管理、地图集成以及数据可视化等安卓开发关键模块。目前已有一百零六人学习参考适合作为行业移动应用开发的入门或实训样例。通过阅读源码可以梳理从界面搭建到业务逻辑处理的完整链路学习在真实业务场景中整合数据库框架、网络请求框架、图表框架等主流工具同时项目提供了水利设施监控、数据管理与实时交互的设计思路包括使用数据库存储水位信息、基于位置服务查找周边设施、通过通知机制实现预警提醒并对权限申请流程做了合理示范。1. 拆开一个水利行业 Android 源码能拿到哪些值得复用的东西水利行业的信息化管理移动端绕不开三个刚需设施数据要能查、水位预警要能推、现场位置要能看。宏华水利小程序这个 Android 源码项目看起来不大但把这三条链路都串了起来。它不是一个简单的 CRUD 工程而是把 SQLite 存储、网络请求、定位服务、地图展示、通知提醒这些移动端高频能力按行业场景重新组织了一遍。对 Android 开发者来说这类行业源码比通用 demo 有价值的地方在于你能看到数据层如何为告警服务、UI 如何适配现场操作、权限如何去配合业务流程。对刚入手 Android 的人来说它也是一个完整的模块化参考系——从界面到数据库再到远程通信每一条线都有对应的代码落点。下面的内容以HongHuaShuiLiXiaoChengXu目录为主线把工程结构、数据层、网络层、可视化与发布这几个关键环节逐一拆开讲。2. 读源码前先看工程结构包名、Gradle 与模块划分逻辑拿到一个 RAR 压缩的 Android 源码包第一步不是急着读 Java 文件而是先建立工程地图。宏华水利小程序的源码目录HongHuaShuiLiXiaoChengXu结构是否规范直接决定后续排查问题的效率。我一般会先看三个文件AndroidManifest.xml、build.gradle、以及 Java 包名下的目录树。一个典型的水利类 App 包结构通常按功能域而不是按层划分。这种划分方式在行业项目里很常见因为需求变更往往集中在某一业务域内比如水位计管理就是一个完整闭环从列表到详情到报警设置都放在一起改起来不用跨包跳来跳去。app/src/main/java/com/honghua/shuili/ ├── activity/ // 界面层一个界面一个 Activity │ ├── MainActivity.java │ ├── DataDetailActivity.java │ └── SettingsActivity.java ├── database/ // 数据库操作层 │ ├── DbOpenHelper.java │ └── WaterLevelDao.java ├── network/ // 网络请求层 │ ├── HttpUtil.java │ └── ApiConfig.java ├── model/ // 实体模型 │ ├── StationModel.java │ └── WaterLevelRecord.java ├── utils/ // 通用工具 │ ├── LocationUtils.java │ └── DateUtils.java └── receiver/ // 广播与告警接收器 └── AlarmReceiver.java2.1.1 Manifest 先行先确认权限与入口可以先打开AndroidManifest.xml把权限声明和入口 Activity 梳理一遍。水利类 App 最常见的权限组合是INTERNET、ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION、READ_EXTERNAL_STORAGE以及用于告警的POST_NOTIFICATIONSAndroid 13 及以上需要运行时申请。uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS /这些权限不是配置完就结束Android 6.0 之后危险权限必须在代码里动态申请Android 13 又单拆了一个通知权限。代码层面需要在MainActivity或启动页里统一做一次申请流程用ActivityCompat.requestPermissions()去触发系统弹窗并重写onRequestPermissionsResult处理用户拒绝的情况。2.1.2 Gradle 文件看依赖选型接着看app/build.gradle依赖列表能反映项目的技术路线。常见配置是这样dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.12.0 implementation com.github.PhilJay:MPAndroidChart:v3.1.0 implementation androidx.room:room-runtime:2.6.1 annotationProcessor androidx.room:room-compiler:2.6.1 }从依赖选择上可以反推项目形态用了 Retrofit 说明走的是标准 REST API 路线引入 Room 说明数据持久化不满足于裸写 SQL而是在向 ORM 靠拢引入 MPAndroidChart 说明界面层有数据可视化需求。这种组合几乎可以覆盖水利行业 App 的绝大多数场景。| 模块 | 技术选型 | 适用场景 | |------|---------|---------| | UI | XML Activity | 工具型 App单界面职责清晰 | | 数据库 | SQLite / Room | 水位数据、设施台账、报警记录 | | 网络 | Retrofit OkHttp Gson | 实时数据上报、远程配置拉取 | | 定位 | LocationManager / 高德 SDK | 设施定位、巡查签到 | | 通知 | NotificationManager / AlarmManager | 水位超限告警、定时巡测 | | 图表 | MPAndroidChart | 水位曲线、流量统计、趋势分析 |模块划分上建议遵循“高内聚、低耦合”的原则。数据层封装成独立的database包不直接暴露 SQLite 的连接细节上层界面只跟 DAO 打交道网络层用ApiConfig统一管理 Base URL 和超时配置避免各界面各自初始化 OkHttp。这样做的好处是当服务器地址变更或者数据库表结构升级时修改点能控制在一个包内。3. 数据层这么设计从 SQLiteOpenHelper 到 Room 的迁移思路水利行业的数据特点是频率高、维度多、单条体积小。一个水位监测站可能每五分钟上报一条记录一年累计超过十万条。这种写入压力下数据库设计必须考虑三个问题表结构是否支持高频插入、查询是否能走索引、以及本地缓存与服务器数据的同步策略。3.1.1 SQLiteOpenHelper 方式与建表细节如果源码用的是传统 SQLiteOpenHelper核心逻辑在继承类中重写onCreate和onUpgrade来管理表结构。一个水位记录表大致如下public class DbOpenHelper extends SQLiteOpenHelper { private static final String DB_NAME shuili.db; private static final int DB_VERSION 1; public DbOpenHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS water_level ( id INTEGER PRIMARY KEY AUTOINCREMENT, station_code TEXT NOT NULL, // 站点编号用于关联设施 water_level REAL NOT NULL, // 水位值单位米 collect_time INTEGER NOT NULL, // 采集时间戳 is_uploaded INTEGER DEFAULT 0)); // 是否已上传服务器 db.execSQL(CREATE INDEX idx_station_time ON water_level(station_code, collect_time DESC)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS water_level); onCreate(db); } }两个字段值得注意is_uploaded是断网续传的关键标志位App 离线时数据先落本地网络恢复后再遍历这个字段做增量同步collect_time必须加索引因为水位曲线查询永远是按时间范围和站点维度过滤的。idx_station_time这个联合索引能让WHERE station_code ? ORDER BY collect_time DESC这类查询直接走索引扫描避免全表排序。Android 原生 SQLiteOpenHelper 有个问题它把 SQL 语句散落在 Java 代码里表结构变更时要手动维护onUpgrade而且没有编译期检查字段改名时容易漏改查询语句。这里有一个通用的处理方式——先用原生方式跑通再用 Room 重构。3.1.2 Room 重构Entity、DAO、Database 三层Room 是 Google 官方推荐的 ORM它在 SQLite 之上增加了编译期校验。宏华水利这类数据表比较固定的项目迁移成本并不高。核心步骤是三个注解类。Entity(tableName water_level, indices {Index(value {station_code, collect_time})}) public class WaterLevelEntity { PrimaryKey(autoGenerate true) public int id; ColumnInfo(name station_code) public String stationCode; ColumnInfo(name water_level) public double waterLevel; ColumnInfo(name collect_time) public long collectTime; ColumnInfo(name is_uploaded) public int isUploaded; }DAO 层负责所有数据库访问操作Dao public interface WaterLevelDao { Query(SELECT * FROM water_level WHERE station_code :code ORDER BY collect_time DESC LIMIT :limit) ListWaterLevelEntity getLatestRecords(String code, int limit); Insert void insert(WaterLevelEntity entity); Query(UPDATE water_level SET is_uploaded 1 WHERE id IN (:ids)) void markUploaded(ListInteger ids); }最后是 Database 定义Database(entities {WaterLevelEntity.class}, version 2, exportSchema false) public abstract class AppDatabase extends RoomDatabase { public abstract WaterLevelDao waterLevelDao(); }Room 相比原生 SQLiteOpenHelper 的优势体现在日常维护上写错字段名编译期直接报错数据库升级可以用Migration类而不需要DROP TABLE避免用户升级版本后本地历史数据被清空。而且它天然支持Flow或LiveData返回类型水位数据变化时 UI 可以自动刷新不需要手动调用notifyDataSetChanged()。批量插入的耗时也是移动端数据库选型的一个重要考量。单条循环insert在 10 万条数据场景下可能超过 60 秒SQLite 事务模式下会大幅缩短db.beginTransaction(); try { for (WaterLevelEntity entity : list) { dao.insert(entity); } db.setTransactionSuccessful(); } finally { db.endTransaction(); }事务的关键点是setTransactionSuccessful()必须在endTransaction()之前调用否则整个事务会回滚。真实项目里水位数据通常是分批从服务器拉取的每批 500 条事务提交一次进度条按批次更新即可。参数方面建议按这个表去调优setWriteAheadLogging(true)开启 WAL 模式读和写可以并发执行setPageSize()调整为 4096 字节匹配现代存储设备的页大小setCacheSize()在内存充足时设置到 4MB减少磁盘 I/O。4. 网络层和定位模块的接线方式Retrofit 与高德地图的协同水利 App 的网络请求场景与普通电商类完全不同。请求周期短、频率高、数据实时性强而且经常要在地图上同步刷新点位数据。Retrofit OkHttp Gson 是目前行业项目里最稳定的组合定位与地图则优先考虑高德 SDK因为 Google Maps 在部分环境下的可用性存在限制而高德的国内地址编码和离线地图策略更适合野外巡查场景。4.1.1 Retrofit 接口定义网络层的第一步是定义 API 接口。水位数据接口一般会支持时间范围查询、站点分页、以及按 id 增量拉取三种模式。接口定义如下public interface ApiService { GET(api/station/list) CallStationResponse getStationList(Query(page) int page, Query(size) int size); GET(api/waterlevel/history) CallListWaterLevelRecord getHistoryData( Query(stationCode) String stationCode, Query(startTime) long startTime, Query(endTime) long endTime); POST(api/waterlevel/report) CallBaseResponse reportWaterLevel(Body WaterLevelReport report); }注意Query参数会被拼接在 URL 后缀上适合 GET 请求Body用于 POST 请求体Retrofit 会配合 GsonConverterFactory 自动把对象序列化成 JSON。接口的参数命名要跟后端约定保持一致否则会出现字段无法解析的隐蔽问题。常见做法是在字段标注SerializedName(station_code)来映射 JSON 里的下划线命名。4.1.2 OkHttp 拦截器与超时配置Retrofit 的底层网络栈是 OkHttp它的拦截器机制适合做统一的日志打印、Token 注入、以及重试逻辑。一个带 Token 的 OkHttp 配置如下OkHttpClient client new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) // 连接超时野外弱网环境下要放宽 .readTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .addInterceptor(chain - { Request original chain.request(); Request request original.newBuilder() .header(Authorization, Bearer getToken()) .header(Accept, application/json) .build(); return chain.proceed(request); }) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(https://api.honghua.example.com/) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); ApiService apiService retrofit.create(ApiService.class);这里的超时参数值得单独说水利现场经常是移动网络覆盖差的区域连接超时设到 15 秒是比较保守的做法低于 10 秒容易在弱网下频繁报SocketTimeoutException。.retryOnConnectionFailure(true)只能处理连接阶段的重试如果服务器返回 500 或 502需要业务层自己判断是否需要重发。4.1.3 定位模块与地图标注定位模块建议集成高德定位 SDK在Application的onCreate里初始化然后在需要定位的页面注册监听器AMapLocationClient locationClient new AMapLocationClient(context); AMapLocationClientOption option new AMapLocationClientOption(); option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); option.setInterval(10000); // 10秒定位一次 option.setOnceLocation(false); // 持续定位巡查员移动时更新 option.setNeedAddress(true); // 返回地址描述 locationClient.setLocationOption(option); locationClient.setLocationListener(aMapLocation - { if (aMapLocation.getErrorCode() 0) { double lat aMapLocation.getLatitude(); double lng aMapLocation.getLongitude(); // 更新地图上的用户位置或对附近水利设施按距离排序 } else { Log.e(Location, 定位失败: aMapLocation.getErrorInfo()); } });定位成功后地图标记点的添加逻辑一般是把StationModel列表遍历为每个设施添加一个 Marker并把站点级别省控、市控、县控映射到不同图标。地图加载完成后根据定位结果调用aMap.moveCamera()把可视区域平移到用户位置附近缩放级别设置在 12 到 15 之间既能看清设施分布又不至于密集重叠。还有一个容易踩的坑定位权限申请失败后高德 SDK 会返回错误码 12缺少定位权限这时候地图页要做出降级处理——不定位但可以手动选择城市避免整个页面白屏。另外定位间隔不建议低于 5 秒否则会快速消耗电量巡查场景 10 秒一次已经够用。5. 告警链路的最后一公里水位曲线与通知提醒的实现水利 App 最核心的价值不在查询而在告警——水位超过警戒线必须第一时间推给值班人员。这个模块由三部分组成图表展示让值班人员直观判断趋势通知栏告知事件AlarmManager 兜底定时检查漏报。三个环节串起来才是完整的告警闭环。5.1.1 用 MPAndroidChart 绘制水位趋势水位数据单独看一条没有意义必须放在时间序列里看趋势。MPAndroidChart 是行业项目中最常用的图表库折线图的接入方式如下LineChart lineChart findViewById(R.id.chart); ListEntry entries new ArrayList(); for (int i 0; i records.size(); i) { WaterLevelRecord record records.get(i); entries.add(new Entry(record.getTime(), (float) record.getLevel())); } LineDataSet dataSet new LineDataSet(entries, 水位(m)); dataSet.setColor(Color.parseColor(#2196F3)); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER); // 曲线平滑更符合水位变化特征 // 添加警戒线根据站点阈值动态设置 LimitLine warningLine new LimitLine((float) warningLevel, 警戒水位); warningLine.setLineColor(Color.RED); warningLine.enableDashedLine(10f, 10f, 0f); LineData lineData new LineData(dataSet); lineChart.setData(lineData); lineChart.getXAxis().setValueFormatter((value, axis) - DateUtils.formatTime((long) value)); lineChart.invalidate();X 轴的ValueFormatter用来把时间戳转成可读的日期格式避免图底显示一串数字。入口点在于LimitLine的阈值不是写死的而是从站点的配置信息中读取否则更换站点后警戒线会用旧值。图表的刷新频率建议控制在 5 秒以上避免频繁invalidate()引起掉帧。5.1.2 告警通知的兼容性处理告警通知在 Android 8.0 之后必须走通知渠道否则通知不显示。完整的告警链路是这样的// 适配 Android 8.0 的通知渠道 NotificationManager nm (NotificationManager) getSystemService(NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( water_alarm, 水位告警, NotificationManager.IMPORTANCE_HIGH); nm.createNotificationChannel(channel); } // 构建并发送通知 Intent intent new Intent(this, DataDetailActivity.class); intent.putExtra(stationCode, stationCode); PendingIntent pi PendingIntent.getActivity( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); Notification notification new NotificationCompat.Builder(this, water_alarm) .setSmallIcon(R.drawable.ic_warning) .setContentTitle(水位超警戒) .setContentText(站点 stationName 当前水位 level m超警戒线 (level - warningLevel) m) .setContentIntent(pi) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) .build(); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) PackageManager.PERMISSION_GRANTED) { nm.notify(stationCode.hashCode(), notification); }报警触发的频率控制很关键处理不当会造成通知轰炸。常见做法是加一个去重策略同一个站点 30 分钟内只发一次通知如果有新数据则更新通知内容而不是新建通知用nm.notify()的第二个参数——同一个通知 ID 会覆盖旧通知。| 检查项 | 参数建议 | 说明 | |--------|---------|------| | 通知渠道 ID | water_alarm | 与创建渠道时保持一致 | | PendingIntent 标志 | FLAG_UPDATE_CURRENT FLAG_IMMUTABLE | 防止用户点击后无法跳转 | | 通知去重窗口 | 30 分钟 | 避免重复轰炸 | | 告警声音 | 默认通知音 震动 | 节省流量场景下不用自定义声音 |5.1.3 AlarmManager 兜底与页面刷新除了服务器下推通知本地还要有一层定时检查的兜底逻辑。用AlarmManager设置定期任务每隔 30 分钟检查一次本地数据库里是否有超过警戒线但未通知的记录有则补发通知。Android 12 及以上需要注意setExactAndAllowWhileIdle()会被限制可以退而求其次用setWindow()容忍几分钟的延迟对水位告警来说是可以接受的。列表页和图表页的数据刷新可以借助一个简单的事件总线机制通知发送时同时把站点信息写入一个静态变量页面onResume时检测这个变量有变化就重新拉取数据并刷新 UI。这样不需要引入 RxBus 或 EventBus 这些额外依赖代码量控制在 20 行以内。6. 发布前的收尾技巧包名替换、APK 瘦身与签名配置源码项目拿到后如果要二次开发变成自己的应用包名替换是第一件要做的事。Android 工程里包名出现在三处build.gradle的applicationId、AndroidManifest.xml的package属性、以及 Java 目录下的物理路径。三处必须同步修改否则安装后会出现两个应用数据互相干扰的问题。android { defaultConfig { applicationId com.yourcompany.waterapp namespace com.yourcompany.waterapp versionCode 2 versionName 1.1.0 minSdk 23 targetSdk 33 } }修改之后清单文件里的package属性在 AGP 7.0 以上已经废弃但老工程里可能还存在建议一并删掉统一由 Gradle 管理。APK 体积控制方面水利 App 的痛点通常是高德地图 SDK 和 MPAndroidChart 带来的体积膨胀。常见的瘦身手段是启用 resource shrinking 和 so 库裁剪buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro ndk { abiFilters armeabi-v7a, arm64-v8a } } }abiFilters过滤掉 x86 架构的 so 库可以显著减小包体积但要注意只有真机是 ARM 架构才能这么设置。模拟器调试时把abiFilters加上 x86发布时再移除。签名配置建议放到单独配置文件里Git 忽略避免密钥泄露。打正式包时用以下方式读取环境变量signingConfigs { release { storeFile file(System.getenv(KEYSTORE_FILE)) storePassword System.getenv(KEYSTORE_PASSWORD) keyAlias System.getenv(KEY_ALIAS) keyPassword System.getenv(KEY_PASSWORD) } }RAR解压后源码的编码格式偶尔会是 GBK用 Android Studio 打开时如果中文注释乱码在File - Settings - Editor - File Encodings里把全局编码改成 UTF-8 即可不改也不影响编译但排查问题时会很不方便。最后验证发布包是否正常可以用adb install装到一台 8.0 版本的低配设备上测试首启速度是否可接受、水位列表滑动是否掉帧、断网状态下打开详情页是否会闪退。水利现场的设备通常不是旗舰机按照低内存设备的标准去验收最稳妥。代码混淆规则文件里不要忘记保留高德 SDK 和 Retrofit 的类否则定位会偶尔失效网络层解析也会莫名其妙报ClassNotFoundException。把这几类规则单独放进proguard-rules.pro与业务混淆配置分离后续维护时一眼就能看出问题出在哪一层。本文还有配套的精品资源点击获取