一、项目背景与目标
随着"让文物活起来"和"数字博物馆"理念的普及,越来越多的博物馆开始借助移动应用向公众提供藏品展示、新闻资讯、参观预约等数字化服务。本文介绍的 museumApp 是一款以"博物馆文化展示与预约"为核心的 Android 原生应用,其目标是:
- 集中展示:在一个 App 内集成博物馆简介、开放时间、新闻资讯、展览活动、藏品典藏等内容。
- 在线预约:让观众可以填写预约日期、人数、时段,提交到本地数据库,并在"我的"中查看自己的预约记录。
- 个人中心:支持账号注册、登录、个人资料维护(含头像更换),并区分普通用户与管理员角色。
- 教学价值:作为 Android 课程设计的实践项目,对"启动页 → 登录/注册 → 底部导航 + Fragment → SQLite 持久化 → 详情页"这一典型 App 流程进行完整封装。
特别说明:项目虽然引入了
volley.jar网络库,但实际业务中并未发起任何真实网络请求 ------所有藏品、新闻、活动、用户、预约等数据均通过本地 SQLite 或硬编码资源(R.drawable)提供,属于"离线优先"的教学型 Demo,可以保证课堂演示时不依赖外网。
二、整体架构
App 采用经典的 MVC + 单 Activity 多 Fragment 结构:
UI 层 :Activity / Fragment / Adapter (View)
业务层 :DBUser / DBReservation(基于 SQLiteOpenHelper 的 DAO)
数据层 :entity(User / Reservation / News / Blank) + SPUtils
资源层 :drawable / layout / values
- 一个
MainActivity承载底部BottomNavigationView,通过FragmentTransaction.replace()切换"首页 / 典藏 / 我的"三个 Fragment。 - 业务页(藏品、活动、新闻、预约、详情等)各自独立
Activity,跳转通过Intent完成。 - 数据持久化由
DBHelper(单例的SQLiteOpenHelper)+ 两个 DAO(DBUser、DBReservation)承担,登录态、自动登录、记住密码等小数据由SPUtils(SharedPreferences 封装)存储。

