Android答题App从零搭建:真机可运行的完整工程

发布时间:2026/10/2 22:40:16
Android答题App从零搭建:真机可运行的完整工程 简介这是一份面向Android初学者与课程设计者的答题类App完整开发项目基于Android Studio实现限时测试、单选题交互、实时判题反馈、成绩统计及错题本管理等核心功能适用于移动应用开发实训、期末项目或自学练手。资源包共50个文件包含12个Java业务逻辑代码、14个XML界面布局与资源定义、10个PNG图标素材、3个Gradle构建配置文件以及APK安装包、MP4演示视频、DOCX说明文档等交付物整体8.81MB结构清晰便于逐模块学习源码与运行调试。已有5867人学习下载提供从环境搭建、代码解读到真机安装的全流程支持配套运行文档详述依赖配置与启动步骤演示视频直观展示答题流程与结果反馈机制特别适合理解Activity生命周期、Fragment切换、SharedPreferences本地存储等Android关键知识点。1. 一个能真正在手机上跑起来的答题App不是Demo是能交作业、能测逻辑、能上线调试的完整Android Studio工程你手头刚配好Android Studio新建项目选了Empty Activity写了两行TextView显示“欢迎答题”然后点Run——模拟器弹出来黑屏三秒后报错ActivityNotFoundException或者更常见的是代码编译通过APK生成成功但安装到真机后点图标直接闪退Logcat里只有一行FATAL EXCEPTION: main连堆栈都没打全。这不是你写错了逻辑而是从项目结构、Activity生命周期绑定、资源路径引用到Manifest声明、权限配置、甚至build.gradle里的minSdkVersion每一步都卡在“能编译”和“能运行”之间那层薄冰上。这篇笔记不讲“Android开发入门”只聚焦一件事用Android Studio从零搭起一个真实可用的答题App——它有题干展示、单选/多选交互、答案提交、得分反馈、本地题库加载JSON格式、离线可用且所有代码能在Android 8.014真机上稳定启动、无白屏、无闪退、无资源找不到错误。适合刚学完Java/Kotlin基础、正卡在“写完代码却跑不起来”阶段的开发者也适合需要快速交付教学实训项目的带教老师。我们不碰网络请求、不接后台、不搞花哨动画就死磕本地答题流程闭环——因为这才是验证你是否真正理解Android组件协作关系的试金石。2. 从空项目到可运行Activity四步构建答题App骨架2.1 创建最小可行项目选对模板比写代码更重要Android Studio 2023.1 新建项目时绝对不要选“Empty Activity”——它默认启用ViewBinding但未在Activity中初始化导致binding ActivityMainBinding.inflate(getLayoutInflater())这行一加就崩溃NullPointerException。正确做法是# 在 Android Studio 中 # 1. File → New → New Project # 2. 选择 Basic Activity不是 Empty # 3. Package name 填 com.example.quizapp务必小写不能含下划线 # 4. Minimum SDK 选 API 21 (Android 5.0) —— 覆盖95%国内机型 # 5. Language 选 Kotlin推荐或 Java本笔记双语注释 # 6. 点击 Finish等待 Gradle 同步完成约1-2分钟为什么选 Basic Activity它自动生成activity_main.xml含AppBarLayoutCoordinatorLayout、MainActivity.kt已预置onCreate()中setContentView(R.layout.activity_main)、以及strings.xml占位符。最关键的是它默认关闭ViewBinding避免新手因绑定时机错乱引发的IllegalStateException。等骨架跑通后再手动启用ViewBinding——这才是可控的演进路径。2.2 替换默认布局用ConstraintLayout实现答题页响应式结构打开app/src/main/res/layout/activity_main.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 !-- 题干显示区 -- TextView android:idid/tvQuestion android:layout_width0dp android:layout_heightwrap_content android:textQ1. 下列哪个是合法的Java标识符 android:textSize18sp android:textStylebold app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintVertical_chainStylepacked / !-- 选项容器垂直链 -- RadioGroup android:idid/radioGroup android:layout_width0dp android:layout_heightwrap_content android:orientationvertical app:layout_constraintTop_toBottomOfid/tvQuestion app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintVertical_bias0.0 RadioButton android:idid/rbOptionA android:layout_widthmatch_parent android:layout_heightwrap_content android:textA. _class android:layout_marginTop8dp / RadioButton android:idid/rbOptionB android:layout_widthmatch_parent android:layout_heightwrap_content android:textB. 123abc android:layout_marginTop4dp / RadioButton android:idid/rbOptionC android:layout_widthmatch_parent android:layout_heightwrap_content android:textC. class android:layout_marginTop4dp / RadioButton android:idid/rbOptionD android:layout_widthmatch_parent android:layout_heightwrap_content android:textD. my-class android:layout_marginTop4dp / /RadioGroup !-- 提交按钮 -- Button android:idid/btnSubmit android:layout_width0dp android:layout_heightwrap_content android:text提交答案 android:layout_marginTop24dp app:layout_constraintTop_toBottomOfid/radioGroup app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / /androidx.constraintlayout.widget.ConstraintLayout关键参数说明app:layout_constraintVertical_chainStylepacked让题干与选项组紧贴避免顶部大片空白android:layout_marginTop8dp/4dp选项间留呼吸感符合Material Design间距规范android:textSize18sp用sp而非dp确保用户调大系统字体时题干可读所有控件ID命名严格遵循tvXxxTextView、rbXxxRadioButton、btnXxxButton前缀为后续Kotlin代码绑定打基础。2.3 编写核心Activity逻辑用Kotlin处理单选提交与状态保存打开app/src/main/java/com/example/quizapp/MainActivity.kt替换全部内容为以下代码含详细注释package com.example.quizapp import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.Button import android.widget.RadioButton import android.widget.RadioGroup import android.widget.TextView import android.widget.Toast class MainActivity : AppCompatActivity() { private lateinit var tvQuestion: TextView private lateinit var radioGroup: RadioGroup private lateinit var btnSubmit: Button private var currentQuestionIndex 0 // 当前题目序号0开始 private var score 0 // 累计得分 // 模拟本地题库题干 正确答案索引0A,1B... private val quizData listOf( QuizItem(Q1. 下列哪个是合法的Java标识符, 0), QuizItem(Q2. Java中String类位于哪个包, 2), QuizItem(Q3. 下列哪个关键字用于定义接口, 3) ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 1. 绑定XML中定义的控件必须在setContentView之后 tvQuestion findViewById(R.id.tvQuestion) radioGroup findViewById(R.id.radioGroup) btnSubmit findViewById(R.id.btnSubmit) // 2. 加载第一题 loadQuestion() // 3. 设置提交按钮点击事件 btnSubmit.setOnClickListener { // 获取当前选中的RadioButton ID val selectedId radioGroup.checkedRadioButtonId if (selectedId -1) { Toast.makeText(this, 请先选择一个答案, Toast.LENGTH_SHORT).show() returnsetOnClickListener } // 根据ID反查是第几个选项A0, B1... val optionIndex when (selectedId) { R.id.rbOptionA - 0 R.id.rbOptionB - 1 R.id.rbOptionC - 2 R.id.rbOptionD - 3 else - -1 } // 判断是否答对 val isCorrect optionIndex quizData[currentQuestionIndex].correctAnswerIndex if (isCorrect) { score Toast.makeText(this, 答对了1分, Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, 答错了正确答案是${A quizData[currentQuestionIndex].correctAnswerIndex}, Toast.LENGTH_SHORT).show() } // 4. 加载下一题或结束 currentQuestionIndex if (currentQuestionIndex quizData.size) { loadQuestion() } else { showResult() } } } private fun loadQuestion() { // 重置RadioGroup选中状态 radioGroup.clearCheck() // 显示当前题干 tvQuestion.text quizData[currentQuestionIndex].question } private fun showResult() { Toast.makeText( this, 测试结束你的得分$score / ${quizData.size}, Toast.LENGTH_LONG ).show() // 此处可跳转到结果页Activity本例简化为Toast } // 内部数据类封装题目信息 data class QuizItem( val question: String, val correctAnswerIndex: Int // 0A, 1B, 2C, 3D ) }逻辑拆解findViewById()必须在setContentView()之后调用否则返回null——这是新手最常翻车点radioGroup.clearCheck()在每次加载新题前清空选中状态避免上一题残留Toast提示用LENGTH_SHORT2秒而非LENGTH_LONG3.5秒防止用户误操作showResult()中未跳转新Activity因本阶段目标是验证主流程避免引入额外复杂度。2.4 验证运行真机调试比模拟器更可靠在手机上启用开发者选项连续点击“关于手机→版本号”7次打开USB调试用USB线连接电脑。Android Studio中点击工具栏 ▶️Run按钮旁的下拉箭头 → 选择你的设备型号如SM-G9730点击 ▶️ 运行观察Gradle构建日志末尾是否出现BUILD SUCCESSFUL in 12s 34 actionable tasks: 34 executed手机自动安装APK并启动看到题干和四个选项即成功。玄学经验若首次运行失败先在手机设置中找到“应用管理→QuizApp→权限”手动开启“存储”权限虽本例未用存储但部分Android 11机型会误判。若仍失败在Android Studio底部点击Logcat标签页筛选com.example.quizapp看是否有Caused by:开头的异常——90%问题在此定位。3. 让答题App真正可用集成本地JSON题库与状态持久化3.1 将题库从硬编码迁移到assets目录下的JSON文件硬编码题库无法扩展且修改需重新编译。正确做法是将题库存为JSON放在assets/目录下运行时动态加载。步骤在app/src/main/下新建文件夹assets右键 → New → Folder → Assets Folder在assets/内创建文件quiz.json内容如下UTF-8编码[ { question: Q1. 下列哪个是合法的Java标识符, options: [_class, 123abc, class, my-class], correctAnswer: 0 }, { question: Q2. Java中String类位于哪个包, options: [java.lang, java.util, java.io, java.net], correctAnswer: 0 }, { question: Q3. 下列哪个关键字用于定义接口, options: [abstract, final, interface, implements], correctAnswer: 2 } ]注意JSON中correctAnswer值为数字0~3对应选项数组索引非字母。3.2 编写JSON解析工具类用Gson库安全反序列化在app/build.gradle的dependencies块中添加Gson依赖dependencies { implementation androidx.core:core-ktx:1.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.code.gson:gson:2.10.1 // 添加这一行 // ... 其他依赖保持不变 }同步Gradle点击右上角 “Sync Now”。然后在com.example.quizapp包下新建data/QuizRepository.ktpackage com.example.quizapp.data import android.content.Context import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.io.InputStreamReader data class QuizItem( val question: String, val options: ListString, val correctAnswer: Int ) class QuizRepository(private val context: Context) { fun loadQuizData(): ListQuizItem { return try { // 从 assets/quiz.json 读取原始流 val inputStream context.assets.open(quiz.json) val reader InputStreamReader(inputStream, UTF-8) // 使用 Gson 解析为 ListQuizItem val type object : TypeTokenListQuizItem() {}.type Gson().fromJson(reader, type) } catch (e: Exception) { // 关键容错JSON解析失败时返回空列表避免App崩溃 e.printStackTrace() emptyList() } } }为什么用TypeTokenGson无法直接解析泛型ListTTypeToken是官方推荐的绕过类型擦除方案。若不用它fromJson()会返回ListMapString, Any需手动转换极易出错。3.3 改造MainActivity注入QuizRepository并动态渲染选项修改MainActivity.kt删除原quizData硬编码注入QuizRepository// 在 MainActivity 类顶部添加 private lateinit var quizRepository: QuizRepository private lateinit var quizData: ListQuizItem // 在 onCreate() 开头添加setContentView之前 quizRepository QuizRepository(this) quizData quizRepository.loadQuizData() if (quizData.isEmpty()) { Toast.makeText(this, 题库加载失败请检查 assets/quiz.json, Toast.LENGTH_LONG).show() return // 防止空题库导致后续空指针 } // 替换 loadQuestion() 方法为 private fun loadQuestion() { radioGroup.clearCheck() tvQuestion.text quizData[currentQuestionIndex].question // 动态设置四个选项文本 val options quizData[currentQuestionIndex].options findViewByIdRadioButton(R.id.rbOptionA).text A. ${options.getOrNull(0) ?: 暂无} findViewByIdRadioButton(R.id.rbOptionB).text B. ${options.getOrNull(1) ?: 暂无} findViewByIdRadioButton(R.id.rbOptionC).text C. ${options.getOrNull(2) ?: 暂无} findViewByIdRadioButton(R.id.rbOptionD).text D. ${options.getOrNull(3) ?: 暂无} } // 修改提交逻辑中的判断 val isCorrect optionIndex quizData[currentQuestionIndex].correctAnswer关键防护getOrNull(0)替代options[0]避免选项不足4个时IndexOutOfBoundsExceptionToast提示题库加载失败而非静默崩溃return提前退出防止quizData为空时currentQuestionIndex越界。3.4 实现答题进度持久化用SharedPreferences记住当前题号用户中途退出App再次打开应从上次位置继续。用SharedPreferences存储currentQuestionIndex// 在 MainActivity 类中添加 private val prefs by lazy { getSharedPreferences(quiz_prefs, Context.MODE_PRIVATE) } private val editor by lazy { prefs.edit() } // 在 onCreate() 结尾添加loadQuestion()之后 currentQuestionIndex prefs.getInt(current_index, 0) if (currentQuestionIndex quizData.size) { currentQuestionIndex 0 // 题库更新后索引越界则重置 } loadQuestion() // 在 btnSubmit 的点击事件结尾添加showResult()之前 editor.putInt(current_index, currentQuestionIndex).apply()为什么用apply()不用commit()apply()异步写入磁盘不阻塞主线程commit()同步执行可能造成UI卡顿。对轻量数据存储apply()是Google官方推荐。4. 避坑指南那些让答题App在真机上集体翻车的5个致命细节4.1 现象App安装后图标不显示桌面找不到入口原因AndroidManifest.xml中activity标签缺少intent-filter或android:exportedtrueAndroid 12强制要求。解决检查app/src/main/AndroidManifest.xml确保MainActivity声明如下activity android:name.MainActivity android:exportedtrue !-- 必须添加此行 -- intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity血泪经验Android Studio 2022.1 新建项目默认添加android:exportedtrue但若你手动修改过Manifest或从旧项目迁移极易遗漏。真机Android 12会直接屏蔽无exported的Activity。4.2 现象题干文字显示为方块□□□或乱码原因assets/quiz.json文件编码不是UTF-8或JSON中中文未转义。解决用VS Code或Notepad打开quiz.json另存为 → 编码选UTF-8无BOM删除JSON中所有\uXXXX形式的Unicode转义直接输入中文Gson支持UTF-8原生中文。4.3 现象点击提交按钮无反应Logcat无任何日志原因Button的android:onClick属性与Activity中方法名不匹配或方法签名错误如缺少View参数。解决绝对不要在XML中写android:onClickonSubmit必须用setOnClickListener{}代码绑定如2.3节所示避免反射调用失败的黑匣子问题。4.4 现象切换横竖屏后题干消失或选项错位原因Android默认销毁并重建Activity但currentQuestionIndex等变量未保存。解决在MainActivity中重写onSaveInstanceState()和onRestoreInstanceState()override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putInt(current_index, currentQuestionIndex) outState.putInt(score, score) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) currentQuestionIndex savedInstanceState.getInt(current_index, 0) score savedInstanceState.getInt(score, 0) }提示此方案比android:configChangesorientation|screenSize更健壮后者需手动处理布局重绘易出错。4.5 现象APK安装失败提示“Parse error: There is a problem parsing the package”原因build.gradle中applicationId包名含大写字母、下划线或以数字开头如com.example.QuizApp或com.example.quiz_app。解决严格使用小写字母点号数字如com.example.quizapp在app/build.gradle中检查applicationId字段确保与AndroidManifest.xml的package属性一致清理项目Build → Clean Project再Build → Rebuild Project。5. 进阶技巧用ViewBinding替代findViewById让代码更安全、更简洁5.1 启用ViewBinding三步开启零反射绑定ViewBinding是Android官方推荐的UI绑定方案比findViewById安全编译期检查ID、比ButterKnife轻量无注解处理器开销。启用步骤在app/build.gradle的android块内添加android { // ... 其他配置 buildFeatures { viewBinding true // 添加这一行 } }同步Gradle修改MainActivity.kt删除所有findViewById()改用Binding// 替换 MainActivity 类定义 class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding // 新增Binding引用 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) // 替代 setContentView() setContentView(binding.root) // root 是 ConstraintLayout 根视图 // 绑定控件用 binding.tvQuestion 替代 findViewByIdTextView(R.id.tvQuestion) binding.tvQuestion.text Q1. ... binding.radioGroup.setOnCheckedChangeListener { _, checkedId - // 处理选项变化 } binding.btnSubmit.setOnClickListener { // 提交逻辑 } } }优势对比方式安全性性能代码量IDE支持findViewById()运行时检查ID不存在则崩溃✅中等⚠️ 无自动补全ViewBinding编译时检查ID不存在直接报错✅✅无反射少30%✅ 全自动补全5.2 处理动态选项用循环替代重复findViewById当选项数不固定如多选题支持3~6个选项硬写rbOptionA/rbOptionB不可维护。用ViewBinding循环// 在 loadQuestion() 中 val options quizData[currentQuestionIndex].options val optionViews listOf( binding.rbOptionA, binding.rbOptionB, binding.rbOptionC, binding.rbOptionD ) // 清空所有选项并设置文本 optionViews.forEachIndexed { index, rb - rb.visibility if (index options.size) View.VISIBLE else View.GONE if (index options.size) { rb.text ${A index}. ${options[index]} } }关键点visibility View.GONE隐藏多余选项比INVISIBLE更省渲染资源forEachIndexed避免手动写4次if-else提升可读性A index自动计算选项字母0→A, 1→B...无需硬编码。5.3 真机调试必装插件ADB WiFi调试与Logcat过滤在办公室WiFi环境下频繁插拔USB线影响效率。用ADB WiFi调试# 1. 手机连USB执行 adb tcpip 5555 # 2. 拔掉USB查看手机IP设置→关于手机→状态信息→IP地址 # 3. 电脑执行替换为你的手机IP adb connect 192.168.1.100:5555 # 4. 验证连接 adb devices # 应显示 192.168.1.100:5555 deviceLogcat过滤技巧在Android Studio Logcat窗口输入com.example.quizapp只看本App日志输入E/AndroidRuntime只看崩溃堆栈输入Toast查看所有Toast提示验证逻辑是否触发。我带过3届实训班学生问得最多的问题永远是“我的代码没错为什么就是不显示”——后来发现90%是没开USB调试、没点信任、或Logcat没过滤。现在我让学生第一课就装好ADB WiFi第二课就学会用E/AndroidRuntime过滤第三课就能自己定位90%的崩溃。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询