Android Studio简易计算器课设:状态机、生命周期与避坑实践
发布时间:2026/11/2 8:51:44 锦皓数字建站

简介这是一份面向K12阶段学习者与Android初学者的期末大作业项目包围绕Android Studio简易计算器展开可用于课程作业提交、课堂演示或入门练习。压缩包共48个文件、约180KB以xml布局与配置、java源码、gradle构建脚本、properties配置及png界面素材为主完整呈现一个可导入IDE的工程结构。已有4342人学习说明其作为入门案例具有较高参考度。项目覆盖主活动页面组织、界面控件属性设置、按钮点击响应、独立计算类封装、基于栈的运算符优先级判断、结果回显、模拟器运行、日志排查与APK导出等环节。读者可据此理解Android应用从界面布局到打包发布的基本流程并借鉴其中的模块划分、基础输入校验与调试排错思路适合需要完成期末答辩、课堂实践或课后拓展的人群参考也可作为教师布置课程设计的参考样例。1. 安卓课设里那个最不起眼的计算器其实最容易翻车每年期末季安卓课设选题里总有一个绕不开的名字简易计算器。很多同学的第一反应是这也太简单了吧随手拖几个按钮、写个 - × ÷就交上去了。真上手才发现不是那么回事——连续按123之后结果乱跳、除零直接崩溃、横竖屏一转数字全没了、连点等号还会无限累加。这些坑几乎每一届都有人踩。这篇就围绕一个 Android Studio 实现的简易计算器课设项目把它的整体结构、核心逻辑、跑起来的方法和二次改造思路拆开讲清楚。关键词 android k12 放在这里的意思是它足够基础适合刚接触 Android Studio 的初学者当第一个完整练手项目但它的状态机、UI 绑定、生命周期处理又足够典型能撑起一份看得过去的课设答辩。如果你正被选题太简单怕老师嫌弃、选题太难怕做不完卡住这个项目值得先跑通一遍。2. 项目结构与技术选型一个单 Activity 的工程长什么样计算器这类工具型 App 有个天然优势它不需要网络、不需要数据库、不需要多页面跳转一个 Activity 就能闭环。先把这套工程骨架看懂后面改功能才不会迷路。2.1 为什么选单 Activity 原生 View 而不是 Compose这个项目的技术栈是最经典的一套Java 或 Kotlin 原生 XML 布局 AppCompatActivityButton/TextView控件构建工具是 Gradle开发环境 Android Studio。没有引入任何第三方依赖。为什么不用 Jetpack Compose对课设场景来说理由很实在。第一绝大多数学校的 Android 课程大纲还是按 View 体系讲的老师看得懂 XML、看得懂findViewById答辩时能对上话。第二Compose 的声明式 UI 需要理解状态重组初学者很容易写出数据变了界面不刷新或者重组导致重复计算的问题反而增加调试成本。第三纯原生 View 的实现代码量集中、调用链短出问题一眼能定位。提示如果你的课程明确要求 Kotlin 或 Compose那就按课程来别为了技术新跟自己过不去。工具型 App 的技术选型稳定交付比技术时髦重要得多。原生的 View 方案对应到代码上就是三个角色XML 负责长什么样Activity 负责怎么响应一个计算逻辑类负责算什么。这个三层划分不是强制的但强烈建议这么分因为把计算逻辑塞进onClick里是后面所有维护噩梦的起点。2.2 目录结构文件该放哪儿Android Studio 新建 Empty Views Activity 之后工程目录大致是这样这个计算器项目基本不做额外扩展app/src/main/ ├── java/com/example/calculator/ │ ├── MainActivity.java # 唯一的 Activity负责 UI 绑定与事件分发 │ └── CalculatorEngine.java # 计算核心纯 Java 类不依赖 Android ├── res/ │ ├── layout/ │ │ └── activity_main.xml # 计算器主界面布局 │ ├── values/ │ │ ├── colors.xml # 配色 │ │ ├── strings.xml # 文案按钮文字建议抽到这里 │ │ └── themes.xml # 主题 │ └── mipmap-*/ # 图标资源 └── AndroidManifest.xml # 声明 Activity 与屏幕方向配置CalculatorEngine单独成类是这套结构里最关键的一个决定。它不 import 任何android.*包意味着你可以在普通 Java 环境里直接写单元测试不用起模拟器。课设答辩时如果老师问你的核心逻辑怎么验证的能拿出一个脱离 Android 的纯逻辑类是加分项。布局文件这边主流写法有两种LinearLayout嵌套GridLayout或者直接用GridLayout铺 4 列。按钮数量是标准的 4 行 5 列排布——数字 0-9、小数点、四则运算符、等号、清除、退格一共 20 个左右的按钮。!-- activity_main.xml 核心片段顶部显示区 4 行 5 列按钮区 -- LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding12dp TextView android:idid/tvExpression android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:gravityend|bottom android:textSize28sp android:maxLines2 android:text0 / TextView android:idid/tvResult android:layout_widthmatch_parent android:layout_heightwrap_content android:gravityend android:textSize40sp android:textStylebold android:text / GridLayout android:layout_widthmatch_parent android:layout_height0dp android:layout_weight3 android:columnCount5 android:rowCount4 Button android:idid/btnClear android:textC / Button android:idid/btnBack android:text⌫ / Button android:idid/btnPercent android:text% / Button android:idid/btnDiv android:text÷ / Button android:idid/btnMul android:text× / !-- 其余行按同样方式铺满 20 个按钮 -- /GridLayout /LinearLayout这里有两个细节值得说清楚。一是用了两个TextView上面一行tvExpression显示完整算式比如1234下面一行tvResult显示当前输入或结果。很多同学只用一个 TextView结果输入中和已计算两种状态混在一起退格和连按等号的逻辑就全乱了。二是GridLayout的宽高用0dp layout_weight撑满剩余空间这样在不同屏幕尺寸上按钮会自动缩放比你写死dp值适配性好得多。按钮的尺寸和样式建议统一抽到styles.xml里否则 20 个按钮每个写一遍layout_width、layout_height、textSize改一次配色要改 20 个地方。2.3 事件分发一个 onClick 还是二十个监听器按钮绑事件有两条路。第一条是每个按钮findViewById之后单独setOnClickListener20 个按钮就是 20 段几乎一样的代码肉眼可见的重复。第二条是让MainActivity实现View.OnClickListener在onClick(View v)里用v.getId()分支处理。这个项目用的是第二条代码量能压下来一大截。// MainActivity 实现 OnClickListener统一入口分发 public class MainActivity extends AppCompatActivity implements View.OnClickListener { private final int[] digitIds { R.id.btn0, R.id.btn1, R.id.btn2, R.id.btn3, R.id.btn4, R.id.btn5, R.id.btn6, R.id.btn7, R.id.btn8, R.id.btn9 }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 批量绑定数字键 运算符 功能键 for (int id : digitIds) findViewById(id).setOnClickListener(this); findViewById(R.id.btnPlus).setOnClickListener(this); findViewById(R.id.btnMinus).setOnClickListener(this); findViewById(R.id.btnEqual).setOnClickListener(this); findViewById(R.id.btnClear).setOnClickListener(this); } Override public void onClick(View v) { int id v.getId(); if (id R.id.btnClear) { resetAll(); // 清空算式与结果 } else if (id R.id.btnEqual) { doCalculate(); // 触发一次完整计算 } else { appendToken(((Button) v).getText().toString()); // 其余按键统一追加 } } }这段代码的思路是把按钮的文本当作输入符号。不管按的是7还是都取出Button上的文字往表达式缓冲里追加等于把 20 个分支压缩成了一个默认分支。好处是新增一个运算符按钮时只要在 XML 里加控件、在onCreate里绑监听一行分支都不用改。参数说明一下digitIds数组是为了批量绑定数字键避免写 10 行findViewByIdappendToken是自定义方法内部要处理结果状态下按数字应该重新开始而不是拼接这类边界这个下面细讲。注意((Button) v).getText()的前提是这些按钮的text属性就是你要输入的字符——如果界面上显示的是×而逻辑里需要*就得在appendToken里做一次符号映射这是个常见的隐藏 bug 点。3. 核心逻辑拆解表达式缓冲与结果计算界面搭好只是壳子计算器真正的难点全在状态两个字上。用户按键是个有顺序的过程你得知道当前是在输入第一个数还是第二个数、刚按完等号还是刚按完运算符。这套逻辑理不清就会掉进连按等号无限累加、运算符连按报错这些经典坑里。3.1 用字符串缓冲还是用状态机两种实现路线主流实现有两条路线各有取舍。第一条是表达式字符串 解析求值。维护一个StringBuilder缓冲区把用户按的每个字符拼进去按等号时用一个解析器自己写的简易解析或ScriptEngine这类脚本引擎算出结果。优点是实现直观、能天然支持12×3这种带优先级的算式缺点是自己写解析器有难度用脚本引擎又太重、不适合课设。第二条是双操作数 当前运算符的状态机也就是只支持a op b这种二元运算按一次等号算一次。优点是逻辑极简、代码量小、几乎不出错缺点是不支持连续混合运算的优先级12×3会算成 9 而不是 7。这个课设项目采用第二条路线理由是课程要求通常只到能完成四则运算这一层双操作数模型完全够用而且状态机模型讲得清楚答辩时能画出状态流转比我调用了一个解析库更有说服力。如果你们老师的要求里明确写了支持运算优先级那就得切到第一条路线或者用两个栈操作数栈 运算符栈手写调度场算法。状态机的核心是三个变量// CalculatorEngine纯逻辑类不依赖任何 Android API public class CalculatorEngine { private String currentInput 0; // 当前正在输入的数字串 private double operandA 0; // 暂存的左操作数 private String pendingOp null; // 暂存的运算符null 表示无 private boolean justEvaluated false; // 上一步是否是按了等号 // 追加一个字符数字或小数点 public void appendDigit(String token) { if (justEvaluated) { // 刚算完结果再按数字应当开启新算式而不是拼接旧结果 currentInput 0; operandA 0; pendingOp null; justEvaluated false; } if (0.equals(currentInput)) { currentInput token; // 去掉前导零 } else { currentInput token; } } // 按运算符若已有待处理运算符先结算再存入新运算符 public void applyOperator(String op) { if (pendingOp ! null) { operandA compute(operandA, Double.parseDouble(currentInput), pendingOp); } else { operandA Double.parseDouble(currentInput); } pendingOp op; currentInput 0; } // 按等号结算并清空待处理运算符 public String equals() { if (pendingOp null) return currentInput; double b Double.parseDouble(currentInput); double result compute(operandA, b, pendingOp); operandA result; currentInput format(result); pendingOp null; justEvaluated true; // 标记进入结果状态 return currentInput; } private double compute(double a, double b, String op) { switch (op) { case : return a b; case -: return a - b; case ×: return a * b; case ÷: if (b 0) throw new ArithmeticException(除数不能为零); return a / b; default: return b; } } }逐段说明。currentInput用字符串而不是 double是为了保留用户正在输入的原始样子——比如用户输入1.或者0.50直接转 double 会丢格式显示出来就变成1.0和0.5体验很差。operandA和pendingOp是状态机的两个记忆槽按运算符时把当前数存进operandA、把运算符存进pendingOp然后currentInput归零准备接收第二个数。justEvaluated这个布尔量是全篇最容易被忽略、又最影响体验的一个变量。它解决的是按完等号之后接着按数字这个场景没有它用户算完112之后按3会变成23因为缓冲区里还留着2。加上这个标记按完等号后任何数字输入都会重置整个状态开启一道新算式。applyOperator里那个若已有待处理运算符先结算的判断是为了支持连续运算123按第二个加号时先把12算出来存回operandA再存入新的加号。这就是很多同学不写会出错的连续按运算符场景。3.2 等号、清空、退格与小数点四个高频边界核心状态机搭好后四个功能键各自的边界处理决定了这个计算器是能用还是好用。等号。除了上面equals()里做的结算还要处理连按等号的情况。真实计算器的行为是23得到 5再按一次得到 8重复上一次运算再按得 10。要支持这个需要额外记住上一次的运算符和右操作数。课设如果不要求最简单的做法是第二次按等号时直接返回当前结果不变避免出现operandA被反复覆盖导致的乱跳。清空C。注意区分CClear和ACAll Clear前者只清当前输入后者清全部状态。简易计算器用一个resetAll()把四个变量全部归位就够但要把 UI 上的两个 TextView 也同步重置否则会出现逻辑已清空、界面还显示旧结果的不一致。退格⌫。这是最容易被跳过、又最容易被老师点名的功能。处理时要判断如果justEvaluated为真说明当前显示的是计算结果退格应当无效或者直接清空否则对currentInput做substring(0, length-1)如果删到空串就补回0。// 退格直接操作 currentInput 字符串尾部 public String backspace() { if (justEvaluated) { return currentInput; // 结果状态不允许退格 } if (currentInput.length() 1) { currentInput currentInput.substring(0, currentInput.length() - 1); } else { currentInput 0; // 删到只剩一位时回退到 0 } if (-.equals(currentInput)) currentInput 0; // 防御性处理 return currentInput; }小数点。要防的是一个数里出现两个小数点1.2.3这种输入必须被拦截。判断逻辑是检查currentInput里是否已包含.包含就直接 return 忽略这次按键。另外0状态下按小数点应该变成0.而不是.或00.这个初始态判断也要写上。把这四个边界处理好这个计算器在体验上就和手机自带的有八成接近了答辩演示时不会被追问到卡壳。4. 跑起来与避坑从 clone 到按下第一个等号代码看着都懂真在 Android Studio 里跑起来问题往往出在环境和配置上。这一章按实际操作顺序走一遍顺便把几个血泪坑点提前摆出来。4.1 环境准备与运行步骤前置条件就三样装好 Android Studio建议用近两年的稳定版、一个能下载的 SDK 平台项目compileSdk对应的版本常见是 33 或 34、一个可用设备。设备首选真机开 USB 调试次选模拟器因为模拟器的键盘输入和触摸落点有时和真机表现不一致而计算器恰恰是纯触摸交互。操作步骤按顺序来Android Studio 里File → Open选中项目根目录有settings.gradle的那一层不要选到app子目录这是新手第一个翻车点。打开后等待 Gradle Sync 完成底部进度条走完、没有红色报错再进行下一步。如果卡在下载依赖检查网络和 Gradle 版本匹配。检查local.properties里的sdk.dir是否指向你本机的 SDK 路径这个文件通常不入版本库缺了会直接构建失败。连设备或启动模拟器在顶部设备下拉框选中目标设备点绿色三角运行。装好后先按1、、2、看到屏幕显示3就算跑通没显示就去 Logcat 看有没有异常堆栈。构建这条命令行也常用出问题时比点按钮更容易看清报错# 编译并安装 debug 包到已连接设备-i 输出日志便于排查 ./gradlew installDebug -i # 只做编译不安装快速验证代码能否通过编译 ./gradlew assembleDebuginstallDebug会先编译、再通过 adb 推送到设备-i参数把 Gradle 的 info 级日志打出来定位编译过了但安装失败这类问题很有效。assembleDebug只产出 APK路径在app/build/outputs/apk/debug/下答辩前打一个装到演示机上比现场连电脑稳。4.2 避坑清单五个真会卡住你的问题现象一旋转屏幕后计算结果消失回到初始0。原因是 Activity 默认在配置变更横竖屏切换时销毁重建CalculatorEngine里那些字段跟着一起没了。解决有两条路一是把计算器的状态变量在onSaveInstanceState里存、onCreate里恢复二是在AndroidManifest.xml的activity上加android:configChangesorientation|screenSize让系统不重建 Activity。课设推荐后者简单直接但要知道这只是回避了问题正式项目建议用ViewModel承载状态。现象二连点等号结果数字越变越大或者乱跳。原因是equals()每次都被调用而operandA在第一次计算后已经被替换成结果第二次按等号时又拿它和currentInput算了一遍。解决方案是加个justEvaluated判断第二次按等号时直接返回当前显示值要支持重复上次运算就得额外存一份lastOp和lastOperand。现象三除以零直接闪退。原因是compute里a / b在b为 0 时整型会抛ArithmeticException浮点会得到Infinity或NaN然后format显示成一个奇怪的字符串。解决办法是在compute里显式判b 0并抛出可捕获的异常在MainActivity里 try-catch 之后弹一个 Toast 提示除数不能为零然后把状态重置。现象四结果出现0.30000000000000004这种长尾。这是浮点数的经典问题0.1 0.2不等于0.3。别去分析二进制表示直接用格式化处理String.format(%.10f, result)之后再stripTrailingZeros去掉尾部的零或者用BigDecimal做运算。课设场景用前者几行就够。现象五按钮文字是×但计算结果是错的或者按×没反应。原因是布局里显示的是全角符号×、÷而compute里 switch 分支写的是*、/两边对不上走到default就把第二个数原样返回了。这就是 2.3 里提到的符号映射问题解决办法是在appendToken或applyOperator入口统一把显示符号转成逻辑符号只在一处做映射别散落在各处。注意改完布局 XML 之后如果界面没变化先试Build → Clean Project再重跑缓存不刷新是 Android Studio 的老毛病别急着怀疑代码。5. 二次开发把课设作业改成能写进简历的小工具跑到这一步一个能用的计算器已经有了。但课设的真正价值不在于做完而在于改得动。我给的建议是答辩前至少做一到两个扩展让项目从和别人一模一样里跳出来。这里说三个成本低、见效明显的改造方向以及验证改造是否成功的方法。5.1 三个可落地的改造方向方向一历史记录列表。用RecyclerView在结果区下方加一个可展开的历史面板每次按等号把算式 结果存进ListString。这个改造的意义不在功能本身而在于它逼你把计算逻辑和UI 刷新解耦——你会发现CalculatorEngine必须能对外通知结果而不是让 Activity 每次算完自己拼字符串。顺带练了 Adapter 和列表复用答辩时能讲的东西立刻多了一倍。数据持久化如果要加分就用SharedPreferences存 JSON 字符串别上数据库小题大做。方向二加入运算优先级。把CalculatorEngine从双操作数状态机升级成操作数栈 运算符栈。核心就两个方法遇到运算符时若栈顶运算符优先级不低于当前运算符就弹出计算否则压栈遇到等号时把栈里剩余的全部算完。这就是调度场算法的简化版代码量在 80 行左右是这份课设里最能体现算法功底的部分。改之前先把旧的equals()用单元测试锁住改完跑一遍对比避免新逻辑把旧功能改坏。方向三主题切换与深色模式。把colors.xml拆成values/colors.xml和values-night/colors.xml两套所有按钮背景、文字颜色都引用color/xxx而不是写死#FFFFFF。这个改造看着是 UI 活儿实际上考的是资源引用规范——很多同学的布局里颜色写死、尺寸写死一做深色模式就全崩趁这个机会把硬编码清理一遍是很好的工程习惯训练。5.2 改造后的验证方法改完别只靠手点容易漏。下面这段纯 Java 单元测试不需要模拟器跑在 JVM 上就行建议每改一次核心逻辑就跑一遍// CalculatorEngineTest验证核心状态机不依赖 Android 环境 public class CalculatorEngineTest { Test public void testSimpleAdd() { CalculatorEngine e new CalculatorEngine(); e.appendDigit(1); e.applyOperator(); e.appendDigit(2); assertEquals(3, e.equals()); } Test public void testChainedOperator() { // 1 2 3 应得到 6验证 applyOperator 里的提前结算 CalculatorEngine e new CalculatorEngine(); e.appendDigit(1); e.applyOperator(); e.appendDigit(2); e.applyOperator(); e.appendDigit(3); assertEquals(6, e.equals()); } Test(expected ArithmeticException.class) public void testDivideByZero() { CalculatorEngine e new CalculatorEngine(); e.appendDigit(5); e.applyOperator(÷); e.appendDigit(0); e.equals(); // 期望抛出异常由 UI 层捕获后提示 } }测试覆盖三类基础运算验证主路径连续运算符验证状态机的提前结算分支除零验证异常路径。改完优先级算法后把testChainedOperator的期望值从 6 改成 7因为12×3按优先级应为 7这个测试就从验证旧行为变成了验证新功能思路很顺。工程习惯上还有一条每次改核心逻辑前先git commit一次当前能跑的版本。计算器的状态逻辑改动很容易出现改好了 A、弄坏了 B没有回退点就只能靠记忆还原那才是真的痛苦。从那以后我每次动这种带状态的逻辑都强制先提交一次、再跑一遍单元测试宁可多花两分钟也不愿意在答辩前一晚对着一堆乱跳的数字找 bug。希望这个项目能帮你顺利跑通第一版也希望你在它之上改出点自己的东西——毕竟课设交上去只是开始真正能写进简历的是那个你亲手扩展过、踩过坑、知道每一行为什么这么写的版本。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。