三、技术栈与依赖
build.gradle 中声明的依赖:
| 技术 | 用途 |
|---|---|
| AppCompat / Material / ConstraintLayout | 基础 UI 框架与主题 |
| RecyclerView | 列表控件(项目中以 ListView 为主,RecyclerView 备用) |
ViewPager + 自定义 AdPagerAdapter |
首页顶部 3 秒自动轮播 + 触摸暂停/继续 |
| Glide + glide-transformations | 头像与详情大图的加载与变换 |
PictureSelector (picture_library v2.6.0) |
"我的资料"中更换头像(相册/拍照) |
volley.jar(libs 目录) |
已引入但未使用 |
| SQLite(系统内置) | 用户表、预约表 |
// build.gradle 节选
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.6.0'
implementation 'jp.wasabeef:glide-transformations:3.3.0'
implementation files('libs\\volley.jar')
四、目录结构与 Java 文件清单
graduate/museumApp/
├── build.gradle
├── libs/volley.jar
└── src/main/
├── AndroidManifest.xml
├── res/ (drawable / layout / values 等)
└── java/com/example/museum/
├── activity/ 13 个 Activity
├── fragment/ 3 个 Fragment
├── adapter/ 5 个 Adapter
├── database/ 3 个数据库类
├── entity/ 4 个实体
└── utils/ 3 个工具类
各包关键类说明:
| 包 | 关键类 | 职责 |
|---|---|---|
activity |
OpenActivity |
启动页:5 秒倒计时 + 数据库初始化 + 跳转登录/主页 |
LoginActivity |
登录:账号、密码、记住密码、自动登录 | |
RegisterActivity |
注册:昵称/性别/手机号/职业/兴趣/密码/头像 | |
MainActivity |
主页:底部导航 + Fragment 容器 | |
ActivitiesActivity |
教育活动列表 | |
NewsActivity |
新闻列表 | |
CollectionActivity |
典藏列表(按分类) | |
ReservationActivity |
网上预约表单 | |
MyInfoActivity |
我的个人资料(编辑 + 头像) | |
MyRecordActivity |
我的预约记录 | |
DetailActivity |
通用详情(博物馆简介/开放时间) | |
CollectionDetailActivity |
藏品详情 | |
NewsDetailActivity |
新闻详情 | |
fragment |
BlankFragment_Main |
首页:轮播 + 功能入口 |
BlankFragment_Collection |
典藏分类 Fragment | |
BlankFragment_me |
"我的" Fragment | |
adapter |
AdPagerAdapter |
首页轮播 ViewPager 适配器 |
BlankAdapter |
通用卡片 ArrayAdapter(首页功能入口) | |
NewsAdapter |
新闻列表 | |
CollectionAdapter |
典藏分类 | |
RecordAdapter |
预约记录 | |
database |
DBHelper |
SQLiteOpenHelper 单例,建表 + 初始化管理员 |
DBUser |
用户增删改查、登录校验 | |
DBReservation |
预约新增 / 查询(按用户名筛选) | |
entity |
User / Reservation / News / Blank |
4 个 POJO |
utils |
SPUtils |
SharedPreferences 封装(键值常量化) |
GlideEngine |
实现 PictureSelector 的 ImageEngine |
|
UserTypeEnum |
用户类型枚举(普通用户/管理员/院长) |
五、数据库设计
数据库名 museum.db,版本 1,存在应用私有数据目录下。
5.1 用户表 users
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| name | text | PRIMARY KEY | 用户名(账号) |
| psw | text | NOT NULL | 密码 |
| occupation | text | NOT NULL | 职业 |
| sex | text | NOT NULL | 性别 |
| phone | text | NOT NULL | 手机号 |
| interest | text | NOT NULL | 兴趣 |
| photo | text | NOT NULL | 头像路径 |
| userType | text | NOT NULL | 用户类型(普通/管理员/院长) |
5.2 预约表 reservation
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | integer | PRIMARY KEY AUTOINCREMENT | 预约 ID |
| name | text | NOT NULL | 预约人姓名 |
| time | text | NOT NULL | 预约日期 |
| number | text | NOT NULL | 预约人数 |
| ampm | text | NOT NULL | 上午/下午 |
首次启动时,
DBHelper.init()会判断管理员账号admin / 123是否存在,若不存在则写入;并把当前登录用户缓存到DBUser.user静态字段中,便于全局访问。
核心建表代码:
1:50:src/main/java/com/example/museum/database/DBHelper.java
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String sql="create table users(name text primary key,psw text not null," +
"occupation text not null,sex text not null,phone text not null," +
"interest text not null,photo text not null,userType text not null)";
sqLiteDatabase.execSQL(sql);
sql = "CREATE TABLE reservation (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name TEXT NOT NULL, time TEXT NOT NULL, " +
"number TEXT NOT NULL, ampm TEXT NOT NULL)";
sqLiteDatabase.execSQL(sql);
}
六、关键模块实现
6.1 启动页与登录态判断
OpenActivity 是 LAUNCHER 入口,使用 CountDownTimer 实现 5 秒倒计时与"跳过"按钮:
22:65:src/main/java/com/example/museum/activity/OpenActivity.java
public class OpenActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_open);
DBHelper.GetInstance(this.getApplication()).init();
in = findViewById(R.id.in);
// 倒计时 5 秒
CountDownTimer timer = new CountDownTimer(5000, 1000) {
public void onTick(long millisUntilFinished) {
in.setText(millisUntilFinished / 1000 + "秒");
}
public void onFinish() { finishView(); }
};
timer.start();
}
private void finishView(){
Intent intent = new Intent();
// 根据 SPUtils 中的登录态决定去向
if (!TextUtils.isEmpty(SPUtils.getName(this))
&& (Boolean) SPUtils.get(this, SPUtils.AUTO_LOGIN, false)) {
intent.setClass(OpenActivity.this, MainActivity.class);
} else {
intent.setClass(OpenActivity.this, LoginActivity.class);
}
startActivity(intent);
finish();
}
}

