拓冰建站拓冰建站
首页 / 资讯中心 / 正文

安卓期末项目“寻味之旅”:Room存储、高德定位与列表交互完整实现

简介这是一份面向移动应用开发课程的Android期末大作业源码项目名称为“寻味之旅”以美食或旅行主题App为切入基于Android Studio构建适合Android初学者、K12阶段学习者以及需要完成类似课程设计的学生参考。资源以完整工程形式呈现覆盖Activity界面搭建、Service后台逻辑、XML布局、数据持久化、网络请求与图片加载等常见模块并体现对Android四大组件、第三方库如Glide、Retrofit的运用可帮助理解一个完整App从项目结构到功能实现的全过程。压缩包共2000个文件约72.4MB主要包含Java源文件、XML布局与配置、Gradle构建脚本、JSON配置及资源文件并含编译生成的class/dex文件、APK与JAR依赖其中Java/XML/Gradle分别对应业务逻辑、界面与工程构建class/dex为编译产物目录结构完整便于按模块阅读和二次开发。目前已有251人学习下载适合作为移动应用开发期末大作业、技术实训或个人练手的参考素材既能用来对照项目梳理Android开发核心知识点也可为完成“寻味之旅”类似App提供可复用的思路与排错经验。1. 移动应用开发安卓期末的“寻味之旅”是最典型的位置列表综合题“寻味之旅”被很多安卓期末大作业当成选题因为它把“美食”场景和“位置”绑在一起之后列表页、详情页、地图定位、搜索、收藏几乎全用上了。单做一个列表页容易显得工作量不足只做地图又很难跟答辩老师讲清数据怎么存。把这个题目做完整等于同时打通了Android里最常被考核的四块UI组件、本地数据库、定位服务、数据刷新。下面按这条线一步步说清所有命令、配置、代码段都按“一个人用AndroidStudio从零建工程、最终能装到真机上演示”的前提来写。不依赖后端、不等网络数据预置在App包里适合期末这种要在陌生教室演示的场景。如果手里已经有一套源码也可以拿本文对各章的选型和参数核对重点看Room、地图Key、动态权限这三个最容易翻车的地方。2. AndroidStudio中搭“寻味之旅”导航骨架与列表首页2.1 用Empty Activity起项目Gradle依赖只留必需期末项目最怕的不是功能少而是依赖冲突。用AndroidStudio新建一个Empty Views Activity工程包名写成自己学号的域名反写语言选Java即可。Java虽然是老写法但老师翻代码时最直接也不必在Kotlin协程版本上再花时间调。项目创建后模块build.gradle里的android节点保持模板默认我一般在此基础上补的配置如下android { compileSdk 34 defaultConfig { applicationId com.example.xunwei minSdk 21 targetSdk 34 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.navigation:navigation-fragment:2.5.3 implementation androidx.navigation:navigation-ui:2.5.3 implementation androidx.recyclerview:recyclerview:1.3.0 implementation androidx.room:room-runtime:2.5.0 annotationProcessor androidx.room:room-compiler:2.5.0 implementation com.google.code.gson:gson:2.10.1 }上表版本以你本机AndroidStudio当前能编译通过为准不要机械照抄。新建工程报TargetSdk版本过高时把compileSdk和targetSdk一起降到模拟器能跑的API级别。这里三个参数的意义分别是compileSdk决定编译时能用哪些APIminSdk决定最低兼容机型targetSdk决定系统以什么兼容模式运行这个App。三者不要求一致但targetSdk最好不大于测试机系统版本否则有些行为差异会干扰现场演示。依赖少还有一个实际好处Gradle同步快。教室里用AndroidStudio第一次构建经常卡在依赖下载上。如果在国内网络环境给Gradle配好阿里云镜像比换库更管用。2.2 底部导航配合FragmentContainerView的页面组织方式“寻味之旅”按最常见做法拆成三个一级页面首页列表、地图发现、我的收藏。写成三个Fragment比写在三个Activity里更符合导航习惯也能让老师看到你对页面复用的理解。activity_main.xml里放FragmentContainerView和BottomNavigationView用约束布局控制上下位置androidx.fragment.app.FragmentContainerView android:idid/main_nav_host android:nameandroidx.navigation.fragment.NavHostFragment android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toTopOfid/bottom_nav app:defaultNavHosttrue app:navGraphnavigation/nav_graph / com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottom_nav android:layout_width0dp android:layout_heightwrap_content app:layout_constraintBottom_toBottomOfparent app:menumenu/bottom_nav_menu /菜单里配三个itemnav_list、nav_map、nav_favorite。nav_graph.xml里把三个Fragment用action串起来在AndroidStudio可视化编辑器里连线即可。关键点是让首页作为startDestination否则App启动后会落在空白页。另外不要在Fragment之间通过Arguments塞大数据列表后面地图Fragment要用数据时通过Room重新读取否则切换页面容易触发Fragment事务异常。2.3 RecyclerView列表页的数据模型和Adapter要点列表页展示的每一条店铺数据先在Java里抽象成FoodItem类字段包括id、店铺名、标签、评分、纬度、经度、收藏状态。这个类后面直接对应Room的Entity所以命名和类型尽量保持一致public class FoodItem { private int id; private String name; private String tag; private float rating; private double latitude; private double longitude; private boolean favorite; }Adapter里常见的坑是图片加载。如果每道菜配网络图片教室断网后列表就全裂了。更稳的做法是不引入Glide给每个店铺配一个drawable资源名或者用tag字段决定一个纯色底加文字缩略图。这样你的项目源码跑起来不依赖外网老师反而会认可你处理了弱网场景。列表页还藏着一个加分项SwipeRefreshLayout。在RecyclerView外面包一层下拉时重新从数据库读一次数据。实现时要在onRefresh回调里把数据库查询放到子线程结束后调用setRefreshing(false)否则刷新的圈会一直转这是现场最容易看到的低级bug。3. 本地数据与项目源码结构Room存收藏、SharedPreferences存偏好3.1 存储方案选择什么时候用Room而不是SQLiteOpenHelper期末答辩被问得最多的一句是“你的数据是怎么存的”。回答“用SQLiteOpenHelper直接写”老师大概率会追问“放在哪个线程”。用Room可以直接说明三件事SQL语法在编译期校验写错字段名直接编译不过查询结果可以暴露成LiveData数据库一变界面自动通知底层仍然是SQLite面试问到也不虚。所以这个项目的数据层组合是收藏、搜索历史、预置店铺数据放进Room用户上次浏览的城市、排序方式、是否首次启动这些轻量配置放进SharedPreferences。数据类型存储方案理由刷新方式店铺预置数据Room需要搜索、排序、关联收藏LiveData观察收藏状态Room和店铺表一起查询写库后自动通知上次浏览城市SharedPreferences单条键值读取快手动提交搜索历史Room多条目且要展示列表按时间倒序查询这套组合的边界很清楚需要结构化查询的数据进数据库只是“记住某个值”的配置进Preferences。很多源码把收藏也塞进SharedPreferences数据少时看不出问题一旦接上地图和搜索状态同步就会变得很痛苦。3.2 实体、DAO、Database三件套的最小实现Room代码分三类文件Entity表示表结构DAO写增删改查Database负责把表和版本管理起来。FoodItem直接复用前面的类加注解Entity(tableName food) public class FoodItem { PrimaryKey(autoGenerate true) public int id; public String name; public String tag; public float rating; public double latitude; public double longitude; public boolean favorite; }DAO接口提供四个方法就够期末用Dao public interface FoodDao { Query(SELECT * FROM food ORDER BY rating DESC) LiveDataListFoodItem observeAll(); Query(SELECT * FROM food WHERE name LIKE % || :word || % OR tag LIKE % || :word || %) ListFoodItem search(String word); Query(SELECT COUNT(*) FROM food) long count(); Insert void insertAll(ListFoodItem list); Update void update(FoodItem item); }Database文件Database(entities {FoodItem.class}, version 1, exportSchema false) public abstract class AppDatabase extends RoomDatabase { private static AppDatabase instance; public abstract FoodDao foodDao(); public static AppDatabase getInstance(Context context) { if (instance null) { instance Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, xunwei.db) .fallbackToDestructiveMigration() .build(); } return instance; } }两个参数要解释清楚fallbackToDestructiveMigration在数据库版本升级且没有写Migration时直接清空重建期末阶段数据都是预置的清空后重新导一遍即可exportSchemafalse关掉Room编译器要求配置schema目录的提示源码更干净。Room禁止主线程访问数据库这点在下一节代码里会体现。3.3 首次启动用assets中的JSON灌入预置数据为了让“寻味之旅”在任何一台机器上装完都有内容看我会把20条左右预置店铺放在app/src/main/assets/foods.json里首次进入前检查数据库数量为空则自动导入。这样即使演示时断网数据闭环也不受影响。public void seedIfEmpty(Context context) { ExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(() - { AppDatabase db AppDatabase.getInstance(context); if (db.foodDao().count() 0) return; try { InputStream is context.getAssets().open(foods.json); byte[] buffer new byte[is.available()]; int len is.read(buffer); String json new String(buffer, 0, len, StandardCharsets.UTF_8); Type type new TypeTokenListFoodItem() {}.getType(); ListFoodItem items new Gson().fromJson(json, type); db.foodDao().insertAll(items); } catch (IOException e) { Log.e(Seed, read assets error, e); } }); }这代码里的两个细节值得在答辩时主动说通过count()判断幂等避免重复导入数据库操作放到单线程池避免主线程访问Room报错。如果直接在Activity的onCreate里调用Room查询会抛出IllegalStateException这是期末作业最常见的崩溃点。用byte数组读JSON只适合几十KB的小文件但预置店铺数据完全够用比在assets里放整个SQLite文件更容易让老师看懂。4. 高德地图接入“寻味之旅”定位权限与周边Marker展示4.1 校园演示为什么优先选高德SDK地图服务在安卓里选择看似很多但落到教室演示场景高德是闭环最完整的国内网络直接可用不依赖额外的服务框架文档和示例代码都是中文老师复核也方便定位SDK支持“单次定位”模式正好适合期末项目只定位一次的需求。Google Maps要依赖整套移动服务框架不少测试机上并不具备当场也没法快速安装。如果你用的是模拟器高德SDK一样能跑只是首次定位会慢几秒。唯一要提前做的是去高德开放平台注册应用。这个步骤绕不开也最容易被拖到最后需要填写应用包名以及发布版和调试版的安全码SHA1。SHA1在AndroidStudio右侧Gradle面板里的android - signingReport能直接看到复制时注意把冒号保留完整。高德控制台生成Key之后再把它配进Manifest的meta-data里。4.2 Manifest配置、SDK Key与依赖声明高德SDK的Maven依赖需要单独声明。在根build.gradle的repositories里加阿里云镜像模块依赖里加两行implementation com.amap.api:3dmap:latest.integration implementation com.amap.api:location:latest.integrationlatest.integration表示拉取最新发布版适合快速做demo如果高德文档给出了稳定版本号可以替换成具体数字。Manifest里必须声明定位和网络权限uses-permission android:nameandroid.permission.INTERNET/ uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/ application meta-data android:namecom.amap.api.v2.apikey android:value在这里填写你的Key / /application这里有一个排查顺序要记牢地图白屏时先看Manifest里的Key是否和控制台一致其次看包名是否一致最后看SHA1。三者错一个地图都只显示网格底图且没有崩溃日志。很多下载的源码地图白屏不是代码问题而是Key绑定的是别人的包名。4.3 动态申请定位权限并在地图上画出餐厅标记Android 6.0以后定位权限必须动态申请。MapView在Fragment里使用时还要在onCreateView里拿到MapView并在Fragment的onCreate、onResume、onDestroy等生命周期里调用MapView对应方法否则地图生命周期会和Fragment脱节。拿到权限后再启动高德定位private void startLocation() { AMapLocationClient locationClient new AMapLocationClient(getContext()); AMapLocationClientOption option new AMapLocationClientOption(); option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); option.setOnceLocation(true); locationClient.setLocationOption(option); locationClient.setLocationListener(location - { if (location.getErrorCode() 0) { LatLng current new LatLng(location.getLatitude(), location.getLongitude()); aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(current, 14f)); drawFoodMarkers(); } else { Toast.makeText(getContext(), 定位失败 location.getErrorInfo(), Toast.LENGTH_SHORT).show(); } }); locationClient.startLocation(); }setOnceLocation(true)是关键参数单次定位对演示最友好既省时间也避免连续定位回调反复绘制Marker。定位失败的Toast在这里不是给普通用户看的它是调试探针如果教室里定位失败屏幕会直接显示错误码最常见的两类是权限没给全和网络定位没开。绘制Marker的代码结构如下for (FoodItem item : items) { aMap.addMarker(new MarkerOptions() .position(new LatLng(item.getLatitude(), item.getLongitude())) .title(item.getName()) .snippet(item.getTag()) .setIcon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE))); }Marker参数里title是点击后显示的标题snippet是标题下方的副文本icon可以换成自己的drawable。期末阶段用默认锥形Marker足够想加分的话把评分高的店铺标成HUE_RED其余用HUE_ORANGE再在文档里写一句“红色标记代表评分4.5分以上”这是视觉层级上的小技巧答辩时非常好聊。5. “寻味”交互闭环搜索防抖、距离排序与收藏联动5.1 搜索框用TextWatcher加300ms防抖首页搜索栏如果只做“点按钮再查询”交互太老但直接在TextWatcher里每次执行数据库查询输入一个词就会触发五六次查询列表闪来闪去。一般做法是加300毫秒防抖用户停止输入300毫秒后才真正查库。这里不需要引入RxJava一个Handler就够handler.removeCallbacks(searchTask); searchTask () - performSearch(editText.getText().toString().trim()); handler.postDelayed(searchTask, 300);onTextChanged每次触发都先把上一次未执行的查询删掉再重新计时。这样连续输入时不会产生中间态的空结果显示手机上最明显的变化是列表不再闪烁。搜索框最右侧如果做了清除按钮点击清除时也要把Runnable一并remove否则清空后还会自动执行一次过期搜索。Room的LIKE查询里关键词两侧用%符号包住DAO参数绑定会自动处理转义这比手动拼SQL字符串安全。5.2 距离计算和多条件排序的落地写法“附近美食”这四个字不能只按评分排。按距离排序时用android.location.Location里的静态方法distanceBetween它已封装球面距离计算不需要第三方经纬度库private float distanceBetween(double latA, double lngA, double latB, double lngB) { float[] results new float[1]; Location.distanceBetween(latA, lngA, latB, lngB, results); return results[0]; }拿到距离后用Collections.sort配合Comparator实现多条件排序。下面这段是“评分优先、距离次之”的综合排序Collections.sort(list, (a, b) - { if (Float.compare(b.getRating(), a.getRating()) ! 0) { return Float.compare(b.getRating(), a.getRating()); } return Float.compare(distanceBetween(lat, lng, a.getLatitude(), a.getLongitude()), distanceBetween(lat, lng, b.getLatitude(), b.getLongitude())); });排序结果不要直接写回数据库因为列表可能还要切回“按收藏优先”。排序只是显示层的事不要在DAO里写死“按距离排序”的SQL跨页面保存距离值很麻烦。列表刷新时重新从Room读原始列表再按当前选中条件排序逻辑最清楚。几十条数据在UI线程排序不会崩但定位回调本身就在主线程养成“数据计算放到子线程、结果回到主线程刷新”的习惯对答辩更有利。5.3 收藏列表用RoomLiveData自动刷新详情页点收藏后返回首页列表收藏状态要立刻变化。这里最优雅的写法是DAO直接暴露LiveData而不是在onResume里手动重新查询Query(SELECT * FROM food ORDER BY favorite DESC, rating DESC) LiveDataListFoodItem observeFoods();Fragment里这样订阅viewModel.getFoods().observe(getViewLifecycleOwner(), items - { adapter.submitList(items); });Room观察到favorite字段改变后会自动把新数据发射给观察者。所以从详情页返回、切换Tab、下拉刷新这三个场景全部被覆盖不需要手动刷新。真正的坑只有两个一是查询方法没有定义成LiveData而是直接返回List那么Room只会查一次二是更新数据库时放在主线程更新没落库就抛异常界面自然也不刷新。如果现场发现收藏后不变化优先看DAO方法有没有被正确调用以及收藏点击事件里有没有调用到update。6. 期末提交的最后一课APK签名、真机检查与演示救场6.1 生成正式签名APK的入口和keytool命令AndroidStudio里执行Build - Generate Signed Bundle or APK第一次生成时选APK再点Create new key store填一组密码和别名即可。如果习惯命令行也可以用keytool生成同样的签名文件keytool -genkeypair -v -keystore xunwei.jks -alias xunwei -keyalg RSA -keysize 2048 -validity 10000keytool参数里validity是有效期天数10000天约27年足够覆盖在校期间keysize 2048是当前兼容性最好的RSA位数。签名文件生成后后续每次发版都选同一个jks和别名就能覆盖安装。别用调试签名提交否则同宿舍另一台电脑装了正式签名的包后你要覆盖安装时必须用同一个正式签名签名不一致会直接提示“应用未安装”。6.2 演示检查清单与两类故障的现场补救检查项通过标准失败时先看什么首页列表有预置数据下拉刷新不转圈assets文件名、count判断搜索中文关键词能搜出结果DAO的LIKE字段与输入框对应地图能显示当前位置和Marker高德Key、SHA1、包名收藏杀掉App重进后收藏还在Room写操作是否在子线程打包安装后图标正常不闪退minSdk与测试机系统版本演示现场最怕两件事地图白屏和列表空数据。地图白屏的救场顺序是先看Manifest里meta-data的Key有没有被错误编辑再看定位失败Toast显示的错误码如果是网络原因就把地图默认缩放调回城市范围同时说一句“定位失败时我保留了浏览模式”。列表空数据则检查seedIfEmpty有没有执行到insertAll直接在Logcat过滤Seed标签这条日志在现场比猜快很多。全部调试完以后最后一步打开开发者选项里的“不保留活动”把App从首页切到地图再切回来看Fragment状态能否恢复。这个操作能模拟老师演示时来回切换后台的真实动作也能提前暴露没有保存位置状态的问题定位监听里的失败分支记得保留Toast它既是给用户的提示也是判断Key是否正确的最短调试路径。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门