简介:这是一份面向计算机专业本科生及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,我强烈推荐采用MVVM(Model-View-ViewModel)架构,并结合Jetpack组件库。这是目前Android官方主推且最成熟的架构方案。
- Model层:负责数据和业务逻辑。这包括本地数据库(如Room)、网络请求(如Retrofit)、以及数据仓库(Repository)。Repository是关键,它作为单一数据源,决定数据是来自网络还是本地缓存,对上层屏蔽数据来源的复杂性。
- ViewModel层:作为View和Model之间的桥梁。它持有与UI相关的数据,并暴露这些数据给View(Activity/Fragment)。ViewModel的生命周期比View长,因此屏幕旋转等配置更改不会导致数据丢失。我们使用
LiveData或StateFlow来包装数据,让View能观察到数据的变化并自动更新UI。 - View层:即Activity和Fragment,职责应尽可能轻量。它只负责绘制UI、处理用户交互,并将交互事件传递给ViewModel,绝不直接操作Model数据。
为什么不是MVC或MVP?MVC在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实例等在哪里注入一目了然。
- 理由:Google基于Dagger的简化版。它能自动管理依赖对象的创建和生命周期,让代码更解耦、更易测试。对于中型项目,引入Hilt是值得的,它让
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)维护一个当前用户的可观察对象(如MutableStateFlow<User?>),所有需要用户信息的界面都观察它。
- Token管理:登录成功后,后端会返回一个Token(如JWT)。我们需要在每次网络请求的Header中携带它。可以使用OkHttp的
注意事项:
- 注册时,前端应对学号、手机号格式做初步校验。
- 密码必须加密传输(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: List<String>, // 图片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列表,再连同其他商品信息一起提交。
- UI:涉及多图上传。使用
- 商品列表:
- 使用
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(通常在
注意事项:
- 注意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(): Flow<List<Product>> // 使用Flow,可观察查询结果变化 @Query("SELECT * FROM products WHERE title LIKE '%' || :keyword || '%'") suspend fun searchProducts(keyword: String): List<Product> @Update suspend fun updateProduct(product: Product) }设计要点:
OnConflictStrategy.REPLACE:当插入数据发生冲突(如相同主键)时,替换旧数据。这很适合从网络更新本地缓存。- 使用
Flow或LiveData作为返回类型,这样当数据库中的数据变化时,UI能自动收到通知并更新。 - 对于商品图片URL列表这种复杂类型,Room不能直接存储
List<String>。有两种方案: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(): Flow<Resource<List<Product>>> = 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): Resource<List<Product>> { return try { val result = productApi.searchProducts(keyword) Resource.Success(result) } catch (e: Exception) { Resource.Error(e.message ?: "未知错误") } } // 发布商品 suspend fun publishProduct(productRequest: PublishRequest): Resource<String> { return try { // 处理图片上传 val imageUrls = mutableListOf<String>() 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 Resource<T>(val data: T? = null, val message: String? = null) { class Success<T>(data: T) : Resource<T>(data) class Error<T>(message: String, data: T? = null) : Resource<T>(data, message) class Loading<T>(data: T? = null) : Resource<T>(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 = MutableStateFlow<ProductListState>(ProductListState.Loading) val productListState: StateFlow<ProductListState> = _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 = MutableStateFlow<List<Product>>(emptyList()) val searchResults: StateFlow<List<Product>> = _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: List<Product>) : 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_constraintDimensionRatio="1: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_height,Glide会根据此进行采样,减少内存占用。 - 在
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 = mockk<ProductApiService>() val mockDao = mockk<ProductDao>() // 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: List<File>): List<String> = coroutineScope { files.map { file -> async { uploadSingleImage(file) } // uploadSingleImage是挂起函数 }.awaitAll() // 等待所有并发任务完成 }- 进度反馈:如果需要显示总进度,可以使用
Channel或Flow来发射上传进度。
- 解决:
问题四:应用在后台被杀死后,用户登录状态丢失。
- 解决:将Token和关键用户信息持久化到
SharedPreferences或DataStore中。每次App启动(或需要认证时)先读取本地Token。在Application类或主Activity的onCreate中检查Token有效性,并决定跳转到登录页还是主页。
- 解决:将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的优势)、遇到的难点及解决方案(如图片上传优化、列表卡顿排查)。这能充分展示你的技术深度。
完成这样一个项目,你收获的不仅仅是一个毕业设计和一份源码,更是一次完整的、贴近工业标准的移动应用开发实战经验。从需求到设计,从编码到测试,每一个环节的思考与决策,都是你未来求职简历上最扎实的一笔。祝你开发顺利,答辩成功!
本文还有配套的精品资源,点击获取