Android旅游App毕设工程闭环实践指南
2026/9/17 2:30:08 网站建设 项目流程

简介:这是一份面向计算机专业本科生的Android毕业设计实战项目资源,聚焦旅游类移动应用开发,帮助学习者掌握从用户系统、景点推荐到订单管理的完整App开发流程。资源包含1562个文件,涵盖213个Java源码(核心业务逻辑)、275个XML布局与配置文件(UI结构)、216个PNG/JPG图标与界面素材(视觉资源)、486个Flat资源文件(编译中间产物),以及关键的tourismdb.sql数据库脚本和TourismClient客户端工程,整体压缩包大小为35.07MB。已有128人下载学习,适合Android初学者通过可运行项目理解注册登录、SQLite数据交互、搜索筛选、费用计算及个人中心等典型模块实现。资源还附带软件下载与视频配置流程说明.docx文档,提供环境搭建指引,并包含gradlew.bat、jar、so等构建与依赖文件,便于直接导入Android Studio调试运行,是少有的结构完整、注释清晰、开箱即用的课程设计级参考工程。

1. 这不是“做个界面+调API”的毕业设计:Android旅游指南App的工程闭环该怎么走?

很多计算机专业学生拿到“基于Android的旅游指南App”这个毕设题目时,第一反应是:用Android Studio拖几个Button、TextView,接个高德或百度地图SDK,再硬编码几条景点数据——交差了。但答辩现场常被问住:离线缓存怎么设计?多语言切换如何不重启Activity?用户收藏的景点数据在App卸载后还能恢复吗?后台定位服务在Android 12+为何频繁被杀?这些不是加分项,而是验证你是否真正理解Android应用生命周期、存储模型与系统约束的必答题。本篇聚焦真实开发中可编译、可调试、可演示、可解释的最小可行闭环:从Android Studio环境配置开始,到实现「城市列表→景点详情→路线规划→本地收藏」四步链路,并覆盖Gradle构建、权限适配、RecyclerView性能优化、Room数据库持久化等毕业设计高频考察点。适合零基础起步但拒绝“假跑通”的本科生,也给已有经验者提供参数级避坑清单。

2. 用Android Studio 2023.2.1(Flamingo)搭建可编译的旅游App基础工程

2.1 创建项目时必须选对模板与最低SDK版本

新建项目时,选择Empty Activity模板(非Basic Activity),原因在于:Basic Activity默认包含AppBarLayout和FloatingActionButton,对旅游类App冗余且易引发Material Design组件兼容性问题;而Empty Activity提供最干净的Activity结构,便于后续自主集成Navigation Component和ViewBinding。Minimum SDK务必设为API 21 (Android 5.0)——这是当前国内主流机型(含大量二手华为、小米设备)的底线,低于此版本将无法使用ConstraintLayout 2.0+的链式布局和Room数据库的@Database注解。若设为API 16(Android 4.1),虽能覆盖更广机型,但需手动处理Fragment兼容包、放弃ViewBinding、改用SQLiteOpenHelper手写CRUD,极大增加代码量与出错概率,不符合毕业设计“可控复杂度”原则。

提示:Android Studio安装时若未勾选Android SDK Platform-Tools和Android SDK Build-Tools,创建项目会失败。可在File → Settings → Appearance & Behavior → System Settings → Android SDK中补全,重点确认已安装Build-Tools 34.0.0及以上版本(Gradle 8.0+强制要求)。

2.2 配置build.gradle(:app)以支持现代Android开发范式

在模块级build.gradle中,需显式启用ViewBinding并声明Room依赖,这是毕业设计中体现架构能力的关键配置:

android { compileSdk 34 defaultConfig { applicationId "com.example.travelguide" minSdk 21 targetSdk 34 // 必须与compileSdk一致,否则Android 12+后台定位权限失效 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildFeatures { viewBinding true // 启用ViewBinding,替代findViewById } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // Room数据库核心依赖(持久化收藏景点) implementation 'androidx.room:room-runtime:2.6.1' implementation 'androidx.room:room-ktx:2.6.1' // Kotlin扩展支持,Java项目可省略 annotationProcessor 'androidx.room:room-compiler:2.6.1' // Java项目必须添加此行 // 地图SDK(以高德为例,需在AndroidManifest.xml中配置key) implementation 'com.amap.api:map2d:6.0.0' // 网络请求(Retrofit + OkHttp,比原生HttpURLConnection更易演示) 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' }

关键参数说明:

  • targetSdk 34:Android 12(API 31)起强制执行后台位置权限,若设为30以下,即使申请了ACCESS_BACKGROUND_LOCATION,系统也会静默拒绝;
  • viewBinding true:生成ActivityMainBinding类,避免findViewById的类型安全隐患,答辩时可展示“无findViewById代码”;
  • room-compiler:Java项目必须用annotationProcessor而非kapt,否则Room实体类编译时报错Cannot resolve symbol Database

2.3 解决Android Studio中文显示与常见构建失败

若Android Studio界面为英文,可通过Help → Edit Custom VM Options… 添加-Dfile.encoding=UTF-8并重启生效(非汉化补丁,避免版权风险)。构建失败常见于tag number over 30 is not supported错误,本质是Gradle插件版本与AGP(Android Gradle Plugin)不匹配。解决方案:打开项目级build.gradle,将com.android.tools.build:gradle升级至8.2.2,对应Gradle版本为8.2(在gradle/wrapper/gradle-wrapper.properties中修改distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip)。

3. 实现旅游App核心功能链:从城市列表到本地收藏的四步闭环

3.1 城市列表页:用RecyclerView+DiffUtil实现高效滚动与动态更新

城市列表采用垂直LinearLayoutManager的RecyclerView,数据源为List<City>,其中City为简单POJO:

public class City { private int id; private String name; private String description; // 城市简介 private int imageResId; // 背景图资源ID }

Adapter需继承ListAdapter并重写DiffUtil.Callback,避免notifyDataSetChanged()导致的全局刷新:

public class CityAdapter extends ListAdapter<City, CityAdapter.ViewHolder> { public CityAdapter() { super(DIFF_CALLBACK); } private static final DiffUtil.Callback DIFF_CALLBACK = new DiffUtil.Callback() { @Override public int getOldListSize() { return 0; } // 实际应传入旧列表size @Override public int getNewListSize() { return 0; } // 实际应传入新列表size @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return getOldList().get(oldItemPosition).getId() == getNewList().get(newItemPosition).getId(); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return getOldList().get(oldItemPosition).equals( getNewList().get(newItemPosition)); } }; @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { ItemCityBinding binding = ItemCityBinding.inflate( LayoutInflater.from(parent.getContext()), parent, false); return new ViewHolder(binding); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.bind(getItem(position)); } static class ViewHolder extends RecyclerView.ViewHolder { private final ItemCityBinding binding; ViewHolder(ItemCityBinding binding) { super(binding.getRoot()); this.binding = binding; } void bind(City city) { binding.tvCityName.setText(city.getName()); binding.tvDescription.setText(city.getDescription()); binding.ivCityImage.setImageResource(city.getImageResId()); } } }

逻辑说明:ListAdapter自动处理列表变更,DiffUtil通过areItemsTheSame(ID比对)和areContentsTheSame(内容比对)计算最小更新集。答辩时可演示:向列表插入10个城市,仅刷新新增项,其余项ViewHolder复用,无闪烁。

3.2 景点详情页:用ViewPager2+FragmentStateAdapter承载图文与路线Tab

景点详情页需同时展示文字介绍、图片画廊、交通路线三类信息,采用ViewPager2配合FragmentStateAdapter实现滑动切换:

// 在DetailActivity中初始化 ViewPager2 viewPager = findViewById(R.id.viewPager); TabLayout tabLayout = findViewById(R.id.tabLayout); // 创建Adapter并设置 ViewPagerAdapter adapter = new ViewPagerAdapter(this); viewPager.setAdapter(adapter); new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { switch (position) { case 0: tab.setText("详情"); break; case 1: tab.setText("图片"); break; case 2: tab.setText("路线"); break; } }).attach(); // Adapter内部管理三个Fragment public class ViewPagerAdapter extends FragmentStateAdapter { public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); } @NonNull @Override public Fragment createFragment(int position) { switch (position) { case 0: return DetailFragment.newInstance(); // 文字详情 case 1: return GalleryFragment.newInstance(); // 图片轮播 case 2: return RouteFragment.newInstance(); // 路线规划 default: return DetailFragment.newInstance(); } } @Override public int getItemCount() { return 3; } }

参数说明:FragmentStateAdapterFragmentPagerAdapter更省内存,适用于Tab较多场景;TabLayoutMediator替代已废弃的setupWithViewPager(),避免AndroidX 1.6.0+版本崩溃。

3.3 路线规划模块:集成高德地图SDK实现步行/公交路径计算

RouteFragment中初始化地图并请求路线:

private AMap aMap; private RouteSearch routeSearch; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mapView = view.findViewById(R.id.mapView); mapView.onCreate(savedInstanceState); aMap = mapView.getMap(); aMap.moveCamera(CameraUpdateFactory.zoomTo(15)); // 初始化路线搜索 routeSearch = new RouteSearch(requireContext()); routeSearch.setRouteSearchListener(new RouteSearch.OnRouteSearchListener() { @Override public void onBusRouteSearched(BusRouteResult result, int errorCode) { if (errorCode == AMapException.CODE_AMAP_SUCCESS && result != null) { // 绘制公交路线 aMap.clear(); for (BusPath path : result.getPaths()) { PolylineOptions polylineOptions = new PolylineOptions(); polylineOptions.addAll(path.getSteps().get(0).getPolyline()); aMap.addPolyline(polylineOptions.color(Color.BLUE)); } } } @Override public void onWalkRouteSearched(WalkRouteResult result, int errorCode) { if (errorCode == AMapException.CODE_AMAP_SUCCESS && result != null) { // 绘制步行路线 aMap.clear(); for (WalkPath path : result.getPaths()) { PolylineOptions polylineOptions = new PolylineOptions(); polylineOptions.addAll(path.getSteps().get(0).getPolyline()); aMap.addPolyline(polylineOptions.color(Color.GREEN)); } } } }); } // 发起步行路线请求(起点为当前定位,终点为景点坐标) private void searchWalkRoute(LatLng start, LatLng end) { WalkRouteQuery query = new WalkRouteQuery(start, end); routeSearch.calculateWalkRouteAsyn(query); }

注意:高德地图Key需在AndroidManifest.xml<application>标签内声明:

<meta-data android:name="com.amap.api.v2.apikey" android:value="你的高德开发者Key" />

3.4 本地收藏功能:用Room数据库实现跨进程、卸载不丢失的数据持久化

定义FavoriteEntity实体类与FavoriteDao接口:

@Entity(tableName = "favorite_table") public class FavoriteEntity { @PrimaryKey(autoGenerate = true) public long id; public String cityName; public String spotName; public String description; public long timestamp; // 收藏时间戳,用于排序 } @Dao public interface FavoriteDao { @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(FavoriteEntity entity); @Query("SELECT * FROM favorite_table ORDER BY timestamp DESC") LiveData<List<FavoriteEntity>> getAllFavorites(); @Query("DELETE FROM favorite_table WHERE id = :id") void delete(long id); } @Database(entities = {FavoriteEntity.class}, version = 1, exportSchema = false) public abstract class TravelDatabase extends RoomDatabase { public abstract FavoriteDao favoriteDao(); }

在Application类中初始化数据库单例:

public class TravelApplication extends Application { private TravelDatabase database; @Override public void onCreate() { super.onCreate(); database = Room.databaseBuilder( getApplicationContext(), TravelDatabase.class, "travel_database" ).build(); } public TravelDatabase getDatabase() { return database; } }

在收藏按钮点击事件中调用:

// 在DetailActivity中 findViewById(R.id.btn_favorite).setOnClickListener(v -> { FavoriteEntity entity = new FavoriteEntity(); entity.cityName = currentCity.getName(); entity.spotName = currentSpot.getName(); entity.description = currentSpot.getDescription(); entity.timestamp = System.currentTimeMillis(); new InsertAsyncTask(database.favoriteDao()).execute(entity); }); // 异步插入避免阻塞主线程 private static class InsertAsyncTask extends AsyncTask<FavoriteEntity, Void, Void> { private final FavoriteDao dao; InsertAsyncTask(FavoriteDao dao) { this.dao = dao; } @Override protected Void doInBackground(FavoriteEntity... entities) { dao.insert(entities[0]); return null; } }

关键点:exportSchema = false避免生成JSON schema文件(毕业设计无需版本迁移);LiveData返回使UI自动响应数据库变更,无需手动notifyDataSetChanged()

4. 毕业设计答辩高频问题应对:权限适配、离线策略与性能验证

4.1 Android 10+存储权限与定位权限的合规写法

Android 10(API 29)起强制启用分区存储(Scoped Storage),读取外部存储图片需用MediaStore而非File路径:

// 错误写法(Android 10+崩溃) Bitmap bitmap = BitmapFactory.decodeFile("/storage/emulated/0/Pictures/spot.jpg"); // 正确写法:通过ContentResolver查询 Uri imageUri = ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageId); InputStream inputStream = getContentResolver().openInputStream(imageUri); Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

定位权限需分步申请:先请求ACCESS_FINE_LOCATION(前台定位),再动态判断是否需要ACCESS_BACKGROUND_LOCATION(后台持续定位):

private void requestLocationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_LOCATION); } else if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) { // 仅当用户开启后台定位才申请 if (shouldRequestBackgroundLocation()) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, REQUEST_CODE_BACKGROUND_LOCATION); } } } }

4.2 离线场景下的数据兜底方案:Asset目录预置JSON+Room初始化

为避免网络不可用时App空白,将城市与景点数据以JSON格式放入app/src/main/assets/cities.json

[ { "id": 1, "name": "北京", "description": "首都,历史文化名城", "spots": [ { "name": "故宫", "description": "明清两代皇宫", "lat": 39.916, "lng": 116.397 } ] } ]

App首次启动时解析并写入Room:

private void initDatabaseIfEmpty() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { if (database.favoriteDao().getAllFavorites().getValue().isEmpty()) { try (InputStream is = getAssets().open("cities.json"); BufferedReader reader = new BufferedReader( new InputStreamReader(is))) { String json = reader.lines().collect(Collectors.joining("\n")); JSONArray citiesArray = new JSONArray(json); for (int i = 0; i < citiesArray.length(); i++) { JSONObject cityObj = citiesArray.getJSONObject(i); // 解析并插入City、Spot等实体 insertCityAndSpots(cityObj); } } catch (IOException | JSONException e) { e.printStackTrace(); } } return null; } }.execute(); }

4.3 性能验证三板斧:StrictMode检测、Systrace分析、RecyclerView FPS监控

Application.onCreate()中启用StrictMode,捕获主线程磁盘IO:

if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .build()); }

使用adb shell dumpsys gfxinfo com.example.travelguide获取帧率数据,合格标准:90%以上帧耗时≤16ms(60fps);若出现大量>16ms帧,需检查onBindViewHolder中是否执行耗时操作(如Bitmap.decodeResource)。

检测维度工具命令合格阈值典型问题
主线程IOadb shell am broadcast -a android.intent.action.BUGREPORTStrictMode无log输出Room查询未用AsyncTask
布局层级Layout Inspector(AS内置)< 10层ConstraintLayout嵌套过多
内存泄漏adb shell dumpsys meminfo com.example.travelguidePSS < 50MB(中端机)Activity持有静态Context引用

最后一步:在真机(推荐小米/华为)上运行,打开开发者选项中的「GPU呈现模式分析」,观察帧渲染曲线是否平滑。若答辩时被问“如何证明你的App性能达标”,直接展示该曲线图比口头描述更有力。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询