登录页支持"自动登录"和"记住密码"两个选项,二者状态均由 SPUtils 持久化。校验逻辑直接走 DBUser.check(account, password),从本地 users 表中匹配。
6.2 主页与底部导航
MainActivity 是 App 唯一的"框架页",由 BottomNavigationView + FrameLayout 组合,通过 FragmentTransaction.replace() 切换 3 个 Fragment:
19:61:src/main/java/com/example/museum/activity/MainActivity.java
public class MainActivity extends AppCompatActivity {
FrameLayout main;
BottomNavigationView navigation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
main = findViewById(R.id.main);
navigation = findViewById(R.id.navigation);
navigation.setOnItemSelectedListener(item -> {
switch (item.getItemId()) {
case R.id.home:
replace(new BlankFragment_Main().newInstance()); return true;
case R.id.cart:
replace(new BlankFragment_Collection().newInstance()); return true;
case R.id.me:
replace(new BlankFragment_me().newInstance()); return true;
}
return false;
});
navigation.setSelectedItemId(R.id.home); // 默认首页
}
}
底部三 Tab 分别为:首页 / 典藏 / 我的。
6.3 首页:自动轮播 + 功能入口
BlankFragment_Main 把"广告位"和"功能入口列表"组合在一起:
- 顶部
ViewPager加载 4 张本地drawable(head1~head4),用Handler.postDelayed实现 3 秒自动切换; - 触摸时通过
OnTouchListener暂停轮播,抬手 / 取消时延 1 秒后继续; - 下方
ListView加载 5 个Blank实体(博物馆简介、开放时间、教育活动、网上预约、新闻咨询),点击不同位置分别跳到DetailActivity/ActivitiesActivity/ReservationActivity/NewsActivity。
31:93:src/main/java/com/example/museum/fragment/BlankFragment_Main.java
private ViewPager mViewPager;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
int currentItem = mViewPager.getCurrentItem();
int nextItem = (currentItem + 1) % mViewPager.getAdapter().getCount();
mViewPager.setCurrentItem(nextItem);
mHandler.postDelayed(this, 3000);
}
};
int[] adImages = {R.drawable.head4, R.drawable.head2, R.drawable.head3, R.drawable.head1};
AdPagerAdapter adapter = new AdPagerAdapter(getActivity(), adImages);
mViewPager.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mHandler.removeCallbacks(mRunnable); break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mHandler.postDelayed(mRunnable, 1000); break;
}
return false;
});
6.4 典藏分类与详情
BlankFragment_Collection 进入"典藏"Tab 时展示 5 个分类入口:镇馆之宝 / 精品鉴赏 / 三维藏品 / 藏品公开 / 藏品征集 。每条点击进入 CollectionActivity,再以 CollectionAdapter 渲染同分类下的具体藏品(缩略图 + 名称),点入 CollectionDetailActivity 查看大图与文字详情。


6.5 新闻与活动
NewsActivity 与 ActivitiesActivity 使用 ArrayAdapter<News> 渲染图文列表,点击进入 NewsDetailActivity / DetailActivity 渲染正文。这部分数据与首页一样来自代码内硬编码,未做联网。


