摘要:本文以一个面向高校的 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 核心依赖
groovy
// 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 组合:
java
// 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 模式,避免重复创建:
java
private void switchFragment(int fragmentIndex) {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
if (fragments[fragmentIndex] == null) {
// 按角色创建对应 Fragment 实例
fragments[fragmentIndex] = userType == UserTypeEnum.User.getCode()
? new StudentHomeFragment() / new UserFragment()
: new AdminHomeFragment() / new UserManageFragment() / new UserFragment();
transaction.add(R.id.ll_main_content, fragments[fragmentIndex]);
}
for (int i = 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 交互:
java
public class Okhttp {
private String http = "http://192.168.3.6:8080/camplus/"; // 服务端基础地址
private static Okhttp netokhttp;
public final OkHttpClient client;
private Okhttp() {
client = new OkHttpClient.Builder()
.readTimeout(2000, TimeUnit.MILLISECONDS) // 读超时 2s
.connectTimeout(3000, TimeUnit.MILLISECONDS) // 连接超时 3s
.build();
}
// 单例获取
public static Okhttp postNetokhttp() { ... }
}
提供五种标准 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 对象:
java
// 典型的调用模式
Okhttp.postNetokhttp().byAccount(account, new Callback() {
@Override public void onResponse(Call call, Response response) throws IOException {
String res = response.body().string();
UserResult result = (UserResult) JsonUtils.fromJson(res, UserResult.class);
if (result != null && result.getCode() == 0 && result.getData() != null) {
// UI 操作需切回主线程
runOnUiThread(() -> { ... });
}
}
});
4.2 本地数据库:SQLite 持久化
通过 MySqliteOpenHelper 管理 6 张数据表:
sql
-- 用户表
CREATE TABLE user(id INTEGER PRIMARY KEY, account, password, name,
sex, phone, room, photo, userType);
-- 请假记录表
CREATE TABLE leave(id INTEGER PRIMARY KEY, userId, note, date, statue, path);
-- 新闻表(含已读/未读标记)
CREATE TABLE news(id INTEGER PRIMARY KEY, title, content, date, path, new, read);
-- 通知表
CREATE TABLE notice(id INTEGER PRIMARY KEY, title, content, date);
-- 管理员课程表
CREATE TABLE adminCourse(id INTEGER PRIMARY KEY, number, name, credits, type);
-- 学生已选课程表
CREATE TABLE studentCourse(id INTEGER PRIMARY KEY, userId, number, name, credits, type);
数据库初始化时预置了:
- 两个默认管理员账号(admin/123,教学院长/123)
- 3 条示例校园新闻(黄冈师范学院相关内容)
- 2 条班级通知
- 6 门示例课程(大学语文、大学美术、中国近代史等)
静态变量 mUser 作为全局当前用户缓存,登录成功后赋值,各 Activity/Fragment 直接读取。
4.3 主题换肤机制
基于 Android-SkinSupport 开源框架实现运行时换肤,分为三步:
第一步:Application 初始化
java
// MyApplication.java
SkinCompatManager.withoutActivity(this)
.addInflater(new SkinAppCompatViewInflater()) // 基础控件拦截
.addInflater(new SkinMaterialViewInflater()) // Material Design 控件拦截
.setSkinStatusBarColorEnable(false) // 关闭状态栏换肤
.setSkinWindowBackgroundEnable(false) // 关闭 WindowBackground 换肤
.loadSkin();
第二步:主题选择与保存
java
// ThemeActivity.java - Spinner 选择主题
ArrayList<String> list = new ArrayList<>();
list.add("经典主题"); // index 0
list.add("养眼主题"); // index 1
list.add("流行主题"); // index 2
spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(...) {
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 秒倒计时自动跳转,同时支持点击"跳过"立即进入:
java
CountDownTimer timer = new CountDownTimer(5000, 1000) {
public void onTick(long millisUntilFinished) {
in.setText(millisUntilFinished / 1000 + "秒");
}
public void onFinish() {
finishView(); // 时间到自动跳转
}
};
跳转前会检查自动登录状态:若之前勾选了自动登录且本地存有 userId,则静默验证并直接进入主页;否则进入登录页。
5.3 双击退出
MainActivity 重写 onKeyDown,监听返回键实现双击退出(间隔 < 2 秒):
java
public void exit() {
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 项目源码撰写,截图来源于应用实际运行效果。