
简介这是一款基于Android Studio开发的轻量级安卓答题应用面向移动开发初学者与课程设计实践者解决日常知识测验、题库训练及错题复盘等典型教学场景需求。资源包共50个文件含12个Java业务逻辑代码、14个XML布局与配置文件、10个PNG图标资源辅以3个Gradle构建脚本、1个MP4演示视频、1个APK安装包及1个DOCX说明文档整体8.81MB结构清晰便于快速编译运行与模块化学习。已有5710人下载学习适合Android基础语法、Activity生命周期、SharedPreferences本地存储、计时器控件与简单数据统计等知识点的综合实践。读者可直接安装APK体验限时答题、实时对错反馈、成绩汇总与错题集自动归档功能并通过源码理解题库加载逻辑、答案校验流程与历史记录持久化实现细节。1. 项目概述从零到一打造一个属于自己的答题App最近几年知识付费和在线教育的浪潮一波接一波各种答题、闯关、知识竞赛类的App层出不穷。作为一个在移动开发领域摸爬滚打了十来年的老码农我经常被问到“想学Android开发有没有什么能快速上手、又能做出点成就感的小项目” 我的回答通常是做一个答题App。这玩意儿麻雀虽小五脏俱全从UI界面、数据存储、逻辑交互到网络请求几乎涵盖了Android应用开发的核心知识点。今天我就以“基于Android Studio开发的安卓答题App”为题跟大家掰开揉碎了聊聊如何从零开始一步步把这个项目做出来并且做得扎实、好用。无论你是刚学完基础语法的新手还是想找个完整项目练手的中级开发者这篇文章都能给你提供一条清晰的路径和一堆“踩过坑”的经验。这个项目的核心目标很明确开发一个运行在Android手机上的应用程序用户打开后可以进行答题练习或测试。它可能包含题库管理、随机出题、计时、判分、历史记录等基础功能也可以根据你的兴趣扩展成背单词、驾照考试、知识竞赛等垂直领域应用。我们将完全使用Android Studio这个官方IDE以及Java或Kotlin语言来实现。我会重点讲解设计思路、关键技术的选型理由、具体实现步骤以及那些官方文档里不会写的“坑”和技巧。好了废话不多说我们直接进入正题。2. 项目整体设计与架构思路在动手写第一行代码之前花点时间把整体设计想清楚能省去后期大量的返工和重构。一个典型的答题App我们可以把它拆解成几个核心模块来思考。2.1 核心功能模块拆解首先我们需要明确这个App至少要具备哪些功能。对于一个最小可行产品MVP来说以下四个模块是基石用户界面UI模块这是用户直接交互的部分。主要包括首页/主界面展示入口比如“开始答题”、“查看历史”、“设置”等按钮。答题界面这是核心需要清晰展示题目文字、可能还有图片、选项单选或多选、当前题号、计时器、提交/下一题按钮。结果展示界面答题结束后显示得分、正确率、用时以及每道题的对错回顾。历史记录界面以列表形式展示历次答题的成绩概要。数据模块数据从哪里来存在哪里。这里有两个关键决策数据源题目数据是写死在代码里如一个ListQuestion放在本地数据库如SQLite还是从网络服务器获取使用Retrofit 后端API对于初学者我强烈建议从本地数据开始复杂度最低。数据持久化用户的答题记录、成绩需要保存。SharedPreferences适合存储简单的键值对如最高分、设置项而SQLite数据库或Room库更适合存储结构化的历史记录列表。业务逻辑模块这是App的大脑负责处理题目管理从数据源加载题目管理当前题目索引随机或顺序出题。答题逻辑判断用户选择的答案是否正确计算分数。这里要处理好单选、多选的不同计分规则。状态管理管理答题流程的状态比如是否已开始、是否已结束、当前用时等。这个状态在屏幕旋转配置变更时不能丢失。辅助功能模块提升体验的部分。计时功能使用CountDownTimer或Handler实现正计时或倒计时。交互动效选项选中效果、翻页动画、得分时的庆祝效果等能让App显得更精致。2.2 技术选型与架构模式考量确定了功能接下来要选择用什么样的“工具”和“方法论”来构建。Android开发的世界里选择很多我的建议是基于你的学习阶段和项目目标来定。语言选择Kotlin vs JavaJava传统、稳定资料极多。如果你是从其他编程语言转来或者学校的课程是Java从Java开始完全没问题。KotlinGoogle官方推荐的Android开发语言语法更简洁、安全空安全特性与Java完全兼容。对于新项目我毫无保留地推荐Kotlin。它写起来更爽能避免很多Java里常见的空指针异常崩溃。本文后续的代码示例将主要使用Kotlin。架构模式为什么推荐MVVM早期Android开发经常把UI代码、业务逻辑、数据操作全部塞在Activity或Fragment里被称为MVC或没有架构导致它们非常臃肿难以测试和维护。现代Android开发强烈推荐使用架构模式来解耦。对于答题AppMVVMModel-View-ViewModel是一个绝佳的选择。Model代表数据和业务逻辑。在我们的项目里就是Question数据类、操作题库的Repository仓库、数据库DAO。ViewUI层即Activity和Fragment。它的职责只应该是观察数据变化并更新UI以及把用户输入如点击选项通知给ViewModel。ViewModel作为View和Model之间的桥梁。它持有UI相关的数据并且这些数据是“可观察的”使用LiveData或StateFlow。当Model层的数据变化时ViewModel通知View更新当View有用户操作时ViewModel去调用Model层的逻辑处理。好处实现了关注点分离。Activity/Fragment不再承担逻辑重任变得很薄ViewModel独立于UI生命周期屏幕旋转时数据不会丢失业务逻辑易于单元测试。关键依赖库Gradle配置现代Android开发离不开一些优秀的开源库它们能极大提升开发效率。在app/build.gradle文件的dependencies块里我们很可能会添加dependencies { // 核心扩展 implementation androidx.core:core-ktx:1.12.0 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.8.0 // UI组件 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 // 数据库 - Room implementation androidx.room:room-runtime:2.6.1 kapt androidx.room:room-compiler:2.6.1 implementation androidx.room:room-ktx:2.6.1 // 对Kotlin协程的支持 // 网络请求 (如果后期需要接入网络题库) implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 // 图片加载 (如果题目带图) implementation com.github.bumptech.glide:glide:4.16.0 kapt com.github.bumptech.glide:compiler:4.16.0 }注意版本号请以Android Studio新建项目时自动生成的最新稳定版为准。添加库后记得点击Sync Now同步项目。3. 核心数据模型与本地存储实现数据是答题App的血液。我们先从最简单的开始定义核心的数据结构并实现本地存储。3.1 定义题目与记录实体首先在Kotlin中我们使用data class来定义题目和答题记录它们结构清晰且自动生成有用的方法如equals(),hashCode(),toString()。题目实体 (Question.kt)import androidx.room.Entity import androidx.room.PrimaryKey Entity(tableName questions) data class Question( PrimaryKey(autoGenerate true) val id: Int 0, // 题目ID主键自增 val questionText: String, // 题目正文 val optionA: String, val optionB: String, val optionC: String, val optionD: String, val correctAnswer: String, // 正确答案如 A 或 A,B val explanation: String? null, // 答案解析可选 val category: String? null // 题目分类如“数学”、“历史” ) { // 一个辅助方法判断用户答案是否正确处理多选 fun isAnswerCorrect(userAnswer: String): Boolean { // 简单处理假设正确答案和用户答案都是排序后的字符串如A,B val correctSet correctAnswer.split(,).map { it.trim() }.toSortedSet() val userSet userAnswer.split(,).map { it.trim() }.toSortedSet() return correctSet userSet } }这里我们使用了Room库的注解Entity为后续存入数据库做准备。correctAnswer字段用字符串存储可以兼容单选“A”和多选“A,B”。答题记录实体 (QuizRecord.kt)import androidx.room.Entity import androidx.room.PrimaryKey import java.util.Date Entity(tableName quiz_records) data class QuizRecord( PrimaryKey(autoGenerate true) val recordId: Long 0, val date: Date Date(), // 答题日期 val totalQuestions: Int, // 总题数 val correctCount: Int, // 答对题数 val timeSpent: Long, // 花费时间毫秒 val score: Int // 得分可根据规则计算 )3.2 使用Room构建本地数据库Room是Google官方推荐的SQLite对象映射库它让你用注解和编译时检查来操作数据库比直接写SQLiteOpenHelper省心太多。创建数据访问对象 (DAO)DAO是一个接口定义了访问数据库的方法。Room会在编译时为我们生成具体的实现。import androidx.room.* Dao interface QuestionDao { Query(SELECT * FROM questions) suspend fun getAllQuestions(): ListQuestion Query(SELECT * FROM questions WHERE category :category) suspend fun getQuestionsByCategory(category: String): ListQuestion Insert suspend fun insertAll(vararg questions: Question) Query(DELETE FROM questions) suspend fun deleteAll() } Dao interface QuizRecordDao { Insert suspend fun insertRecord(record: QuizRecord) Query(SELECT * FROM quiz_records ORDER BY date DESC) fun getAllRecords(): LiveDataListQuizRecord // 使用LiveDataUI可以自动观察变化 }注意suspend关键字这表示这是挂起函数需要在协程中调用。LiveData用于观察数据变化当数据库记录更新时所有观察者会自动收到通知。定义数据库类import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import android.content.Context Database(entities [Question::class, QuizRecord::class], version 1, exportSchema false) abstract class AppDatabase : RoomDatabase() { abstract fun questionDao(): QuestionDao abstract fun quizRecordDao(): QuizRecordDao companion object { Volatile private var INSTANCE: AppDatabase? null fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, quiz_database // 数据库文件名 ).build() INSTANCE instance instance } } } }这里使用了单例模式来获取数据库实例避免重复打开数据库造成资源浪费。初始化预置题库数据库建好了但里面是空的。我们可以在App第一次启动时插入一些预置的题目。这可以在一个Repository仓库类中完成。class QuizRepository(private val questionDao: QuestionDao) { suspend fun initializeQuestionsIfNeeded() { val count questionDao.getAllQuestions().size if (count 0) { // 如果题库为空插入初始数据 val initialQuestions listOf( Question( questionText Android官方推荐的开发语言是什么, optionA Java, optionB Kotlin, optionC Dart, optionD C, correctAnswer B, category Android ), Question( questionText Room是用于什么的库, optionA 网络请求, optionB 图片加载, optionC 数据库操作, optionD 依赖注入, correctAnswer C, category Android ) // ... 可以添加更多题目 ) questionDao.insertAll(*initialQuestions.toTypedArray()) } } }实操心得在实际项目中预置题库可能很大直接写在代码里不优雅。可以考虑将题目放在assets目录下的一个JSON文件中App启动时读取并插入数据库。这样管理和更新题库会更方便。4. UI界面设计与MVVM架构搭建有了数据层我们开始构建用户界面并用MVVM模式将它们连接起来。4.1 使用Jetpack Compose还是传统XML这是一个重要的选择。Jetpack Compose是Android新一代声明式UI工具包用Kotlin代码写UI非常强大和现代。但对于初学者或者希望教程受众更广的情况我决定先使用传统的XML布局ViewBinding的方式。因为目前市面上大量的遗留代码和教程仍然是基于XML的掌握它仍然是必要的。当然如果你已经熟悉Compose用它来实现会更具生产力和乐趣。4.2 构建核心答题界面我们创建一个QuizActivity和对应的布局文件activity_quiz.xml。布局文件 (activity_quiz.xml)?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding16dp !-- 顶部状态栏题号、计时器、进度 -- LinearLayout android:idid/top_bar android:layout_width0dp android:layout_heightwrap_content android:orientationhorizontal android:gravitycenter_vertical app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent TextView android:idid/tv_question_index android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text第 1 / 10 题 android:textSize16sp / Chronometer android:idid/chronometer android:layout_widthwrap_content android:layout_heightwrap_content / /LinearLayout !-- 题目正文 -- TextView android:idid/tv_question_text android:layout_width0dp android:layout_heightwrap_content android:layout_marginTop24dp android:textSize20sp android:textStylebold app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/top_bar / !-- 选项区域使用RadioGroup实现单选 -- RadioGroup android:idid/radio_group_options android:layout_width0dp android:layout_heightwrap_content android:layout_marginTop32dp app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_question_text RadioButton android:idid/rb_option_a android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginBottom8dp android:textOption A / !-- 同理添加 RadioButton B, C, D -- RadioButton android:idid/rb_option_b ... / RadioButton android:idid/rb_option_c ... / RadioButton android:idid/rb_option_d ... / /RadioGroup !-- 如果是多选题可以用CheckBox代替RadioGroup这里略过 -- !-- 底部操作按钮 -- Button android:idid/btn_previous android:layout_widthwrap_content android:layout_heightwrap_content android:text上一题 app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent / Button android:idid/btn_submit_or_next android:layout_widthwrap_content android:layout_heightwrap_content android:text下一题 app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent / Button android:idid/btn_finish android:layout_widthwrap_content android:layout_heightwrap_content android:text交卷 app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toStartOfid/btn_submit_or_next app:layout_constraintStart_toEndOfid/btn_previous / /androidx.constraintlayout.widget.ConstraintLayout这是一个非常基础的布局使用了ConstraintLayout来定位。实际项目中你可能需要为选项添加更美观的卡片背景并处理好不同屏幕尺寸的适配。创建ViewModel (QuizViewModel.kt)ViewModel负责管理答题界面的所有状态和逻辑。import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class QuizViewModel(private val repository: QuizRepository) : ViewModel() { // 可观察的数据 private val _questions MutableLiveDataListQuestion() val questions: LiveDataListQuestion _questions private val _currentQuestionIndex MutableLiveData(0) val currentQuestionIndex: LiveDataInt _currentQuestionIndex private val _userAnswers mutableMapOfInt, String() // 题号索引 - 用户答案 private val _timeElapsed MutableLiveData(0L) // 毫秒 private val _quizFinished MutableLiveData(false) val quizFinished: LiveDataBoolean _quizFinished // 初始化加载题目 init { loadQuestions() } private fun loadQuestions() { viewModelScope.launch { // 这里可以从Repository获取题目比如随机10道 val allQuestions repository.getRandomQuestions(10) // 假设Repository有这个方法 _questions.value allQuestions } } fun getCurrentQuestion(): Question? { return questions.value?.getOrNull(currentQuestionIndex.value ?: 0) } fun submitAnswer(answer: String) { val index currentQuestionIndex.value ?: 0 _userAnswers[index] answer } fun moveToNextQuestion() { val current currentQuestionIndex.value ?: 0 val total questions.value?.size ?: 0 if (current total - 1) { _currentQuestionIndex.value current 1 } else { // 已经是最后一题 finishQuiz() } } fun moveToPreviousQuestion() { val current currentQuestionIndex.value ?: 0 if (current 0) { _currentQuestionIndex.value current - 1 } } private fun finishQuiz() { _quizFinished.value true // 计算分数保存记录到数据库 calculateAndSaveRecord() } private fun calculateAndSaveRecord() { viewModelScope.launch { val qList questions.value ?: returnlaunch var correctCount 0 qList.forEachIndexed { index, question - val userAnswer _userAnswers[index] if (userAnswer ! null question.isAnswerCorrect(userAnswer)) { correctCount } } val record QuizRecord( totalQuestions qList.size, correctCount correctCount, timeSpent _timeElapsed.value ?: 0, score (correctCount.toDouble() / qList.size * 100).toInt() ) repository.saveQuizRecord(record) } } fun updateTime(elapsedMillis: Long) { _timeElapsed.value elapsedMillis } }ViewModel通过LiveData暴露数据Activity/Fragment观察这些数据。所有对数据的修改如翻页、答题都通过ViewModel的方法进行保证了数据流的单向性和可预测性。Activity中的绑定与观察 (QuizActivity.kt)Activity的职责变得非常清晰初始化UI绑定ViewModel观察数据变化并更新UI。import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.os.SystemClock import androidx.activity.viewModels import androidx.lifecycle.Observer class QuizActivity : AppCompatActivity() { // 使用viewModels委托属性来获取ViewModel实例 private val viewModel: QuizViewModel by viewModels { // 如果需要传递参数给ViewModelFactory可以在这里自定义 QuizViewModelFactory((application as QuizApplication).repository) } private lateinit var binding: ActivityQuizBinding // ViewBinding实例 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityQuizBinding.inflate(layoutInflater) setContentView(binding.root) setupObservers() setupClickListeners() startTimer() } private fun setupObservers() { // 观察当前题目变化更新UI viewModel.currentQuestionIndex.observe(this, Observer { index - updateQuestionUI() }) // 观察题目列表 viewModel.questions.observe(this, Observer { questions - if (questions.isNotEmpty()) { updateQuestionUI() } }) // 观察答题是否结束 viewModel.quizFinished.observe(this, Observer { finished - if (finished) { // 跳转到结果页面 val intent Intent(this, ResultActivity::class.java).apply { // 可以传递分数等数据 } startActivity(intent) finish() } }) } private fun updateQuestionUI() { val question viewModel.getCurrentQuestion() ?: return binding.tvQuestionText.text question.questionText binding.rbOptionA.text question.optionA binding.rbOptionB.text question.optionB binding.rbOptionC.text question.optionC binding.rbOptionD.text question.optionD // 更新题号显示 val index (viewModel.currentQuestionIndex.value ?: 0) 1 val total viewModel.questions.value?.size ?: 0 binding.tvQuestionIndex.text 第 $index / $total 题 // 恢复用户之前的选择如果有 val savedAnswer viewModel.getUserAnswerForCurrentQuestion() // ... 根据savedAnswer设置RadioGroup的选中状态 } private fun setupClickListeners() { binding.btnNext.setOnClickListener { // 获取当前选择的答案 val selectedId binding.radioGroupOptions.checkedRadioButtonId val answer when (selectedId) { R.id.rb_option_a - A R.id.rb_option_b - B R.id.rb_option_c - C R.id.rb_option_d - D else - } if (answer.isNotEmpty()) { viewModel.submitAnswer(answer) } viewModel.moveToNextQuestion() } binding.btnPrevious.setOnClickListener { viewModel.moveToPreviousQuestion() } binding.btnFinish.setOnClickListener { viewModel.finishQuiz() } } private fun startTimer() { binding.chronometer.base SystemClock.elapsedRealtime() binding.chronometer.start() binding.chronometer.setOnChronometerTickListener { val elapsedMillis SystemClock.elapsedRealtime() - it.base viewModel.updateTime(elapsedMillis) } } }注意事项Chronometer的base时间是基于系统启动后的毫秒数。在onCreate中设置base为当前时间然后start()它就会从0开始正计时。通过OnChronometerTickListener我们可以获取到经过的毫秒数并同步给ViewModel。这里有个坑如果Activity因为配置变更如旋转屏幕重建Chronometer会重新从0开始。为了避免这个问题我们需要在ViewModel中保存timeElapsed并在Activity重建时重新设置chronometer.base SystemClock.elapsedRealtime() - timeElapsed。5. 高级功能实现与性能优化基础功能跑通后我们可以考虑添加一些提升体验和健壮性的功能。5.1 实现题目分类与随机抽题一个实用的答题App应该支持按分类练习和随机出题。这需要在Repository和ViewModel中增加逻辑。在Repository中添加方法class QuizRepository(private val questionDao: QuestionDao) { // ... 其他代码 suspend fun getQuestionsByCategory(category: String): ListQuestion { return questionDao.getQuestionsByCategory(category) } suspend fun getRandomQuestions(limit: Int): ListQuestion { val allQuestions questionDao.getAllQuestions() return allQuestions.shuffled().take(limit) } suspend fun getRandomQuestionsByCategory(category: String, limit: Int): ListQuestion { val categoryQuestions questionDao.getQuestionsByCategory(category) return categoryQuestions.shuffled().take(limit) } }shuffled()是Kotlin标准库函数用于随机打乱列表顺序。在ViewModel中提供选择可以在QuizViewModel的初始化中增加参数或者在开始答题前调用一个startQuiz(category: String?, isRandom: Boolean)方法来决定加载题目的策略。5.2 支持多选题与复杂判分逻辑之前的例子只处理了单选题。多选题需要改变UI使用CheckBox和判分逻辑。修改Question实体和判分逻辑我们的Question实体已经用逗号分隔的字符串存储了正确答案如A,CisAnswerCorrect方法也做了集合比较这本身就支持了多选题。关键在于UI层如何收集用户的多选答案。创建多选题布局片段我们可以创建一个新的Fragment例如MultipleChoiceFragment或者动态切换布局。布局中使用四个CheckBox。在Activity/Fragment中收集多选答案// 假设有四个CheckBox: cbOptionA, cbOptionB, ... val selectedOptions mutableListOfString() if (binding.cbOptionA.isChecked) selectedOptions.add(A) if (binding.cbOptionB.isChecked) selectedOptions.add(B) // ... 同理C, D val userAnswer selectedOptions.sorted().joinToString(,) // 排序后拼接成A,C格式 viewModel.submitAnswer(userAnswer)5.3 应用主题、动画与用户体验优化应用Material Design主题在res/values/themes.xml中定义你的App主题使用MaterialComponents主题可以轻松获得现代化的外观和统一的组件样式。style nameTheme.QuizApp parentTheme.MaterialComponents.DayNight.NoActionBar !-- 自定义颜色 -- item namecolorPrimarycolor/purple_500/item item namecolorPrimaryVariantcolor/purple_700/item item namecolorOnPrimarycolor/white/item !-- 其他属性 -- /style在AndroidManifest.xml的application标签中应用这个主题。添加简单的转场动画在跳转到结果页面时可以添加Activity切换动画。在res/anim目录下创建动画资源文件如slide_in_right.xml,slide_out_left.xml然后在startActivity后调用overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)。使用ViewBinding/DataBinding减少样板代码如上例所示使用ViewBinding可以安全且方便地访问视图避免了findViewById。对于更复杂的双向绑定如直接将LiveData绑定到TextView可以考虑使用DataBinding但这会引入更高的复杂度。5.4 性能与内存优化要点避免在UI线程进行数据库/网络操作所有耗时操作如suspend函数都必须在协程如viewModelScope.launch或后台线程中执行。图片优化如果题目包含图片务必使用Glide或Coil这样的图片加载库。它们会自动处理缓存、压缩、生命周期管理。RecyclerView的使用在历史记录列表界面务必使用RecyclerView来展示列表而不是在ScrollView里动态添加视图。RecyclerView会回收不可见的视图项对于长列表有巨大的性能优势。LiveData与生命周期在Activity/Fragment中观察LiveData时传入thisLifecycleOwner作为参数。这样LiveData会自动在界面销毁时移除观察者防止内存泄漏。数据库事务当需要插入大量预置数据时使用Room的Transaction注解来包裹插入操作可以显著提升速度。6. 测试、调试与常见问题排查开发过程中测试和调试是保证质量的关键环节。6.1 单元测试ViewModelViewModel是纯Kotlin类不依赖Android框架非常适合做单元测试。你可以使用JUnit和Mockito等框架。import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.* import org.mockito.junit.MockitoJUnitRunner RunWith(MockitoJUnitRunner::class) class QuizViewModelTest { Test fun submitAnswer should save answer() { // 1. 创建Mock Repository val mockRepo mock(QuizRepository::class.java) // 2. 创建ViewModel注入Mock val viewModel QuizViewModel(mockRepo) // 3. 模拟数据 val testQuestion Question(id1, questionTextTest, optionAA, optionBB, optionCC, optionDD, correctAnswerA) when(mockRepo.getRandomQuestions(anyInt())).thenReturn(listOf(testQuestion)) // 4. 触发操作这里需要想办法触发loadQuestions可能需要重构ViewModel使其可测试 // viewModel.loadQuestions() // 5. 验证结果 // verify(mockRepo).saveQuizRecord(any()) } }注意为了可测试性ViewModel的构造函数应该注入依赖如Repository而不是在内部直接实例化。这样在测试时就可以注入Mock对象。6.2 Android UI测试使用Espresso框架进行界面测试模拟用户点击、输入等操作。RunWith(AndroidJUnit4::class) class QuizActivityTest { get:Rule val activityRule ActivityScenarioRule(QuizActivity::class.java) Test fun selectAnswerAndGoNext() { // 检查初始题号 onView(withId(R.id.tv_question_index)).check(matches(withText(containsString(第 1 /)))) // 点击第一个选项 onView(withId(R.id.rb_option_a)).perform(click()) // 点击下一题按钮 onView(withId(R.id.btn_next)).perform(click()) // 检查题号是否更新为第2题 onView(withId(R.id.tv_question_index)).check(matches(withText(containsString(第 2 /)))) } }6.3 常见问题与排查技巧实录Room数据库编译报错Cannot find setter for field问题实体类Entity中的字段是val只读属性Room在编译时需要生成setter方法。解决确保实体类中的字段是var可变属性或者为val字段提供正确的getter。通常将主键id设为val其他字段设为var即可。LiveData观察不到数据变化问题在Fragment中观察LiveData但回调从未触发。排查检查LiveData的value是否真的被改变了在ViewModel中使用_currentQuestionIndex.value newValue。确保在onCreate()或onViewCreated()中调用observe()并且传入的LifecycleOwner是正确的在Activity中用this在Fragment中用viewLifecycleOwner。检查是否在后台线程更新了LiveData的值。如果是在协程中更新确保使用了postValue()如果不在主线程或直接赋值如果在主线程或viewModelScope中。屏幕旋转后数据丢失问题Activity重建后界面状态如当前题号、用户已选答案恢复到了初始状态。解决关键数据应保存在ViewModel中。ViewModel的生命周期比Activity长配置变更时不会销毁。UI控件的瞬时状态如EditText的输入、RadioButton的选中状态可以通过onSaveInstanceState()和onRestoreInstanceState()来保存恢复或者使用Android的SavedStateHandle与ViewModel结合使用。按钮点击无反应或应用崩溃排查步骤查看Logcat这是最直接的途径。崩溃会有红色的异常堆栈信息明确指示错误行和原因如空指针、主线程网络操作。检查ViewBinding确保binding变量已正确初始化在setContentView之前或之后调用inflate并且控件ID与布局文件中的一致。检查点击事件绑定确认setOnClickListener被正确调用并且引用的按钮ID无误。使用断点调试在Android Studio中在可疑代码行左侧点击设置断点然后以Debug模式运行App。程序执行到断点时会暂停你可以查看所有变量的当前值一步步执行这是定位逻辑错误的神器。生成APK文件过大原因默认的Debug包包含了很多调试信息并且支持所有CPU架构armeabi-v7a, arm64-v8a, x86, x86_64。优化在build.gradle中配置abiFilters只打包你需要的架构通常只需armeabi-v7a, arm64-v8a。使用R8/ProGuard代码混淆和资源压缩。对图片等资源进行压缩。正式发布时使用Build - Generate Signed Bundle / APK来生成Release版本。开发这样一个完整的答题App就像搭积木从数据到逻辑再到界面一层层构建起来。过程中你会遇到各种问题但每一个问题的解决都会让你对Android开发的理解更深一层。我强烈建议你在实现基础功能后自己尝试添加新特性比如夜间模式、题目收藏、错题本、分享成绩到社交媒体甚至接入在线题库API。这些挑战会让你学到更多。最后记住多写、多试、多查文档官方Android Developers文档和Kotlinlang.org是你的好朋友遇到问题善用搜索引擎和Stack Overflow但更重要的是理解解决方案背后的原理。本文还有配套的精品资源点击获取