6.6 网上预约
ReservationActivity 是 App 业务核心:
- 通过
DatePickerDialog选择预约日期(自动阻止选择过去日期),并格式化为yyyy年MM月dd日; - 使用
RadioGroup选择上午 / 下午; - 表单上展示"当前时段剩余预约量"(
DBReservation.allCount - DBReservation.count,其中allCount = 999); - 提交时校验:人数不能为空、本次人数 + 历史累计不能超过 999;校验通过后调用
DBReservation.add(...)写入数据库。
76:114:src/main/java/com/example/museum/activity/ReservationActivity.java
protected void showDatePickerDialog() {
new DatePickerDialog(this, (view, yearSet, monthSet, daySet) -> {
if (yearSet < year) {
Toast.makeText(this, "不能选择过去日期", Toast.LENGTH_SHORT).show();
return;
}
Calendar c = Calendar.getInstance();
c.set(yearSet, monthSet, daySet);
time = new SimpleDateFormat("yyyy年MM月dd日", Locale.CHINA).format(c.getTime());
textView.setText(time);
}, year, month - 1, day).show();
}
private void save() {
String number = edit_number.getText().toString();
String am = rgSex.getCheckedRadioButtonId() == R.id.rb_am ? "上午" : "下午";
if ("".equals(number)) {
Toast.makeText(this, "人数不能为空", Toast.LENGTH_LONG).show();
return;
}
if (DBReservation.count + Integer.valueOf(number) > 999) {
Toast.makeText(this, "输入人数超出剩余数量", Toast.LENGTH_LONG).show();
return;
}
DBReservation db = new DBReservation(getBaseContext());
db.add(new Reservation(DBUser.user.getName(), time, number, am));
Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();
finish();
}
预约完成后,可在"我的 → 预约记录"中按用户名筛选展示历史预约。

6.7 个人中心
"我的"Fragment 通过 DBUser.user 静态字段拿到当前登录用户,渲染头像 + 昵称 + 三个入口:
- 个人信息 →
MyInfoActivity:支持修改昵称/性别/手机号/职业/兴趣,并通过PictureSelector+GlideEngine拍照/选图更换头像,写入users.photo。 - 预约记录 →
MyRecordActivity:调用DBReservation.getAll(userName)拿到当前用户所有预约。 - 退出登录 :清空
SPUtils后回到LoginActivity。


