Android Studio校园二手交易App开发:MVVM架构与Jetpack实战指南
简介这是一份面向计算机专业本科生及Android初学者的毕业设计级实战项目源码聚焦校园场景下的二手商品交易全流程开发帮助学习者系统掌握移动应用从架构搭建到功能落地的完整链路。压缩包共186个文件含52个Java业务逻辑文件、57个XML布局与配置文件涵盖Activity、RecyclerView列表、权限声明等、24个PNG图标资源及13个sample示例代码片段辅以gradle构建脚本、README说明文档和Git版本控制信息整体大小为18.67MB。目前已有470人下载学习资源结构清晰app模块完整覆盖用户注册登录、商品发布浏览、购物车管理、订单交易、评价反馈与消息通知六大核心功能且代码遵循Material Design规范适合作为课程设计参考、毕设原型或Android进阶实践范本。1. 项目概述从零到一构建一个校园二手交易App最近几年无论是自己带学生做毕业设计还是看到身边学弟学妹们的选题“校园二手交易系统”几乎成了一个经久不衰的热门。这背后其实反映了一个非常实际的需求校园作为一个半封闭的社区学生群体流动性强每年都有大量的教材、数码产品、生活用品面临“毕业即闲置”的困境。一个专门服务于本校师生的二手交易平台天然具备信任度高、交易成本低、物流便捷的优势。所以当你选择这个作为Android Studio的毕业设计课题时方向是绝对正确的它既有足够的复杂度来展示你的综合能力又具备明确的实用价值。这个项目本质上是一个典型的移动端电商应用但相比淘宝、闲鱼它更聚焦于“校园”这个垂直场景。这意味着你需要重点考虑几个特性用户身份认证通常与学号绑定、商品分类贴合校园生活教材、数码、代步工具、宿舍神器等、基于地理位置或楼栋的信息筛选、以及相对简单的交易流程支持线上沟通、线下面对面交易。用Android Studio来实现不仅能够完整串联起Android开发的四大组件、网络通信、数据存储、UI设计等核心知识还能让你深入理解一个完整App从需求分析、架构设计到编码实现、测试上线的全流程。对于毕业生而言这是一个含金量很高的作品。接下来我将以一个“重构者”和“实践者”的角度带你深度拆解如何用Android Studio实现一个功能完备、代码清晰、易于扩展的校园二手交易App。我会假设你已有一定的Java/Kotlin和Android基础重点放在架构设计、模块拆分、技术选型和那些容易踩坑的细节上。我们不止于得到一个能运行的“源码.zip”更要理解每一行代码背后的意图打造一个真正属于你自己的毕业设计。2. 系统架构与核心技术选型解析在动手敲代码之前花时间在架构设计上是最高效的投资。一个清晰的架构能让后续开发、调试、扩展事半功倍也是你毕业设计答辩中的亮点。2.1 整体架构模式为什么选择MVVM对于这类数据驱动型的App我强烈推荐采用MVVMModel-View-ViewModel架构并结合Jetpack组件库。这是目前Android官方主推且最成熟的架构方案。Model层负责数据和业务逻辑。这包括本地数据库如Room、网络请求如Retrofit、以及数据仓库Repository。Repository是关键它作为单一数据源决定数据是来自网络还是本地缓存对上层屏蔽数据来源的复杂性。ViewModel层作为View和Model之间的桥梁。它持有与UI相关的数据并暴露这些数据给ViewActivity/Fragment。ViewModel的生命周期比View长因此屏幕旋转等配置更改不会导致数据丢失。我们使用LiveData或StateFlow来包装数据让View能观察到数据的变化并自动更新UI。View层即Activity和Fragment职责应尽可能轻量。它只负责绘制UI、处理用户交互并将交互事件传递给ViewModel绝不直接操作Model数据。为什么不是MVC或MVPMVC在Android中容易导致Activity/Fragment过于臃肿既当Controller又当View。MVP改善了这一点但需要手动编写大量接口模板代码较多。MVVM配合数据绑定或ViewBinding以及LiveData的观察机制能大幅减少“胶水代码”使UI与数据的同步变得自动化和声明式。2.2 技术栈选型与理由基于MVVM我们锁定以下核心技术栈这些都是经过大量项目验证的“黄金组合”开发语言Kotlin理由Google官方首选语言。空安全、扩展函数、协程等特性能让你写出更简洁、健壮的代码。对于毕业设计使用Kotlin是加分项。网络请求Retrofit OkHttp理由Retrofit是声明式的REST客户端通过接口定义API使用起来非常优雅。OkHttp作为底层客户端提供了强大的拦截器功能便于统一添加日志、请求头如Token认证。本地数据库Room理由SQLite的抽象层编译时检查SQL语句避免了运行时错误。与LiveData/Flow天然集成能轻松实现数据库变化驱动UI更新。用于缓存商品信息、用户信息、聊天记录等。异步处理Kotlin协程Coroutines理由用于替代传统的AsyncTask和回调地狱。以同步的方式写异步代码逻辑清晰。在ViewModel中使用viewModelScope启动协程生命周期自动管理。依赖注入Hilt理由Google基于Dagger的简化版。它能自动管理依赖对象的创建和生命周期让代码更解耦、更易测试。对于中型项目引入Hilt是值得的它让Repository、Retrofit实例等在哪里注入一目了然。UI与导航Jetpack Compose 或 传统View系统选择如果你的项目时间充裕且想展示前沿技术强烈推荐尝试Jetpack Compose。它是声明式UI工具包代码更简洁状态管理更直观。但学习曲线稍陡。稳妥选择使用传统的View系统XML布局配合ViewBinding以及Navigation组件管理页面跳转。这是最成熟、资料最多的方案能确保你顺利完工。图片加载与缓存Glide 或 Coil理由必须使用专业库自己处理图片压缩、缓存、生命周期绑定会非常痛苦。Glide功能全面强大Coil由Kotlin编写更轻量与协程集成好。两者任选其一。后端与云服务Bmob后端云 或 自建Spring Boot后端快速原型对于毕业设计为了聚焦Android端可以使用Bmob、LeanCloud这类后端云服务。它们提供了现成的用户系统、数据存储、文件存储、甚至即时通讯SDK能极大加快开发速度。全栈展示如果你想展示全栈能力可以自己用Spring Boot搭建一个简单的后端API。这样毕业设计就包含了“移动端服务端”内容更充实但工作量也翻倍。实操心得技术选型切忌求新求全。毕业设计的核心是“在有限时间内做出一个完整、可演示、代码规范的作品”。因此我建议采用“Kotlin MVVM Jetpack (ViewModel, LiveData, Room, Navigation) Retrofit Glide”这个经典组合。如果时间紧后端用Bmob。这个组合稳定、资料多遇到问题几乎都能搜到解决方案。3. 核心功能模块设计与实现拆解一个校园二手交易App的核心功能可以拆解为以下几个模块我们逐一分析其设计要点和实现思路。3.1 用户系统模块这是所有功能的基石主要包括注册、登录、个人信息管理。数据结构设计// User.kt - 用户实体类 Entity(tableName users) data class User( PrimaryKey val userId: String, // 通常使用学号或后端生成的唯一ID val username: String, val avatarUrl: String?, val phone: String, // 用于联系 val dormitory: String?, // 宿舍楼信息方便线下交易 val createTime: Long )关键实现Token管理登录成功后后端会返回一个Token如JWT。我们需要在每次网络请求的Header中携带它。可以使用OkHttp的Interceptor统一添加。登录状态持久化使用SharedPreferences或DataStore简单存储Token和用户基本信息。更佳实践是将用户信息存入Room作为单条缓存。全局用户状态在应用顶层如自定义Application类或通过Hilt注入的单例UserManager维护一个当前用户的可观察对象如MutableStateFlowUser?所有需要用户信息的界面都观察它。注意事项注册时前端应对学号、手机号格式做初步校验。密码必须加密传输HTTPS是基础后端应对密码进行哈希存储。考虑“忘记密码”流程通常需要通过绑定的邮箱或手机号验证。3.2 商品模块这是App的核心包括商品发布、浏览、搜索、详情查看。数据结构设计// Product.kt - 商品实体类 Entity(tableName products) data class Product( PrimaryKey val productId: String, val title: String, val description: String, val price: Double, val originalPrice: Double?, // 原价 val category: String, // 分类教材、数码、生活等 val imageUrls: ListString, // 图片URL列表可用Json字符串存储或用关联表 val sellerId: String, // 关联卖家用户ID val sellerName: String, val publishTime: Long, val location: String?, // 交易地点如“西区食堂”、“3号宿舍楼” val status: Int // 状态0-在售1-已售出2-已下架 )关键实现商品发布UI涉及多图上传。使用Intent.ACTION_GET_CONTENT或Intent.ACTION_PICK选择图片并用Glide/Coil预览。后端将图片文件上传到云存储如阿里云OSS、腾讯云COS或Bmob的文件服务获取URL列表再连同其他商品信息一起提交。商品列表使用RecyclerView展示Item布局包含缩略图、标题、价格、卖家、发布时间。实现分页加载是必须的使用Paging 3库可以优雅地实现。当用户滑动到底部时自动加载下一页数据。下拉刷新使用SwipeRefreshLayout。商品搜索与筛选顶部一个搜索栏可以按关键词搜索。提供筛选器如按分类、价格区间、新旧程度、发布时间筛选。筛选条件可以作为查询参数传递给后端API。商品详情页使用ViewPager2展示多张商品图片。展示商品所有信息并提供“联系卖家”和“我想要”的入口。实操心得图片处理是关键在上传前一定要对图片进行压缩。可以使用Glide加载时指定尺寸或者使用Luban等第三方压缩库。原图直接上传会耗费大量流量和时间用户体验极差。列表性能优化RecyclerView的ViewHolder模式要正确使用。图片加载要处理好滚动时的取消和复用。对于复杂列表可以考虑使用DiffUtil来高效更新数据。3.3 聊天模块即时通讯买卖双方沟通的渠道。这是功能亮点但实现复杂度较高。方案选择集成第三方SDK推荐如融云、环信、腾讯云IM。它们提供了完整的IM能力登录、消息收发、会话列表你只需要集成SDK并做UI封装。这是最快最稳的方案适合毕业设计。使用后端云服务Bmob也提供了即时通讯服务可以简化开发。自研WebSocket不推荐自己用OkHttp的WebSocket或Socket.IO实现需要处理连接管理、心跳、消息重发、离线存储等一系列复杂问题极易出错不适合作为毕业设计的核心。实现要点以集成SDK为例初始化IM SDK通常在Application中。用户登录App后也用同一UserId登录IM SDK。在商品详情页点击“联系卖家”其实就是启动一个聊天界面并传入卖家的UserId作为会话目标。实现一个会话列表页面展示所有私聊会话。实现聊天界面能发送文本、图片商品图、表情等。注意事项注意IM SDK的生命周期管理确保及时登录登出。消息漫游和离线推送通常需要配置厂商通道小米、华为等这部分较复杂如果时间有限可以只做在线消息作为演示功能。3.4 其他辅助模块消息通知模块用于系统通知如商品被下单、收到新消息。可以使用Firebase Cloud Messaging (FCM)或国内厂商推送如小米、OPPO推送集成起来有一定工作量。简易版可以在App内做一个红点提醒从服务器拉取未读消息数。收藏与浏览历史在本地Room中建立Favorite和History表关联用户ID和商品ID。方便用户追踪感兴趣的商品。设置与关于页面包含退出登录、清除缓存、版本信息等。4. 数据库与网络层详细设计这一层是应用的“发动机”设计好坏直接决定数据处理的效率和稳定性。4.1 本地数据库设计Room我们使用Room来缓存网络数据提升二次访问速度并在无网络时提供基本展示。// AppDatabase.kt Database(entities [User::class, Product::class, Favorite::class], version 1, exportSchema false) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun productDao(): ProductDao abstract fun favoriteDao(): FavoriteDao companion object { // 单例实例 Volatile private var INSTANCE: AppDatabase? null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, campus_trade.db ).build() INSTANCE instance instance } } } } // ProductDao.kt Dao interface ProductDao { Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertProduct(product: Product) Query(SELECT * FROM products WHERE status 0 ORDER BY publishTime DESC) fun getLatestProducts(): FlowListProduct // 使用Flow可观察查询结果变化 Query(SELECT * FROM products WHERE title LIKE % || :keyword || %) suspend fun searchProducts(keyword: String): ListProduct Update suspend fun updateProduct(product: Product) }设计要点OnConflictStrategy.REPLACE当插入数据发生冲突如相同主键时替换旧数据。这很适合从网络更新本地缓存。使用Flow或LiveData作为返回类型这样当数据库中的数据变化时UI能自动收到通知并更新。对于商品图片URL列表这种复杂类型Room不能直接存储ListString。有两种方案1) 使用TypeConverter将其转换为JSON字符串存储2) 新建一张ProductImage表与商品表建立一对多关系。方案1更简单方案2更规范。4.2 网络层与数据仓库Repository模式Repository是MVVM架构中的“调度中心”它决定数据从哪里来网络 or 本地。// ProductRepository.kt class ProductRepository Inject constructor( private val productApi: ProductApiService, // Retrofit接口 private val productDao: ProductDao, private val ioDispatcher: CoroutineDispatcher Dispatchers.IO ) { // 获取商品列表先尝试从网络获取成功后更新本地数据库如果失败则返回本地缓存。 fun getLatestProducts(): FlowResourceListProduct flow { emit(Resource.Loading()) // 发射加载状态 val cachedProducts productDao.getLatestProducts().first() // 先取缓存 if (cachedProducts.isNotEmpty()) { emit(Resource.Success(cachedProducts)) } try { val networkProducts productApi.getLatestProducts() // 网络请求 productDao.insertAll(networkProducts) // 清空旧缓存或增量更新根据业务定 emit(Resource.Success(networkProducts)) } catch (e: Exception) { e.printStackTrace() // 如果网络失败且本地缓存为空才发射错误 if (cachedProducts.isEmpty()) { emit(Resource.Error(网络错误: ${e.message})) } // 否则UI将继续显示缓存数据 } }.flowOn(ioDispatcher) // 搜索商品 suspend fun searchProducts(keyword: String): ResourceListProduct { return try { val result productApi.searchProducts(keyword) Resource.Success(result) } catch (e: Exception) { Resource.Error(e.message ?: 未知错误) } } // 发布商品 suspend fun publishProduct(productRequest: PublishRequest): ResourceString { return try { // 处理图片上传 val imageUrls mutableListOfString() for (imageFile in productRequest.imageFiles) { val url uploadImage(imageFile) // 调用单独的上传接口 imageUrls.add(url) } // 提交商品信息 val response productApi.publishProduct( productRequest.copy(imageUrls imageUrls) ) Resource.Success(response.productId) } catch (e: Exception) { Resource.Error(e.message ?: 发布失败) } } } // 一个简单的资源包装类用于统一处理加载、成功、错误状态 sealed class ResourceT(val data: T? null, val message: String? null) { class SuccessT(data: T) : ResourceT(data) class ErrorT(message: String, data: T? null) : ResourceT(data, message) class LoadingT(data: T? null) : ResourceT(data) }关键点解析单一数据源UI只从Repository获取数据不关心数据来自哪里。网络优先缓存兜底在getLatestProducts中我们采用了常见策略先发射缓存数据如果存在让UI快速展示同时发起网络请求成功后用新数据更新缓存和UI。网络失败时如果缓存有数据UI依然有内容可看提升了用户体验。状态管理使用Resource密封类包装数据让ViewModel和View能清晰地知道当前是加载中、成功还是失败便于显示加载动画、错误提示等。错误处理网络请求必须用try-catch包裹防止崩溃。错误信息应友好地传递给用户。4.3 ViewModel与UI绑定ViewModel从Repository获取数据并转换为UI可观察的状态。// ProductListViewModel.kt HiltViewModel class ProductListViewModel Inject constructor( private val repository: ProductRepository ) : ViewModel() { private val _productListState MutableStateFlowProductListState(ProductListState.Loading) val productListState: StateFlowProductListState _productListState.asStateFlow() init { loadProducts() } fun loadProducts() { viewModelScope.launch { repository.getLatestProducts().collect { resource - _productListState.value when (resource) { is Resource.Loading - ProductListState.Loading is Resource.Success - ProductListState.Success(resource.data ?: emptyList()) is Resource.Error - ProductListState.Error(resource.message) } } } } // 搜索功能 private val _searchResults MutableStateFlowListProduct(emptyList()) val searchResults: StateFlowListProduct _searchResults.asStateFlow() fun searchProducts(keyword: String) { viewModelScope.launch { val result repository.searchProducts(keyword) if (result is Resource.Success) { _searchResults.value result.data ?: emptyList() } else { // 处理搜索错误 } } } } // UI状态密封类 sealed class ProductListState { object Loading : ProductListState() data class Success(val products: ListProduct) : ProductListState() data class Error(val message: String?) : ProductListState() }在Fragment或Activity中观察这个StateFlow并根据不同的状态更新UI// ProductListFragment.kt AndroidEntryPoint class ProductListFragment : Fragment() { private val viewModel: ProductListViewModel by viewModels() private lateinit var binding: FragmentProductListBinding override fun onCreateView(...): View? { binding FragmentProductListBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 观察商品列表状态 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.productListState.collect { state - when (state) { is ProductListState.Loading - { binding.progressBar.visibility View.VISIBLE binding.recyclerView.visibility View.GONE } is ProductListState.Success - { binding.progressBar.visibility View.GONE binding.recyclerView.visibility View.VISIBLE // 更新RecyclerView适配器数据 adapter.submitList(state.products) } is ProductListState.Error - { binding.progressBar.visibility View.GONE // 显示错误提示如Snackbar Snackbar.make(binding.root, 加载失败: ${state.message}, Snackbar.LENGTH_LONG).show() } } } } } // 设置下拉刷新 binding.swipeRefreshLayout.setOnRefreshListener { viewModel.loadProducts() binding.swipeRefreshLayout.isRefreshing false } // 设置搜索 binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { query?.let { viewModel.searchProducts(it) } return true } override fun onQueryTextChange(newText: String?): Boolean { // 可做实时搜索但要注意防抖 return false } }) } }5. 界面开发与用户体验优化要点UI是用户直接感知的部分良好的交互设计能极大提升应用质感。5.1 主界面与导航设计通常采用底部导航栏BottomNavigationView结合Navigation组件。首页商品瀑布流列表。考虑加入轮播图Banner展示热门或推荐商品。发布页一个表单页面用于发布商品。布局要清晰对必填项要有明确提示。消息页展示会话列表。我的页用户个人信息、我发布的商品、我的收藏、设置等。使用Navigation组件管理这些页面的跳转和回退栈能有效避免Fragment重叠等问题。5.2 商品列表项RecyclerView Item优化列表是最高频的界面性能优化至关重要。布局优化使用ConstraintLayout减少布局层级。对于图片使用固定宽高比如app:layout_constraintDimensionRatio1:1防止图片拉伸。图片加载优化// 在Adapter的onBindViewHolder中 Glide.with(holder.itemView.context) .load(product.imageUrls.firstOrNull()) // 加载第一张图作为缩略图 .placeholder(R.drawable.placeholder) // 占位图 .error(R.drawable.error) // 错误图 .centerCrop() .into(holder.imageView)关键技巧为ImageView设置明确的layout_width和layout_heightGlide会根据此进行采样减少内存占用。在RecyclerView滚动时可以考虑暂停Glide加载以提升流畅度Glide.with(context).pauseRequests()停止滚动时再恢复。使用DiffUtil当列表数据更新时DiffUtil会计算新旧数据集的差异并只更新发生变化的Item而不是重绘整个列表效率极高。Paging 3库内部已集成此机制。5.3 发布商品页面的细节处理这是交互最复杂的页面之一。图片选择与预览使用registerForActivityResult来启动系统相册或相机。选中的图片用RecyclerView水平排列展示允许用户删除已选图片。限制最大选择数量如9张并提示用户。表单验证在点击“发布”按钮时对标题、描述、价格、分类等进行非空和格式校验。价格输入框应限制只能输入数字和小数点。使用TextInputLayout可以方便地显示错误提示。发布状态反馈发布请求是网络操作需要显示加载对话框。发布成功或失败后给出明确的Toast或Snackbar提示并导航回首页或商品详情页。5.4 全局样式与暗黑模式使用Material Components库并定义统一的主题themes.xml规范颜色、字体、圆角等。从Android 10开始支持暗黑模式可以通过定义-night资源文件夹来适配提升应用品质。6. 项目构建、测试与部署实战6.1 项目结构组织清晰的项目结构是团队协作和后期维护的基础。推荐按功能模块分包com.yourcompany.campustrade/ ├── data/ # 数据层 │ ├── local/ # Room数据库相关 (Dao, Entity, Database) │ ├── remote/ # 网络请求相关 (ApiService, DTO) │ └── repository/ # 仓库实现 ├── di/ # 依赖注入模块 (Hilt Modules) ├── domain/ # 领域层 (可选存放业务逻辑用例) ├── ui/ # 表现层 │ ├── home/ # 首页 │ ├── publish/ # 发布页 │ ├── message/ # 消息页 │ ├── profile/ # 我的页 │ ├── detail/ # 商品详情页 │ └── common/ # 通用组件、适配器、基类 ├── utils/ # 工具类 (扩展函数、常量、SharedPrefs管理) └── App.kt # 应用入口6.2 版本控制与代码规范Git从第一天就使用Git。在GitHub或Gitee上创建私有仓库。进行有意义的提交Commit如“feat: 完成商品发布页面UI”、“fix: 修复图片上传失败的问题”。代码规范遵循Kotlin官方编码规范。使用ktlint或detekt插件在提交前自动格式化代码。6.3 测试策略毕业设计不要求100%测试覆盖率但关键路径的测试能体现你的工程素养。本地单元测试Unit Test测试不依赖Android框架的类如Repository、ViewModel使用TestDispatcher、工具类等。使用JUnit和MockK来模拟依赖。Test fun search products with empty keyword should return empty list() runTest { // 1. 创建模拟对象 val mockApi mockkProductApiService() val mockDao mockkProductDao() // 2. 设置模拟行为 coEvery { mockApi.searchProducts() } returns emptyList() // 3. 创建被测对象 val repository ProductRepository(mockApi, mockDao) // 4. 执行测试 val result repository.searchProducts() // 5. 验证结果 assertTrue(result is Resource.Success) assertTrue((result as Resource.Success).data.isEmpty()) }仪器化测试Instrumented Test在真机或模拟器上测试涉及UI或数据库的操作。例如测试数据库的增删改查。运行较慢但更真实。6.4 常见问题与调试技巧实录在开发过程中你几乎一定会遇到以下问题问题一网络请求成功但数据不显示/UI不更新。排查首先检查LiveData或StateFlow的值是否真的改变了。在ViewModel中设置断点或打印日志。其次检查UI层是否正确地观察了数据。LifecycleOwner是否正确在Fragment的onViewCreated之后开始观察。技巧使用Android Studio的Layout Inspector和Database Inspector工具可以实时查看UI布局和数据库内容非常方便。问题二RecyclerView列表滑动卡顿。排查布局层次过深使用Layout Inspector检查Item布局尽量扁平化。图片加载确保图片尺寸经过合理压缩未加载过大图片。主线程耗时操作检查onBindViewHolder中是否有复杂的逻辑计算或同步IO操作。解决使用Glide的override(Target.SIZE_ORIGINAL)指定加载尺寸将复杂计算移到后台线程。问题三发布商品时多张图片上传慢且容易失败。解决压缩如前所述上传前务必压缩。并发上传使用协程的async并发上传多张图片。suspend fun uploadImages(files: ListFile): ListString coroutineScope { files.map { file - async { uploadSingleImage(file) } // uploadSingleImage是挂起函数 }.awaitAll() // 等待所有并发任务完成 }进度反馈如果需要显示总进度可以使用Channel或Flow来发射上传进度。问题四应用在后台被杀死后用户登录状态丢失。解决将Token和关键用户信息持久化到SharedPreferences或DataStore中。每次App启动或需要认证时先读取本地Token。在Application类或主Activity的onCreate中检查Token有效性并决定跳转到登录页还是主页。问题五不同Android版本或厂商设备的兼容性问题。预防使用最新的targetSdkVersion和compileSdkVersion但minSdkVersion不要设得太高如设为21覆盖绝大多数设备。使用AndroidX库它们有更好的向后兼容性。对于文件存储如保存图片到相册使用FileProvider来适配Android 7.0及以上版本。对于权限申请使用ActivityResult API并处理好权限被永久拒绝的情况。6.5 毕业设计文档与源码整理最后你需要将项目打包并准备答辩材料。源码整理删除项目中的build文件夹、.gradle文件夹、*.iml文件。在根目录提供一个清晰的README.md文件说明项目名称、功能、技术栈、如何编译运行如API Base URL需要如何配置。将项目依赖的Gradle配置好确保他人git clone后能直接Sync并运行。生成“源码.zip”在Android Studio中点击File - Export To Zip File...或者直接在文件管理器中将整个项目文件夹压缩。确保压缩包解压后能直接用Android Studio打开。答辩准备演示文稿PPT结构建议为项目背景与意义 - 需求分析 - 系统设计架构图、数据库ER图 - 核心功能演示录屏或现场操作 - 关键技术详解 - 总结与展望。演示视频提前录制一个3-5分钟的功能演示视频防止现场演示时出现意外。重点阐述不要只讲界面。重点讲你的架构设计为什么用MVVM、技术选型Retrofit、Room的优势、遇到的难点及解决方案如图片上传优化、列表卡顿排查。这能充分展示你的技术深度。完成这样一个项目你收获的不仅仅是一个毕业设计和一份源码更是一次完整的、贴近工业标准的移动应用开发实战经验。从需求到设计从编码到测试每一个环节的思考与决策都是你未来求职简历上最扎实的一笔。祝你开发顺利答辩成功本文还有配套的精品资源点击获取