
如果你是一名计算机专业的学生正在为“Android Studio 记账本”这个期末大作业或毕业设计而发愁那么这篇文章就是为你准备的。你可能已经看过很多零散的教程但依然困惑于如何将“记账功能”从一个简单的想法变成一个结构清晰、功能完整、能拿得出手的 Android 应用。常见的痛点包括界面设计一团糟、数据不知道存哪里、增删改查逻辑混乱、项目结构经不起老师提问。这篇文章要解决的正是这些问题。我将以一个典型的“记账本”应用为例带你从零开始在 Android Studio 中构建一个具备完整增删改查CRUD功能、采用主流架构、并包含数据可视化模块的项目。本文的核心判断是一个优秀的课程设计其价值不在于功能的堆砌而在于对 Android 开发核心知识点的系统性串联与工程化实践。你将不仅得到一个可运行的代码更能理解其背后的设计思路、技术选型原因以及如何应对答辩中的技术提问。我们将从项目架构设计开始逐步完成数据模型、数据库操作、用户界面和核心业务逻辑最后还会加入图表展示功能。文章会提供完整的、可复制的代码片段并解释每一处关键设计的选择理由。无论你是 Android 新手还是想优化现有项目的同学都能从中获得可直接落地的解决方案。1. 项目核心目标与设计思路在动手写代码之前明确我们要构建一个什么样的应用至关重要。一个记账本的核心是管理“收入”和“支出”。但作为课程设计我们需要展示更全面的能力。1.1 核心功能清单一个基础的记账本应用应包含以下功能模块账目管理添加、查看、修改、删除单条记账记录。记录展示以列表形式清晰展示所有记录并能按时间、类型等排序筛选。数据统计对支出和收入进行汇总并以图表形式直观展示消费构成。数据持久化将记录安全地存储在本地数据库中确保应用重启后数据不丢失。1.2 技术选型与架构设计为了实现上述功能并体现一定的技术深度我们采用以下技术栈和架构开发环境Android Studio版本以当前稳定版为准本文重点在通用逻辑。语言Kotlin。相比 JavaKotlin 语法更简洁空安全特性能让代码更健壮是当前 Android 开发的官方首选语言。架构采用MVVMModel-View-ViewModel。这是 Google 官方推荐的架构模式能有效将界面逻辑与业务逻辑分离使代码更易于测试和维护。对于课程设计而言采用 MVVM 是极大的加分项。数据库使用Room Persistence Library。它是 SQLite 的抽象层提供了编译时 SQL 检查、方便的 ORM对象关系映射功能极大地简化了数据库操作。异步处理使用Kotlin Coroutines协程。用于处理数据库操作等耗时任务避免阻塞主线程导致界面卡顿。图表库使用MPAndroidChart。这是一个功能强大且灵活的图表库可以轻松实现饼图、柱状图等用于数据可视化。界面构建使用Jetpack Compose推荐或View Binding。Compose 是现代声明式 UI 工具包代码更简洁View Binding 是传统视图绑定方式稳定易学。本文将基于更现代的 Compose 进行讲解并提供关键思路。这个技术组合不仅是当前企业级开发的常见选择也能全面覆盖课程考察点UI设计、数据存储、架构模式、异步编程和第三方库集成。2. 环境准备与项目创建2.1 安装与配置 Android Studio从官网下载并安装最新稳定版的 Android Studio。安装完成后启动 Android Studio它会自动安装必要的 Android SDK 和工具。可选在File - Settings - Appearance Behavior - System Settings - Android SDK中确保已安装你目标版本的 SDK如 API 34。2.2 创建新项目点击New Project。选择模板。为了更清晰地学习我们选择Empty Activity如果使用 Compose则选择Empty Compose Activity然后点击Next。配置项目Name:MyMoneyBook(项目名称)Package name:com.example.mymoneybook(按需修改)Save location: 选择你的项目存放路径Language:选择 KotlinMinimum SDK: 选择API 24: Android 7.0 (Nougat)或更高以兼容大多数设备。点击Finish。2.3 配置项目依赖项目创建后打开app模块下的build.gradle.kts文件Module 级别。我们需要添加必要的库依赖。找到dependencies块添加以下依赖dependencies { // ... 其他默认依赖 // Room 数据库 val room_version 2.6.1 implementation(androidx.room:room-runtime:$room_version) implementation(androidx.room:room-ktx:$room_version) // 对 Kotlin 协程的支持 kapt(androidx.room:room-compiler:$room_version) // Kotlin 注解处理器 // Lifecycle 和 ViewModel (用于 MVVM) implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0) implementation(androidx.lifecycle:lifecycle-livedata-ktx:2.7.0) implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.7.0) // Compose implementation(platform(androidx.compose:compose-bom:2024.02.02)) implementation(androidx.compose.ui:ui) implementation(androidx.compose.ui:ui-graphics) implementation(androidx.compose.ui:ui-tooling-preview) implementation(androidx.compose.material3:material3) implementation(androidx.activity:activity-compose:1.8.2) implementation(androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0) // 在 Compose 中使用 ViewModel // MPAndroidChart implementation(com.github.PhilJay:MPAndroidChart:v3.1.0) // 单元测试 testImplementation(junit:junit:4.13.2) androidTestImplementation(androidx.test.ext:junit:1.1.5) androidTestImplementation(androidx.test.espresso:espresso-core:3.5.1) androidTestImplementation(platform(androidx.compose:compose-bom:2024.02.02)) androidTestImplementation(androidx.compose.ui:ui-test-junit4) }添加完成后点击右上角的Sync Now同步项目。同步过程会下载这些库。3. 数据层设计定义实体与数据库数据层是应用的基石我们首先创建数据模型和数据库。3.1 创建数据实体Entity在app/src/main/java/com/example/mymoneybook/下新建一个data目录然后创建Transaction.kt文件。这个类代表一条记账记录。// 文件路径app/src/main/java/com/example/mymoneybook/data/Transaction.kt package com.example.mymoneybook.data import androidx.room.Entity import androidx.room.PrimaryKey import java.util.Date import java.util.UUID // Entity 注解告诉 Room 这是一个数据库表表名默认为类名小写这里指定为 “transactions” Entity(tableName transactions) data class Transaction( // PrimaryKey 标记为主键并设置自动生成 PrimaryKey(autoGenerate true) val id: Int 0, // 账目类型收入Income或支出Expense val type: String, // 分类如餐饮、交通、工资、奖金等 val category: String, // 金额 val amount: Double, // 备注 val note: String? null, // 记录日期使用 Long 类型存储时间戳 val date: Long System.currentTimeMillis() ) { // 一个辅助属性用于在 UI 中方便地显示日期 val formattedDate: String get() android.text.format.DateFormat.format(yyyy-MM-dd HH:mm, Date(date)).toString() }3.2 创建数据访问对象DAODAO 是一个接口定义了访问数据库的方法。在data目录下创建TransactionDao.kt。// 文件路径app/src/main/java/com/example/mymoneybook/data/TransactionDao.kt package com.example.mymoneybook.data import androidx.room.* import kotlinx.coroutines.flow.Flow // Dao 注解标记这是一个 DAO 接口 Dao interface TransactionDao { // Insert 插入一条记录冲突策略为替换 Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(transaction: Transaction) // Update 更新一条记录 Update suspend fun update(transaction: Transaction) // Delete 删除一条记录 Delete suspend fun delete(transaction: Transaction) // Query 自定义 SQL 查询获取所有记录按日期降序排列 // Flow 是 Kotlin 协程中的响应式流当数据库变化时UI 会自动更新 Query(SELECT * FROM transactions ORDER BY date DESC) fun getAllTransactions(): FlowListTransaction // 查询某段时间内的总支出 Query(SELECT SUM(amount) FROM transactions WHERE type Expense AND date BETWEEN :start AND :end) suspend fun getTotalExpenseBetween(start: Long, end: Long): Double? // 查询某段时间内的总收入 Query(SELECT SUM(amount) FROM transactions WHERE type Income AND date BETWEEN :start AND :end) suspend fun getTotalIncomeBetween(start: Long, end: Long): Double? // 按类别统计支出用于饼图 Query(SELECT category, SUM(amount) as total FROM transactions WHERE type Expense AND date BETWEEN :start AND :end GROUP BY category) suspend fun getExpenseByCategory(start: Long, end: Long): ListCategorySum } // 一个数据类用于接收按类别统计的查询结果 data class CategorySum(val category: String, val total: Double)3.3 创建数据库类在data目录下创建AppDatabase.kt文件它是数据库的持有者。// 文件路径app/src/main/java/com/example/mymoneybook/data/AppDatabase.kt package com.example.mymoneybook.data import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase // Database 注解声明这是一个 Room 数据库包含哪些实体并指定版本号 Database(entities [Transaction::class], version 1, exportSchema false) abstract class AppDatabase : RoomDatabase() { // 提供获取 DAO 的抽象方法 abstract fun transactionDao(): TransactionDao // 单例模式确保整个应用只有一个数据库实例 companion object { Volatile private var Instance: AppDatabase? null fun getDatabase(context: Context): AppDatabase { return Instance ?: synchronized(this) { Room.databaseBuilder( context, AppDatabase::class.java, moneybook_database // 数据库文件名 ).build().also { Instance it } } } } }至此数据层搭建完成。Room 会在编译时根据这些注解生成具体的实现代码。4. 业务逻辑层实现 ViewModelViewModel 是连接 ViewUI和 Model数据的桥梁它负责准备和管理 UI 相关的数据。在app/src/main/java/com/example/mymoneybook/下新建ui目录然后在其中创建transaction子目录。在transaction目录下创建TransactionViewModel.kt。// 文件路径app/src/main/java/com/example/mymoneybook/ui/transaction/TransactionViewModel.kt package com.example.mymoneybook.ui.transaction import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.mymoneybook.data.AppDatabase import com.example.mymoneybook.data.Transaction import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class TransactionViewModel(private val database: AppDatabase) : ViewModel() { private val dao database.transactionDao() // 使用 stateIn 将 Flow 转换为 StateFlow便于 Compose 收集 // SharingStarted.WhileSubscribed(5000) 表示当没有订阅者5秒后停止上游流节省资源 val allTransactions: StateFlowListTransaction dao.getAllTransactions() .stateIn( scope viewModelScope, started SharingStarted.WhileSubscribed(5000), initialValue emptyList() ) // 添加新记录 fun addTransaction(transaction: Transaction) { viewModelScope.launch { dao.insert(transaction) } } // 更新记录 fun updateTransaction(transaction: Transaction) { viewModelScope.launch { dao.update(transaction) } } // 删除记录 fun deleteTransaction(transaction: Transaction) { viewModelScope.launch { dao.delete(transaction) } } // 获取总支出和总收入示例本月 suspend fun getMonthlySummary(startOfMonth: Long, endOfMonth: Long): PairDouble, Double { val expense dao.getTotalExpenseBetween(startOfMonth, endOfMonth) ?: 0.0 val income dao.getTotalIncomeBetween(startOfMonth, endOfMonth) ?: 0.0 return Pair(expense, income) } // 获取按类别统计的支出用于图表 suspend fun getCategoryExpense(startOfMonth: Long, endOfMonth: Long) dao.getExpenseByCategory(startOfMonth, endOfMonth) }5. 用户界面层使用 Jetpack Compose 构建我们将创建几个主要的 Composable 函数来构建界面。首先修改MainActivity.kt。5.1 主活动入口// 文件路径app/src/main/java/com/example/mymoneybook/MainActivity.kt package com.example.mymoneybook import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.room.Room import com.example.mymoneybook.data.AppDatabase import com.example.mymoneybook.ui.theme.MyMoneyBookTheme import com.example.mymoneybook.ui.transaction.TransactionScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 获取数据库实例 val database AppDatabase.getDatabase(applicationContext) setContent { MyMoneyBookTheme { // 一个表面容器使用 background 颜色作为背景 Surface( modifier Modifier.fillMaxSize(), color MaterialTheme.colorScheme.background ) { // 传入数据库启动主界面 TransactionScreen(database database) } } } } }5.2 主界面屏幕在ui/transaction目录下创建TransactionScreen.kt。这里我们使用Navigation组件进行简单的页面切换但为了简化我们先实现一个包含底部导航的骨架。// 文件路径app/src/main/java/com/example/mymoneybook/ui/transaction/TransactionScreen.kt package com.example.mymoneybook.ui.transaction import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.runtime.livedata.observeAsState import com.example.mymoneybook.data.AppDatabase OptIn(ExperimentalMaterial3Api::class) Composable fun TransactionScreen(database: AppDatabase) { // 获取 ViewModel传入数据库 val viewModel: TransactionViewModel viewModel( factory TransactionViewModelFactory(database) ) // 收集交易列表数据 val transactions by viewModel.allTransactions.collectAsState(initial emptyList()) // 简单的 Scaffold 布局包含顶部栏和内容 Scaffold( topBar { CenterAlignedTopAppBar( title { Text(我的记账本) } ) }, content { innerPadding - // 内容区域 Column( modifier Modifier .fillMaxSize() .padding(innerPadding) .padding(16.dp), verticalArrangement Arrangement.spacedBy(8.dp) ) { // 1. 概览卡片 SummaryCard(viewModel) // 2. 添加记录按钮 AddButton(onClick { /* 跳转到添加页面 */ }) // 3. 交易记录列表 TransactionList(transactions transactions, onDelete { transaction - viewModel.deleteTransaction(transaction) }) } } ) } // 用于创建 ViewModel 的 Factory class TransactionViewModelFactory(private val database: AppDatabase) : ViewModelProvider.Factory { override fun T : ViewModel create(modelClass: ClassT): T { if (modelClass.isAssignableFrom(TransactionViewModel::class.java)) { Suppress(UNCHECKED_CAST) return TransactionViewModel(database) as T } throw IllegalArgumentException(Unknown ViewModel class) } }5.3 列表项组件在同一个目录下创建TransactionItem.kt。// 文件路径app/src/main/java/com/example/mymoneybook/ui/transaction/TransactionItem.kt package com.example.mymoneybook.ui.transaction import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.example.mymoneybook.data.Transaction OptIn(ExperimentalMaterial3Api::class) Composable fun TransactionItem( transaction: Transaction, onDelete: () - Unit, modifier: Modifier Modifier ) { Card( modifier modifier.fillMaxWidth(), colors CardDefaults.cardColors( containerColor if (transaction.type Income) Color(0xFFE8F5E9) else Color(0xFFFFEBEE) // 收入绿色背景支出红色背景 ) ) { Row( modifier Modifier .fillMaxWidth() .padding(16.dp), horizontalArrangement Arrangement.SpaceBetween, verticalAlignment Alignment.CenterVertically ) { Column(modifier Modifier.weight(1f)) { Text( text ${transaction.category} - ${transaction.note ?: }, style MaterialTheme.typography.bodyLarge, fontWeight FontWeight.Bold ) Spacer(modifier Modifier.height(4.dp)) Text( text transaction.formattedDate, style MaterialTheme.typography.bodySmall, color MaterialTheme.colorScheme.onSurfaceVariant ) } Text( text ${if (transaction.type Income) else -} ¥${transaction.amount}, style MaterialTheme.typography.titleLarge, fontWeight FontWeight.Bold, color if (transaction.type Income) Color(0xFF2E7D32) else Color(0xFFC62828) ) IconButton(onClick onDelete) { Icon(Icons.Default.Delete, contentDescription 删除) } } } }5.4 列表和概览组件继续完善TransactionScreen.kt中的其他组件。// 在 TransactionScreen.kt 中添加以下 Composable 函数 Composable fun SummaryCard(viewModel: TransactionViewModel) { var summary by remember { mutableStateOf(Pair(0.0, 0.0)) } LaunchedEffect(Unit) { // 获取本月第一天和最后一天的时间戳这里简化处理实际应计算 val startOfMonth 0L // 替换为实际计算逻辑 val endOfMonth System.currentTimeMillis() summary viewModel.getMonthlySummary(startOfMonth, endOfMonth) } Card( modifier Modifier.fillMaxWidth(), colors CardDefaults.cardColors(containerColor MaterialTheme.colorScheme.primaryContainer) ) { Row( modifier Modifier .fillMaxWidth() .padding(16.dp), horizontalArrangement Arrangement.SpaceEvenly ) { Column(horizontalAlignment Alignment.CenterHorizontally) { Text(本月支出, style MaterialTheme.typography.bodySmall) Text( text ¥${summary.first}, style MaterialTheme.typography.headlineSmall, color MaterialTheme.colorScheme.error ) } Column(horizontalAlignment Alignment.CenterHorizontally) { Text(本月收入, style MaterialTheme.typography.bodySmall) Text( text ¥${summary.second}, style MaterialTheme.typography.headlineSmall, color MaterialTheme.colorScheme.primary ) } Column(horizontalAlignment Alignment.CenterHorizontally) { Text(结余, style MaterialTheme.typography.bodySmall) Text( text ¥${summary.second - summary.first}, style MaterialTheme.typography.headlineSmall, fontWeight FontWeight.Bold ) } } } } Composable fun AddButton(onClick: () - Unit) { Button( onClick onClick, modifier Modifier.fillMaxWidth(), shape MaterialTheme.shapes.medium ) { Text(记一笔) } } Composable fun TransactionList(transactions: ListTransaction, onDelete: (Transaction) - Unit) { LazyColumn( modifier Modifier.fillMaxWidth(), verticalArrangement Arrangement.spacedBy(8.dp) ) { items(transactions) { transaction - TransactionItem( transaction transaction, onDelete { onDelete(transaction) } ) } } }6. 运行与效果验证6.1 连接设备或启动模拟器在 Android Studio 中确保已连接真机或已创建并启动一个模拟器如 Pixel 5 API 34。6.2 编译并运行项目点击工具栏上的绿色运行按钮或按ShiftF10。Android Studio 会构建项目并将其安装到设备上。6.3 预期效果应用启动后你将看到一个简单的界面顶部有一个标题栏“我的记账本”。中间有一个概览卡片显示本月支出、收入和结余初始为0。下方有一个“记一笔”按钮。再下方是空白的交易记录列表。6.4 测试数据操作通过代码插入为了快速验证增删改查我们可以在TransactionViewModel的初始化部分添加一些测试数据。在TransactionViewModel的init块中添加init { viewModelScope.launch { // 清空旧数据仅用于测试 // dao.getAllTransactions().firstOrNull()?.forEach { dao.delete(it) } // 插入测试数据 val testTransactions listOf( Transaction(type Expense, category 餐饮, amount 45.5, note 午餐), Transaction(type Expense, category 交通, amount 8.0, note 地铁), Transaction(type Income, category 工资, amount 10000.0, note 十月工资), Transaction(type Expense, category 购物, amount 299.0, note 买书), ) testTransactions.forEach { dao.insert(it) } } }注意测试完成后请注释或删除这段代码以免每次启动都插入重复数据。重新运行应用你应该能在列表中看到这几条测试记录并且概览卡片中的数字会更新。7. 常见问题与排查思路在开发过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案项目同步失败1. 网络问题导致依赖下载失败。2. Gradle 版本与项目不兼容。3.build.gradle文件语法错误。1. 查看Build输出窗口的错误信息。2. 检查网络连接。3. 核对依赖库名称和版本号。1. 使用稳定的网络或配置国内镜像源。2. 点击File - Sync Project with Gradle Files重新同步。3. 根据错误信息修正build.gradle。应用安装失败1. 设备上已存在同名但签名不同的应用。2. 设备存储空间不足。3. 最低 SDK 版本高于设备系统。1. 查看Run窗口的安装错误日志。2. 检查设备存储。1. 卸载设备上的旧版本应用。2. 清理设备存储。3. 在build.gradle中降低minSdkVersion或使用更高版本的设备/模拟器。应用运行时崩溃1. 数据库未初始化或访问错误。2. 空指针异常NPE。3. 主线程执行了耗时操作。1. 查看Logcat窗口的崩溃堆栈信息红色部分。2. 定位到崩溃的代码行。1. 确保AppDatabase.getDatabase(context)在Application或Activity生命周期早期被调用。2. 使用 Kotlin 的空安全操作符?.,?:。3. 确保数据库操作在协程viewModelScope.launch或后台线程中执行。界面不更新1.StateFlow或LiveData未被正确观察collectAsState。2. 数据变化未触发重组。1. 检查 Composable 函数是否正确地收集了状态。2. 使用Log.d打印数据变化。1. 确保在 Composable 中使用collectAsState()或observeAsState()。2. 确保修改的是StateFlow/MutableStateFlow或LiveData/MutableLiveData持有的数据。Room 编译错误1. 实体类定义错误如主键问题。2. DAO 中的 SQL 语法错误。3. 未添加kapt插件。1. 查看Build输出的具体错误信息。2. 检查Query中的 SQL 语句。1. 确保实体类有PrimaryKey。2. 使用 Room 的编译时检查功能修正 SQL。3. 在build.gradle的插件部分确认已添加id org.jetbrains.kotlin.kapt。8. 功能扩展与最佳实践建议一个基础的记账本已经完成但要让其成为优秀的课程设计还需要考虑以下扩展和优化8.1 功能扩展方向添加/编辑页面实现一个完整的表单页面用于输入或修改账目信息类型、分类、金额、备注、日期。数据筛选与搜索在列表顶部添加筛选器支持按类型、分类、时间范围进行筛选或添加搜索框。图表分析页面集成 MPAndroidChart创建一个新的 Fragment 或 Composable绘制本月支出分类饼图、月度消费趋势折线图等。数据导出将记账记录导出为 CSV 或 Excel 文件保存到手机存储。数据备份与恢复利用 Android 的备份 API 或云存储如 Firebase实现数据备份。8.2 工程化最佳实践代码分层与模块化将data,domain,ui层严格分离。可以考虑将Repository仓库模式引入作为 ViewModel 和数据源之间的中间层处理数据获取逻辑。使用依赖注入引入 Hilt 或 Koin 等依赖注入框架来管理ViewModel、Database等依赖使代码更可测试、更松散耦合。完善的错误处理在网络请求、数据库操作、用户输入验证等处添加try-catch并使用Snackbar或Toast友好地提示用户。主题与样式充分利用 Compose 的MaterialTheme或 XML 的styles.xml和themes.xml统一应用的颜色、字体和形状提升 UI 专业性。单元测试为ViewModel、Repository甚至DAO编写单元测试确保核心业务逻辑的正确性。这在答辩中是非常亮眼的一点。版本控制使用 Git 进行版本管理保持清晰的提交记录。这不仅是好习惯也能在项目演示时展示你的工程能力。8.3 答辩准备要点清晰阐述架构能说清楚为什么选择 MVVM以及 View、ViewModel、Model 各自的职责和数据流向。解释技术选型能说明 Room 相比原生 SQLite 的优势协程相比传统线程/AsyncTask 的优势。演示核心流程流畅演示记录的增、删、改、查操作。展示扩展功能如果有图表、筛选等扩展功能重点演示并解释实现原理。回答技术问题准备回答关于 LiveData/StateFlow 区别、数据库迁移、性能优化、生命周期感知等常见 Android 问题。通过遵循以上步骤和建议你构建的不仅仅是一个“能跑通”的记账本更是一个体现了现代 Android 开发理念、结构清晰、易于维护和扩展的课程设计项目。它足以让你在期末作业或毕业设计中获得高分并为你的 Android 开发学习之路打下坚实的基础。建议你将此项目作为骨架不断填充和完善功能在实践中深化理解。