简介:一套完整的安卓开发结课大作业资料包,由王英强老师编写,主要面向初学安卓开发、需要完成课程设计的学生,内容涵盖从项目创建、代码编写到打包上线的完整过程。压缩包内共六百二十五个文件,整体大小约十九点三八兆字节;源码中包含界面布局文件、程序代码以及构建脚本,可学习页面生命周期、页面跳转、碎片管理、后台服务、数据存储、权限申请等核心知识点;文件列表中还有编译生成的中间产物和可直接运行的安装包,能够帮助理解应用从源码到安装包的构建、签名与压缩流程。附带的实训报告则系统记录了需求分析、功能设计、问题排查和优化思路,便于借鉴完整的项目开发方法。目前有一万五千二百三十九人浏览学习,适合作为结课大作业的参考范例,也适合复习安卓开发的工程结构、调试技巧、构建配置与发布流程。
1. 结课大作业不是写 App,是走通一条交付链路
Android Studio 结课大作业到最后关头才动手的人,往往不是代码写不出来,而是不知道“做到什么程度才算完”。课程评分里有一个隐性维度:你的项目能不能在别人电脑上打开、能不能装到手机上跑、代码里有没有明显凑数的地方。这比界面好看更影响成绩。所以这篇内容的思路很直接:把结课大作业当作一条完整的交付链路来走——环境准备、项目骨架、功能实现、调试打包、答辩演示,每一段都给可复制的命令和参数。项目规模按 3 到 5 个界面、2 到 3 张数据表、1 个网络请求来规划,正好覆盖大多数课程的知识点,也适合一到两周内写完。
2. Android Studio 项目搭建与 Gradle 配置
2.1 三个必须提前确认的环境项
环境问题的排查成本高于编码,尤其是用新电脑打开旧项目时,报错往往不是代码引起的。
第一,SDK 路径。Android Studio 默认把 SDK 放在用户目录,Windows 常见路径是C:\Users\<用户名>\AppData\Local\Android\Sdk,macOS 是~/Library/Android/sdk。如果之前有人改过路径,新建项目时会直接弹出SDK location not found。处理方式:File → Project Structure → SDK Location,重新指向 SDK 根目录。注意不要指到platform-tools或build-tools子目录,Gradle 需要的是 SDK 根路径下的 platform、build-tools、platform-tools 等并列目录。
第二,Gradle 仓库源。这是国内环境下第一个“卡住”的地方。Android Studio 默认从google()和mavenCentral()拉依赖,但连接速度很不稳定。我一般会在创建项目后立刻修改根目录的settings.gradle.kts,在仓库列表前面加镜像:
pluginManagement { repositories { maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/central") } maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://maven.aliyun.com/repository/central") } google() mavenCentral() } }镜像放在最前面,Gradle 按顺序查找仓库时能优先命中。FAIL_ON_PROJECT_REPOS的作用是禁止在模块级build.gradle.kts里再单独声明仓库,这只适合“所有依赖统一管”的场景,如果你用的旧代码里模块级有repositories,要把那几行删掉,否则直接编译失败。
第三,模拟器配置。创建虚拟设备时选择带 Google APIs 的 x86_64 系统镜像,硬件加速用默认的Windows Hypervisor Platform或AEHD(取决于 Android Studio 版本)。内存分配建议 2GB 起步,低于 1.5GB 会在启动时反复黑屏。如果启动模拟器时报accel相关错误,去 BIOS 确认 CPU 虚拟化没有关闭,这是最常见的模拟器起不来的原因。
2.2 用 Empty Views Activity 创建项目
创建项目的入口是New Project → Empty Views Activity。为什么不建议用Empty Activity(Compose 模板)?课程作业的评分老师通常按 XML 布局知识来理解界面实现,Compose 在答辩时容易陷入“这是什么东西”的追问。XML 模板更稳,也有现成的布局思路可以参考。
创建时三个参数要明确:
| 参数 | 建议值 | 原因 |
|---|---|---|
| Package name | com.example.你的项目名 | 作为所有类的命名空间,不要带大写字母 |
| Minimum SDK | API 24 或 26 | 覆盖市场 95% 以上设备,也避开老版本兼容坑 |
| Build configuration language | Kotlin DSL | 新版默认,文档和搜索最多的配置语法 |
创建完成后,项目会自动生成目录。不要着急写代码,先跑一次app模块,确认空模板能在模拟器里启动。这一步通过,后续添加代码时才能区分“是环境问题还是代码问题”。
2.3 模块 build.gradle.kts 里这套依赖组合
课程作业用到的依赖不外乎五类:UI 兼容、数据库、网络、图片加载、注解处理。把下面这段作为起点,注释对应了每个库的作用:
dependencies { // UI 基础 implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") // RecyclerView 列表 implementation("androidx.recyclerview:recyclerview:1.3.2") // Room 数据库及编译期代码生成 implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") kapt("androidx.room:room-compiler:2.6.1") // Retrofit 网络请求和 JSON 解析 implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") }房间数据库的room-compiler必须用kapt而不是implementation,而且要在plugins块里声明插件:
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }典型的版本坑是:Room 2.6.x 搭配 Lifecycle 2.7.x 时,编译可能报cannot find symbol之类的错误,屏幕上的堆栈指向 Room 生成的代码。解决办法不是换 Room 版本,而是查看build.gradle.kts里是否重复引入了不同版本的lifecycle相关库,统一版本号后再试。
依赖版本没必要追求“最新”。1.6.1、2.9.0这些版本符号常有人去升级,但课程作业用稳定版本就够了,AGP 版本和 dependencies 版本组合越不“激进”,越少遇到互相不兼容的问题。
2.4 分包结构,宁可多分也不要全塞一个文件
第一版代码建议这样分包:
com.example.coursework/ ├── MainActivity.kt ├── data/ │ ├── AppDatabase.kt │ ├── ItemDao.kt │ └── ItemRepository.kt ├── model/ │ └── Item.kt └── ui/ ├── ItemAdapter.kt └── detail/DetailActivity.ktmodel放数据实体;data放数据库、DAO、网络接口;ui放 Activity 和列表 Adapter。这种分法没有任何框架上的深度,但答辩时你可以很清楚地讲出“哪层负责什么”。一个 Activity 里塞几百行代码的写法最大的问题是答辩提问环节——老师随便指一个方法问“它为什么放在这里”,答不上来反而拉低印象分。
分包后写代码的边界也明确:Activity 只负责界面事件,数据库操作全部丢给 Repository,Repository 要访问 Room 的 DAO。这比在 Activity 里直接调用 DAO 更符合常规的代码结构,评审老师愿意看到这一层。
3. 核心功能实现:布局、ViewBinding 与 Room 持久化
3.1 XML 布局优先记住 ConstraintLayout
界面层不要每页都用 LinearLayout 嵌套。课程作业最常见的列表页 + 详情页,列表项布局用 ConstraintLayout 能少一层嵌套,渲染开销也小。以下是列表 item 的常见写法:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="12dp"> <TextView android:id="@+id/tvTitle" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toStartOf="@id/ivArrow" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" /> <ImageView android:id="@+id/ivArrow" android:layout_width="24dp" android:layout_height="24dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>这里0dp加左右约束是关键,它让标题宽度由左右两侧位置决定,而不是固定像素,这样屏幕适配不会出问题。如果遇到过标题过长把箭头挤出屏幕的情况,多半就是宽度写了固定值或没有约束末端。第二点,item 高度用wrap_content而不是match_parent,否则整个列表会有大量留白。
3.2 ViewBinding 替代 findViewById 的模板代码
课程作业代码里最不值得出现的重复就是findViewById加类型强转。在模块build.gradle.kts中启用后就能减少很多行:
android { buildFeatures { viewBinding = true } }Activity 里这样写:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnSave.setOnClickListener { val title = binding.etTitle.text.toString() if (title.isNotBlank()) { saveItem(title) } } } }ActivityMainBinding是根据布局文件名activity_main.xml自动生成的,命名规则就是把下划线去掉、首字母大写再加Binding。两个容易失败的细节:一是setContentView(binding.root)之后不要再写setContentView(R.layout.activity_main),否则binding里的控件全部无法找到 view,界面空白还不报编译错;二是如果需要“点击后跳到详情页”,用startActivity(Intent(this, DetailActivity::class.java)),Intent 参数要传具体 Activity 的 class 而不是字符串。
3.3 Room 三件套:Entity、Dao、Database
如果项目里必须有数据保存功能,Room 是比 SQLiteOpenHelper 合适得多的选择。三处代码基本固定。
Entity 定义表结构:
@Entity(tableName = "items") data class Item( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val detail: String, val createdAt: Long = System.currentTimeMillis() )DAO 提供增删查方法:
@Dao interface ItemDao { @Insert suspend fun insert(item: Item) @Update suspend fun update(item: Item) @Delete suspend fun delete(item: Item) @Query("SELECT * FROM items ORDER BY createdAt DESC") fun getAll(): List<Item> }注意getAll()没有用挂起函数,因为 Room 在非挂起返回的情况下会阻塞主线程,这里我建议把它改成这样:
@Query("SELECT * FROM items ORDER BY createdAt DESC") suspend fun getAll(): List<Item>加suspend后,调用方必须在协程或 ViewModel 里执行,避免Cannot access database on the main thread的崩溃。上面这段 DAO 里@Insert suspend fun insert是标准写法,但初写代码时容易在 Activity 里Thread里调用 DAO,结果数据写不进去——因为 Room 的 suspend 设计要求调用发生在协程作用域,而不是裸的thread {}。
Database 类负责创建实例:
@Database(entities = [Item::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun itemDao(): ItemDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun get(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val db = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "coursework.db" ).build() INSTANCE = db db } } } }把 context 换成applicationContext是防止泄漏 Activity 引用。version = 1表示当前表结构版本,如果以后要加字段,记得改版本号并写 Migration,否则升级安装时直接崩溃。这套写法在答辩时可以讲清楚的逻辑点有三个:单例确保数据库实例全局唯一、DAO 接口与 SQL 解耦、suspend保证主线程安全。能说清这三点,数据部分基本不会被问倒。
在 Activity 里观察数据并更新列表的标准写法:
lifecycleScope.launch { val items = db.itemDao().getAll() adapter.submitList(items) }lifecycleScope是 Lifecycle 库提供的协程作用域,Activity destroy 时自动取消。如果你在 ViewModel 里使用,要换成viewModelScope。这一行的区别是答辩常问的细节。
3.4 网络请求只接 Retrofit 这一套
如果结课选题需要展示远程数据,比如课程表、新闻列表、天气,推荐直接用 Retrofit。以下是带 JSON 解析的最小实现。
在model包定义数据类:
data class ApiResult( @SerializedName("code") val code: Int, @SerializedName("message") val message: String?, @SerializedName("data") val data: List<Item>? )@SerializedName用于把服务端 JSON 字段映射到 Kotlin 属性。之后定义接口:
interface ApiService { @GET("api/items") suspend fun fetchItems(): ApiResult }然后在 Repository 中创建实例并调用:
object ApiClient { private val retrofit = Retrofit.Builder() .baseUrl("https://example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() val service: ApiService = retrofit.create(ApiService::class.java) } class ItemRepository(private val dao: ItemDao) { suspend fun loadFromRemote(): List<Item> { val result = ApiClient.service.fetchItems() return result.data ?: emptyList() } }Retrofit 的suspend方法自动在线程池执行,回调回主线程,不需要自己写withContext(Dispatchers.IO),多写反而容易导致性能多一次切换。别忽略addConverterFactory(GsonConverterFactory.create()),没有这个,.create(ApiService::class.java)会直接抛 “Unable to create call adapter” 的运行时错误。
4. 调试、构建打包与常见报错处理
4.1 日志关键字比断点更常用
真机或模拟器调试时,Logcat 里过滤Tag是最快捷的。统一的TAG常量写起来很简单:
companion object { private const val TAG = "MainActivity" }然后需要的地方打印:
Log.d(TAG, "save button clicked, title=$title") Log.e(TAG, "network call failed", exception)答辩时老师问“你怎么知道程序跑到了这里”时,能说出“打断点验证 + 日志打点确认”,这已经比其他学生高一层。断点调试的使用要点是:在 Android Studio 左侧点击代码行号,然后用 Debug 模式运行,程序会停在断点处,可以查看当前的变量值。如果断点不生效,检查是否在Run → Edit Configurations里选成了Run而不是Debug。
4.2 真机联调比模拟器更贴近交付
模拟器适合开发过程,最终演示和安装包验证建议用真机。Android Studio 里用 USB 连接手机时,有几点特别影响效率:
- 手机开启开发者选项,
USB 调试开关必须打开。 - 部分品牌手机需要选择文件传输模式而不是“仅充电”。
- 无线调试:Android 11 以上可以
adb pair配对后无线连接,适合 USB 口不稳定时使用。
无线调试的步骤:手机开启无线调试,Android StudioFile → Settings → Build, Execution, Deployment → Debugger → Wireless里选择配对,输入手机显示的配对码,完成后就可以拔掉 USB 线。实际部署时,数据线和 USB 口接触不良容易导致 Logcat 断开,无线调试能减少这类干扰。
4.3 Gradle 构建失败的三类高发错误
| 报错特征 | 原因 | 处理方式 |
|---|---|---|
Could not find com.android.tools.build:gradle:x.x.x | 镜像源里没有对应版本或缓存过期 | 确认settings.gradle.kts中 Aliyun 镜像仓库已加;File → Sync Project with Gradle Files重新同步 |
tag number over 30 is not supported | dex 方法数超过 65535,或 multidex 配置缺失 | 在模块build.gradle.kts里加multiDexEnabled = true,并把依赖库精简到必要项 |
trouble writing output: Too many methods | 同上 | 同上,先减少不用的依赖,不要再往里加 “备用” 库 |
multiDexEnabled是在模块android {}块里:
android { compileSdk = 34 defaultConfig { applicationId = "com.example.coursework" minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0" multiDexEnabled = true } }如果编译报的是 AGP 版本太新导致 Gradle 版本不兼容,日志会明确写出最低支持的 Gradle 版本。这时File → Project Structure → Project里改 Gradle Distribution 的版本号,位置在gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip不要手动去改 Gradle 的安装目录,wrapper 指定的版本才是项目在别人电脑上能否打开的关键。课程作业交到老师电脑上时,真正决定能不能跑的就是gradle-wrapper.properties里的这一行。
4.4 生成可安装的 APK
完成编码后,用Build → Generate App Bundle or APK → APK生成安装包。Debug 包可以直接装,但答辩演示建议用 Release 签名包,因为部分模拟器或手机默认禁止安装 Debug 包。生成 Release 需要签名:
android { signingConfigs { create("release") { storeFile = file("../release.keystore") storePassword = "your-password" keyAlias = "release" keyPassword = "your-password" } } buildTypes { release { signingConfig = signingConfigs.getByName("release") isMinifyEnabled = false } } }isMinifyEnabled = false表示不做混淆,课程作业不需要开启 R8 混淆,否则可能出现 “类找不到” 的 Release 包崩溃而 Debug 包正常。签名文件是.keystore格式的一个文件,由Build → Generate向导自动生成,需要记住密码,后面更新版本时不能换。这个方法也顺带解决一个问题:发布 APK 时如果不想暴露密码在代码里,可以从环境变量读取,但课程项目建议直接把密码写在自己电脑上的配置里就行,简单直接。
5. 用 Git 固定交付状态与答辩自检
5.1 提交节奏与最终版本冻结
代码写完后最容易出的问题不是功能,而是“改到哪一版才算最终版”。用 Git 给每个阶段打 tag 是一个很值得养成的习惯。
开发过程中按节点提交,每个提交对应一个能运行的状态:
git init git add . git commit -m "feat: 完成项目骨架和列表页" git tag v0.1然后继续加功能:
git add . git commit -m "feat: 完成详情页和数据保存" git tag v0.2答辩前一周的最终修改再打一个 tag:
git add . git commit -m "final: 修复数据刷新和空态提示" git tag v1.0-release打 tag 的价值在于,一旦后面做演示视频或重新生成 APK 时改坏代码,可以立刻回退到v1.0-release,而不需要在代码里翻找哪个版本是能用的。git log --oneline --decorate可以快速查看 tag 列表,配合git reset --hard v1.0-release就能回到稳定版。
5.2 演示前十分钟的自检清单
在答辩教室或录演示视频开始之前,按下面这张表逐项过一遍,命中每一项就能避免大多数现场事故:
| 检查项 | 操作 | 合格标准 |
|---|---|---|
| 安装包可安装 | 在模拟器或真机上执行安装 | 不出现 “应用未安装” 或签名冲突 |
| 首次启动不崩溃 | 冷启动 App | Logcat 无红色异常 |
| 数据可写入 | 添加一条记录并重启应用 | 数据没有丢失 |
| 网络请求不阻塞 | 进入需要网络的页面 | 2 秒内有反馈或错误提示 |
| 返回键可用 | 在详情页按返回 | 回到列表页且状态不变 |
| 屏幕旋转 | 旋转一次屏幕 | 不崩溃,字段不丢 |
其中“屏幕旋转”最容易被忽略,课程作业里很多崩溃其实是配置变更导致 Activity 重建,数据没保存就丢失。如果时间不够,最简单的方案是在 Manifest 的 Activity 标签里固定竖屏:
<activity android:name=".MainActivity" android:screenOrientation="portrait" />答辩时演示到网络请求,如果网络环境不乐观,提前准备好一个返回假数据的本地 JSON 文件,放在assets目录下,在 Repository 里做一层判断:没网时读本地文件作为兜底数据。这个设计在答辩时反而是加分项,因为它体现了一个工程视角:不做无提示的失败。
最后,演示页面务必确认字体大小和屏幕分辨率匹配。模拟器的默认屏幕与实际手机存在差异,在模拟器里布局正常、真机上挤成一团的案例很常见。把布局缩略图(View → Tool Windows → Layout Inspector)打开扫一眼,确认没有控件重叠或文字截断,这比任何“我是按规范写的”都有说服力。
本文还有配套的精品资源,点击获取