七、关键流程
7.1 启动与登录流程
OpenActivity(启动页,5s 倒计时)
│
├── DBHelper.init():建库 + 写入管理员 admin/123
│
├── SPUtils.AUTO_LOGIN == true → MainActivity
│
└── 否则 → LoginActivity
├── 点击"注册" → RegisterActivity → LoginActivity
└── 登录成功 → MainActivity
7.2 预约流程
MainActivity → 首页"网上预约"卡片 → ReservationActivity
→ 选择日期 + 上午/下午 + 人数
→ 校验通过 → DBReservation.add()
→ 跳回 MainActivity,"我的 → 预约记录" → MyRecordActivity 拉取记录
八、运行效果
下面整理项目 pic/ 目录下的实际运行截图,按典型用户路径排序:
| 阶段 | 截图 | 说明 |
|---|---|---|
| 启动后首页 | pic/Screenshot_20240630_085930.png |
顶部 ViewPager 轮播 + 5 个功能入口 + 底部三 Tab |
| 进入功能 | pic/Screenshot_20240630_090022.png |
点击"博物馆简介"进入通用详情页 |
| 活动列表 | pic/Screenshot_20240630_090157.png |
教育活动图文列表 |
| 活动详情 | pic/Screenshot_20240630_090214.png |
大图 + 活动正文 |
| 典藏分类 | pic/Screenshot_20240630_090606.png |
5 个圆角按钮 |
| 典藏列表 | pic/Screenshot_20240630_090625.png |
镇馆之宝 |
| 典藏列表 | pic/Screenshot_20240630_090642.png |
精品鉴赏 |
| 典藏列表 | pic/Screenshot_20240630_090707.png |
三维藏品 |
| 典藏列表 | pic/Screenshot_20240630_090728.png |
藏品公开 |
| 典藏列表 | pic/Screenshot_20240630_090750.png |
藏品征集 |
| 典藏详情 | pic/Screenshot_20240630_090803.png |
大图 + 详情正文 |
| 新闻列表 | pic/Screenshot_20240630_090254.png |
招聘、活动、新闻混合 |
| 新闻详情 | pic/Screenshot_20240630_090312.png |
时间 + 长文 |
| 登录 | pic/Screenshot_20240630_124422.png |
记住密码 / 自动登录 |
| 注册 | pic/Screenshot_20240630_124439.png |
7 个字段 + 头像 |
| 我的 | pic/Screenshot_20240630_124356.png |
头像 + 三个入口 |
| 个人资料 | pic/Screenshot_20240630_124509.png |
头像 + 表单 + 保存 |
| 预约记录 | pic/Screenshot_20240630_124550.png |
按用户筛选后的记录 |
九、设计亮点与经验总结
- 单 Activity + Fragment 的轻量"主页"模式 :业务页各自独立
Activity,主页仅保留"首页 / 典藏 / 我的"三个 Fragment,避免传统"多 Activity + Activity 间互相回传"的样板代码。 - DAO + 单例
DBHelper:DBHelper.GetInstance(context)全程复用SQLiteDatabase,避免频繁open/close,同时把onCreate写表与init()初始化种子数据解耦。 - 首页轮播的"触摸可控 + 自动续播" :通过
OnTouchListener监听ACTION_DOWN/UP/CANCEL,再配合Handler.removeCallbacks/postDelayed,既能暂停也能延时恢复,体验比第三方 Banner 控件更可控。 - 预约余量的"软上限" :
allCount = 999、count跟随getAll()累加,是一种"教学型防超"------既演示了 DAO 的统计查询,又避免了对真实库存逻辑的过度设计。 - 登录态分离 :把"是否记住密码 / 是否自动登录"两个状态拆为两个
CheckBox+ 两个SPUtils键值,是常见需求;再叠加CountDownTimer启动页倒计时,完整覆盖真实 App 的"冷启动 → 拉起主页"的体验链路。 - 可扩展的"通用详情页" :
DetailActivity接收Blank实体作为Intent附加数据,由Blank携带图标 + 标题 + 描述,使"博物馆简介 / 开放时间"等纯展示型页面共用一个 Activity,降低模板代码。
十、可改进点
虽然本项目足以支撑教学演示和功能跑通,但仍有不少可演进空间:
- 真实网络层 :把硬编码列表数据抽到接口(
/api/collections、/api/news),用 Retrofit/OkHttp 替换当前的volley.jar占位,并在ListView上加SwipeRefreshLayout。 - Room 替代裸 SQLite :用 Room 把
User/Reservation注解为 Entity,写编译期校验的 DAO 接口,配合LiveData/Flow让预约记录自动刷新。 - MVVM 化 :引入
ViewModel + LiveData,把"首页 Fragment + 业务 Activity"改造成数据驱动,配合ViewBinding替换findViewById。 - 图片选择统一化 :用
ActivityResultContracts替代旧版onActivityResult,并结合 Android 13 的READ_MEDIA_IMAGES权限模型重构头像选择。 - 暗色模式与国际化 :当前主题写死绿色(
#1AB394),可结合values-night与Material You提升一致性。 - 统一包名遗留 :
AndroidManifest.xml的package="com.example.smallshop"与 Java 包com.example.museum不一致,可通过android.namespace+applicationIdSuffix整理,避免R类的 import 必须写成com.example.smallshop.R。
十一、小结
museumApp 用最经典的"启动页 + 登录 + 主页三 Tab + 详情/表单页"结构,完整跑通了一个 Android 入门到进阶的"博物馆文化展示与预约"业务。它不追求花哨的视觉效果,但把 Activity/Fragment 导航、SQLite 持久化、SharedPreferences 登录态、ViewPager 轮播、PictureSelector 选图、Glide 加载 等基础但重要的知识点全部串了起来,非常适合作为 Android 课程设计或入门实战项目的参考实现。