摘要:本文以一个面向高校的 Android 校园信息管理系统(campusInfoApp)为案例,从架构设计、核心模块实现、关键技术选型三个维度展开分析,详细阐述 MVP 分层、角色路由、主题换肤、网络封装等工程化实践,为同类校园移动应用开发提供参考。
一、项目概述
1.1 项目背景
随着高校信息化建设的推进,学生和管理员对校园信息服务的需求日益多元化——新闻公告浏览、选课操作、请假审批、通知发布等功能分散在不同系统中,用户体验割裂。本项目旨在构建一款一站式校园信息管理 Android 应用,将多场景业务整合至统一入口。
1.2 功能矩阵
| 功能模块 | 学生端 | 管理员端 |
|---|---|---|
| 用户体系 | 注册 / 登录 / 个人信息 / 修改密码 | 同左 |
| 新闻公告 | 浏览列表 + 查看详情 | 发布 / 编辑 / 删除 |
| 通知公告 | 浏览列表 + 查看详情 | 发布 / 管理 |
| 请假管理 | 提交请假申请(含图片附件) | 审批 / 查看列表 |
| 选课系统 | 课程查询 / 选课 / 已选课程查看 | 添加课程 / 管理课程列表 |
| 主题换肤 | 三套皮肤切换(经典/养眼/流行) | 同左 |
1.3 应用截图
图 1 — 学生首页功能入口
首页采用卡片式布局,以图标+文字的形式展示六大功能入口:校园新闻、查询课表、请假、选课、发班级通知、微信订阅号。底部为自定义 RadioButton 导航栏(首页/学生管理/我的),管理员登录后自动显示"学生管理"Tab。
图 2 — 注册页面
注册页面支持头像上传(通过 PictureSelector 选择相机或相册)、用户名/昵称/性别/手机号/专业/密码等字段录入,表单校验在客户端完成。
图 3 — 课表查询页面
课表查询以班级为单位展示(软工1班~4班),点击进入对应班级的课程详情,数据来源于本地 SQLite 预置 + 远程服务端同步。
二、技术栈与依赖
2.1 基础配置
| 配置项 | 值 |
|---|---|
| Application ID | com.mmsx.campush |
| compileSdkVersion | 30 |
| targetSdkVersion | 29 |
| minSdkVersion | 21(覆盖 Android 5.0+) |
| 源文件规模 | 58 个 Java 文件 + 32 个布局 XML |
2.2 核心依赖
// build.gradle 核心依赖implementation'com.squareup.okhttp3:okhttp:4.4.0'// HTTP 客户端implementation'com.google.code.gson:gson:2.8.6'// JSON 序列化implementation'com.github.bumptech.glide:glide:4.11.0'// 图片加载implementation'jp.wasabeef:glide-transformations:3.3.0'// 图片变换(圆角等)implementation'com.github.LuckSiege.PictureSelector:picture_library:v2.6.0'// 图片选择器implementation'skin.support:skin-support:4.0.5'// 换肤框架implementation'androidx.recyclerview:recyclerview:1.1.0'// 列表组件说明:
build.gradle中声明了 Retrofit 和 RxJava 依赖,但实际代码中网络层采用OkHttp 原生封装(Okhttp.java单例工具类),未使用 Retrofit/RxJava 组合。这属于开发过程中的技术决策调整——对于接口数量有限的中型应用,直接使用 OkHttp 足够简洁高效。
三、架构设计
3.1 整体架构
项目采用经典的Activity + Fragment 主从结构,辅以工具层和数据层的分层组织:
┌─────────────────────────────────────────────┐ │ UI 层 │ │ Activity (容器) → Fragment (内容页) │ │ BaseActivity → 具体业务 Activity │ ├─────────────────────────────────────────────┤ │ 适配器层 │ │ NewsAdapter / LeaveAdapter / SelectionAdapter ... ├─────────────────────────────────────────────┤ │ 工具层 │ │ Okhttp (网络) / SPUtils (持久化) │ │ GlideEngine (图片) / JsonUtils (解析) │ ├─────────────────────────────────────────────┤ │ 数据层 │ │ MySqliteOpenHelper (SQLite) │ │ Bean (数据实体) │ └─────────────────────────────────────────────┘3.2 应用启动流程
应用启动时经历以下关键链路:
Application.onCreate() └─ SkinCompatManager 初始化换肤框架 ↓ OpenActivity (欢迎页) ├─ CountDownTimer 5秒倒计时 ├─ 检查 SPUtils.AUTO_LOGIN 是否启用 │ ├─ 是 → Okhttp.byAccount() 验证有效性 → MainActivity │ └─ 否 → LoginActivity ↓ LoginActivity ├─ 验证码生成(随机4位字母数字) ├─ 记住密码 / 自动登录(SPUtils 持久化) └─ Okhttp.byAccount() 远程验证 → 写入用户角色 → MainActivity ↓ MainActivity (主框架) ├─ 读取 SPUtils.USER_TYPE 判断角色 ├─ 学生 → [StudentHomeFragment, UserFragment] └─ 管理员 → [AdminHomeFragment, UserManageFragment, UserFragment]3.3 角色路由机制
这是本项目的核心设计之一。MainActivity通过SharedPreferences中存储的USER_TYPE字段决定加载哪套 Fragment 组合:
// MainActivity.java - 角色判断逻辑userType=ConverterUtils.toInt(SPUtils.get(myActivity,SPUtils.USER_TYPE,""));// 管理员隐藏"学生管理"Tab(学生不显示)rbManege.setVisibility(userType==UserTypeEnum.User.getCode()?View.GONE:View.VISIBLE);// "我的"Tab 根据角色映射到不同索引switchFragment(userType==UserTypeEnum.User.getCode()?1:2);UserTypeEnum定义了三种角色类型:
| 枚举值 | 含义 | 导航 Tab 数量 |
|---|---|---|
User(0) | 普通学生 | 2 个(首页 + 我的) |
Admin(1) | 管理员 | 3 个(首页 + 学生管理 + 我的) |
Dean(2) | 教学院长 | 3 个(同管理员) |
Fragment 采用懒加载 + show/hide模式,避免重复创建:
privatevoidswitchFragment(intfragmentIndex){FragmentTransactiontransaction=getFragmentManager().beginTransaction();if(fragments[fragmentIndex]==null){// 按角色创建对应 Fragment 实例fragments[fragmentIndex]=userType==UserTypeEnum.User.getCode()?newStudentHomeFragment()/newUserFragment():newAdminHomeFragment()/newUserManageFragment()/newUserFragment();transaction.add(R.id.ll_main_content,fragments[fragmentIndex]);}for(inti=0;i<fragments.length;i++){if(fragmentIndex!=i&&fragments[i]!=null)transaction.hide(fragments[i]);// 隐藏其他 Fragment}transaction.show(fragments[fragmentIndex]);transaction.commit();}四、核心模块技术实现
4.1 网络层封装:OkHttp 工具类
项目将 OkHttp 封装为单例工具类Okhttp.java,统一管理所有 HTTP 交互:
publicclassOkhttp{privateStringhttp="http://192.168.3.6:8080/camplus/";// 服务端基础地址privatestaticOkhttpnetokhttp;publicfinalOkHttpClientclient;privateOkhttp(){client=newOkHttpClient.Builder().readTimeout(2000,TimeUnit.MILLISECONDS)// 读超时 2s.connectTimeout(3000,TimeUnit.MILLISECONDS)// 连接超时 3s.build();}// 单例获取publicstaticOkhttppostNetokhttp(){...}}提供五种标准 REST 操作方法:
| 方法 | HTTP 动词 | 用途 |
|---|---|---|
postJson(url, json, callback) | POST | 新增数据(JSON Body) |
putJson(url, json, callback) | PUT | 更新数据 |
getJson(url, callback) | GET | 查询列表 |
deleteJson(url, id, callback) | DELETE | 按 ID 删除 |
uploadFile(path, callback) | POST (Multipart) | 图片文件上传 |
byAccount(account, callback) | GET | 按账号查询用户 |
所有请求均采用异步回调模式(enqueue(callback)),在onResponse回调中通过JsonUtils将响应字符串反序列化为对应的 Bean 对象:
// 典型的调用模式Okhttp.postNetokhttp().byAccount(account,newCallback(){@OverridepublicvoidonResponse(Callcall,Responseresponse)throwsIOException{Stringres=response.body().string();UserResultresult=(UserResult)JsonUtils.fromJson(res,UserResult.class);if(result!=null&&result.getCode()==0&&result.getData()!=null){// UI 操作需切回主线程runOnUiThread(()->{...});}}});4.2 本地数据库:SQLite 持久化
通过MySqliteOpenHelper管理 6 张数据表:
-- 用户表CREATETABLEuser(idINTEGERPRIMARYKEY,account,password,name,sex,phone,room,photo,userType);-- 请假记录表CREATETABLEleave(idINTEGERPRIMARYKEY,userId,note,date,statue,path);-- 新闻表(含已读/未读标记)CREATETABLEnews(idINTEGERPRIMARYKEY,title,content,date,path,new,read);-- 通知表CREATETABLEnotice(idINTEGERPRIMARYKEY,title,content,date);-- 管理员课程表CREATETABLEadminCourse(idINTEGERPRIMARYKEY,number,name,credits,type);-- 学生已选课程表CREATETABLEstudentCourse(idINTEGERPRIMARYKEY,userId,number,name,credits,type);数据库初始化时预置了:
- 两个默认管理员账号(admin/123,教学院长/123)
- 3 条示例校园新闻(黄冈师范学院相关内容)
- 2 条班级通知
- 6 门示例课程(大学语文、大学美术、中国近代史等)
静态变量mUser作为全局当前用户缓存,登录成功后赋值,各 Activity/Fragment 直接读取。
4.3 主题换肤机制
基于Android-SkinSupport开源框架实现运行时换肤,分为三步:
第一步:Application 初始化
// MyApplication.javaSkinCompatManager.withoutActivity(this).addInflater(newSkinAppCompatViewInflater())// 基础控件拦截.addInflater(newSkinMaterialViewInflater())// Material Design 控件拦截.setSkinStatusBarColorEnable(false)// 关闭状态栏换肤.setSkinWindowBackgroundEnable(false)// 关闭 WindowBackground 换肤.loadSkin();第二步:主题选择与保存
// ThemeActivity.java - Spinner 选择主题ArrayList<String>list=newArrayList<>();list.add("经典主题");// index 0list.add("养眼主题");// index 1list.add("流行主题");// index 2spinner.setOnItemSelectedListener(newOnItemSelectedListener(){@OverridepublicvoidonItemSelected(...){selectedType=position;SPUtils.put(activity,SPUtils.STUDENT_THEME,selectedType);// 持久化选择setTheme();// 应用主题}});第三步:资源映射
三套主题通过不同的 drawable 资源实现视觉差异(如按钮背景btn_border_fill/btn_border_fill_1/btn_border_fill_2),换肤框架根据当前选中索引动态加载对应资源。这种方式无需重启 Activity 即可生效。
4.4 图片处理体系
项目构建了一套完整的图片处理流水线:
PictureSelector (选择) ↓ GlideEngine (加载引擎实现 PictureSelector 的 ImageEngine 接口) ├── loadImage() → 普通/网络图片加载 ├── loadFolderImage() → 相册目录缩略图(圆角 R=8dp) ├── loadGridImage() → 网格图片(200x200 centerCrop) ├── loadAsGifImage() → GIF 动图支持 └── 长图智能识别 → MediaUtils.isLongImg() 判断 ├─ 长图 → SubsamplingScaleImageView(支持缩放/双击) └─ 普通图 → ImageView ↓ Okhttp.uploadFile() (上传至服务端) → MultipartBody FORM 表单上传关键设计点:
- 长图自适应:通过判断宽高比自动切换到
SubsamplingScaleImageView,支持双击缩放和手势拖拽,避免 OOM - 圆角缩略图:相册目录页使用
RoundedBitmapDrawableFactory设置 8dp 圆角 - 头像上传:请假申请时可附加图片证据,通过
MultipartBody上传至服务端/file/upload接口
4.5 数据持久化:SPUtils 工具类
基于SharedPreferences封装的通用键值存储工具,管理以下持久化项:
| Key | 类型 | 用途 |
|---|---|---|
is_first | Boolean | 是否首次启动 |
user_type | Integer | 用户角色(0学生/1管理员/2院长) |
user_id | String | 当前登录账号 |
last_name | String | 上次输入的用户名(记住密码) |
last_psw | String | 上次输入的密码 |
auto_login | Boolean | 自动登录开关 |
remember_password | Boolean | 记住密码开关 |
student_theme | Integer | 当前主题索引 |
内部通过反射调用apply()方法实现异步提交,并做了兼容性降级(apply不可用时回退到commit)。
五、UI/UX 设计亮点
5.1 自定义导航栏
底部导航未使用BottomNavigationView,而是采用RadioGroup + RadioButton自定义方案:
- 每个 RadioButton 通过
setCompoundDrawables()设置图标(68x68 dp) - 使用 Selector 资源实现选中/未选中状态切换(颜色+图标变化)
- 根据角色动态控制
rbManege(学生管理)可见性
这种方案的灵活性在于可以完全控制图标大小、间距和样式,适合需要高度定制导航栏的场景。
5.2 欢迎页倒计时
OpenActivity实现 5 秒倒计时自动跳转,同时支持点击"跳过"立即进入:
CountDownTimertimer=newCountDownTimer(5000,1000){publicvoidonTick(longmillisUntilFinished){in.setText(millisUntilFinished/1000+"秒");}publicvoidonFinish(){finishView();// 时间到自动跳转}};跳转前会检查自动登录状态:若之前勾选了自动登录且本地存有 userId,则静默验证并直接进入主页;否则进入登录页。
5.3 双击退出
MainActivity重写onKeyDown,监听返回键实现双击退出(间隔 < 2 秒):
publicvoidexit(){if(System.currentTimeMillis()-time>2000){time=System.currentTimeMillis();Toast.makeText(myActivity,"再点击一次退出应用程序",Toast.LENGTH_LONG).show();}else{finish();}}5.4 登录安全增强
- 图形验证码:每次进入
LoginActivity(onStart)随机生成 4 位字母数字验证码,纯客户端校验 - 密码本地比对:先远程拉取用户信息,再在客户端做密码
equalsIgnoreCase比较 - 记住密码:通过 SPUtils 持久化上次登录凭据,复用 CheckBox 状态
六、项目结构与代码组织
6.1 包结构
com.example.campus/ ├── MyApplication.java # Application 入口(换肤初始化) ├── bean/ # 数据实体 │ ├── User / News / Leave / CourseSelection / StudentCourse │ └── *Result # 统一响应包装(code + data) ├── enums/ # 枚举定义 │ ├── UserTypeEnum # 用户角色 │ ├── ComeTypeEnum # 请假去向类型 │ └── RepairsStatusEnum # 报修状态 ├── util/ # 工具类 │ ├── Okhttp.java # 网络请求封装(单例) │ ├── SPUtils.java # SharedPreferences 封装 │ ├── MySqliteOpenHelper.java # 数据库管理 │ ├── JsonUtils.java # JSON 解析工具 │ ├── ConverterUtils.java # 类型转换工具 │ ├── GlideEngine.java # Glide 图片引擎 │ └── KeyBoardUtil.java # 键盘控制工具 ├── widget/ # 自定义控件 │ └── ActionBar.java # 统一标题栏组件 ├── adapter/ # RecyclerView 适配器 │ ├── NewsAdapter / NoticeAdapter │ ├── LeaveAdapter / CourseSelectionAdapter │ └── UserAdapter └── ui/ ├── activity/ # Activity 页面(32个) │ ├── BaseActivity.java # 基类 │ ├── OpenActivity / LoginActivity / RegisterActivity │ ├── MainActivity # 主框架(Fragment 容器) │ ├── ThemeActivity # 主题设置 │ ├── *News*Activity # 新闻相关(学生浏览 + 管理员管理) │ ├── *Notice*Activity # 通知相关 │ ├── *Leave*Activity # 请假相关 │ ├── *Selection*Activity # 选课相关 │ └── PersonActivity / PasswordActivity └── fragment/ # Fragment 内容页 ├── StudentHomeFragment # 学生首页 ├── AdminHomeFragment # 管理员首页 ├── UserFragment # 个人中心 └── UserManageFragment # 用户管理(管理员)6.2 BaseActivity 基类
所有 Activity 继承自BaseActivity,统一处理公共逻辑(如换肤兼容、ActionBar 绑定等),保证各页面行为一致性。
七、总结与展望
7.1 技术亮点总结
| 维度 | 实践 | 效果 |
|---|---|---|
| 架构 | Activity + Fragment 主从结构 + 角色路由 | 一套代码适配学生/管理员双角色 |
| 网络 | OkHttp 单例封装 + 异步回调 | 统一接口风格,超时可控 |
| 存储 | SQLite 本地持久化 + SharedPreferences 配置存储 | 离线可用 + 用户偏好保留 |
| 换肤 | Android-SkinSupport 运行时换肤 | 无需重启即时生效 |
| 图片 | PictureSelector + Glide + 长图智能识别 | 全链路图片处理能力 |
| 体验 | 欢迎倒计时 / 双击退出 / 记住密码 / 自动登录 | 符合移动端交互习惯 |
7.2 可优化方向
- 网络层升级:可引入 Retrofit + RxJava(依赖已声明)实现更优雅的链式调用和线程切换;考虑添加拦截器统一处理 Token、日志、错误码
- 架构现代化:迁移至 Jetpack Component(ViewModel + LiveData / Navigation Component),替代手动 Fragment 管理和show/hide 模式
- 数据库 ORM:引入 Room 替代原生 SQLiteOpenHelper,获得编译期 SQL 检查、LiveData 观察等能力
- 模块化拆分:按功能域(news / notice / course / leave)拆分为独立模块,降低耦合度
- 图片缓存策略:结合 Glide 磁盘缓存与服务端 CDN,优化图片加载性能
- 安全加固:密码传输加密(HTTPS)、Token 机制替代明文密码比对、服务端验证码替代客户端生成
7.3 适用场景
本项目的技术方案适用于以下类型的中小型 Android 应用开发:
- 企业/校园内部管理工具
- 多角色权限的信息展示平台
- 以列表浏览 + 表单提交为主的应用
- 需要主题定制能力的 C 端产品
本文基于 campusInfoApp 项目源码撰写,截图来源于应用实际运行效果。