不少刚接触Android开发的朋友想找个能顺手练完整个基础链路的项目第一反应多半是“做个计算器”。它没有复杂的网络请求也不需要设计数据库表但要真正把一个能用的计算器做出来你需要把界面布局、按钮事件、状态切换、异常处理这几件事全部串在一起这里面的坑并不比做一个列表页少。我当初用AS写这个简单计算器时前后重构过两版代码第一次只想着把功能跑通结果连续运算和退格逻辑乱成一团第二次才把状态机理清楚写出来的东西不管自己多久之后回看都能一眼明白。这篇文章不打算给你一个“复制粘贴就完事”的成品而是把从新建项目到调试上真的完整走一遍把每个设计选择的理由也讲清楚。适合两类人看一是刚学完基础语法、想找个项目练手的新手二是会写点界面但总在计算逻辑上卡壳的朋友。看完你应该能独立写出一个界面干净、逻辑可靠的四则运算计算器并且知道怎么扩展成科学计算器。1. 项目启动与整体设计思路1.1 创建工程和基础选型在AS里新建项目时选择“Empty Views Activity”或者“Empty Activity”都可以关键是语言建议直接选Kotlin。现在Kotlin已经是Android开发的默认选择用它写计算器代码更紧凑空指针问题也少很多。项目名可以叫“SimpleCalculator”或者你喜欢的任何名字包名保持默认即可。最低支持的Android版本minSdk我习惯设为24也就是Android 7.0。这个版本覆盖了绝大多数还在流通的设备同时又能让你放心使用较新的API和第三方库不用为了兼容老版本写一堆判断。如果你手头的设备或模拟器系统版本比较老调到21也能运行只是布局属性上稍微注意一点就行。新建完项目不要急着写代码。先打开模块的build.gradle文件看一眼确认一下依赖里有没有自动带上appcompat。这个库提供了向后兼容的AppCompatActivity以及各种Material风格控件是写界面很基础的东西。如果没有在dependencies里补一行implementation(androidx.appcompat:appcompat:1.6.1)1.2 功能边界到底做到哪一步才算“简单计算器”很多人一上来就想做一个和系统计算器一模一样的应用有历史记录、有百分比、有正负号切换、有连续表达式。我建议第一次做时收着点把功能边界划清楚否则会被各种细节拖垮。我这次定的目标是支持数字 0-9 的输入支持小数点输入支持加、减、乘、除四种运算支持等号计算支持清空C支持退格删除最后一位支持连续运算比如输入5 3 2 能得到 10不做的功能包括括号、科学函数、历史记录、按键音效。等核心逻辑稳定之后这些都可以当作扩展项再加。这样做的好处是你可以把精力集中在“一个表达式如何被拆解和计算”这件事上而不是被各种花哨按钮干扰。1.3 两种实现路线的取舍计算器的核心逻辑一般有两种实现方式第一种是“表达式解析”。把所有输入拼成一个字符串比如53*2然后调用表达式求值引擎解析并计算结果。这种方式功能上限很高支持括号、优先级都很方便但引入的依赖和算法复杂度也会提高。第二种是“逐步计算”。用户每次输入一个数字再按运算符时就把当前显示值和上一个值做一次运算保存中间结果。这种方式不依赖第三方库代码写在明处每一步的变量变化都能看清特别适合新手理解状态管理。我这次选的是第二种原因很实际简单计算器的需求足够简单逐步计算的代码量更少逻辑也更符合“人按计算器的习惯”。如果你以后想做带括号的复杂表达式再回归到表达式解析也不迟那是另一个话题了。2. 界面布局与控件规划2.1 用线性布局搭出计算器骨架计算器界面无非是一个“显示屏”加若干行按钮。Android里实现这种规则排列的方式有很多比如GridLayout、TableLayout或者嵌套多个LinearLayout。我习惯用嵌套线性布局因为代码直观权重分配也好控制。整体结构是这样的最外层是一个垂直LinearLayout设置一定的内边距顶部放一个TextView用来显示输入的内容和结果中间放3行数字和运算符按钮每行使用一个水平LinearLayout每个按钮设置layout_width0dplayout_weight1让它们等宽排列这样写出来的布局不会因为屏幕变宽而乱掉按钮会自动拉伸填满一行。初次做布局的新手最容易犯的错是把按钮宽高直接写死成200dp换个屏幕就挤成一团甚至看不见权重方案能彻底避免这个问题。2.2 完整的布局代码示例下面是我用的activity_calculator.xml主体结构为了篇幅省略了部分重复按钮但写法是完全一致的LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp TextView android:idid/tv_display android:layout_widthmatch_parent android:layout_height100dp android:gravitybottom|end android:text0 android:textSize36sp android:background#F5F5F5 android:padding12dp / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop12dp Button android:idid/btn_7 android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text7 / Button android:idid/btn_8 android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text8 / Button android:idid/btn_9 android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text9 / Button android:idid/btn_add android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text / /LinearLayout LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal Button android:idid/btn_4 android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text4 / Button android:idid/btn_5 android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text5 / Button android:idid/btn_6 android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text6 / Button android:idid/btn_subtract android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text- / /LinearLayout !-- 数字1、2、3和乘法按钮写法同上 -- !-- 数字0、小数点、清空、除法和退格按钮写法同上 -- LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal Button android:idid/btn_clear android:layout_width0dp android:layout_height60dp android:layout_weight1 android:textC / Button android:idid/btn_backspace android:layout_width0dp android:layout_height60dp android:layout_weight1 android:text⌫ / Button android:idid/btn_equals android:layout_width0dp android:layout_height60dp android:layout_weight2 android:text / /LinearLayout /LinearLayout注意最后一行的等号按钮我给它的layout_weight设成了 2让它在视觉上占两个按钮的宽度这样界面看起来更接近常见计算器布局也和前面的行对齐。权重总和为1124等号自然就是一半宽度。2.3 控件命名规范和后续维护按钮的id要见名知义我习惯把数字按钮命名为btn_0、btn_1运算符按钮命名为btn_add、btn_subtract、btn_multiply、btn_divide功能按钮命名为btn_clear、btn_backspace、btn_equals。这样在代码里查找和绑定按钮时几乎不用注释就知道它是干什么的。之前见过不少人把按钮命名为button1、button2写到后面自己都不记得哪个是哪个调试时只能一个个试。如果你觉得纯白按钮太单调可以给运算符和功能键加背景色。用一个drawable资源定义圆角和背景颜色或者直接用MaterialButton的app:backgroundTint属性方便又美观。颜色选择上数字键用灰色调运算符用深蓝或橙色功能键用浅灰用户一眼就能区分按键类型。3. 运算逻辑与核心代码实现3.1 状态管理三个关键变量逐步计算的核心是维护好三个状态上一个数字、挂起的运算符、当前输入的内容。我定义了下面这些成员变量private var firstNumber: Double? null private var pendingOperator: String? null private var isNewInput true三个变量的含义分别是firstNumber保存运算符左边的数值。它为空时说明当前还没有进行任何运算。pendingOperator保存用户刚按下的运算符比如、-、×、÷。isNewInput标记下一次按下数字时是应该清空当前显示、还是直接追加在现有数字后面。isNewInput是整个逻辑里最容易理解错的地方。举个例子用户输入5然后按下接下来再按3。如果此时没有这个标记显示框里的5就会变成53那肯定不对。按下之后我们需要把“下一次输入当作新的运算数处理”所以isNewInput被设为true数字按钮点击时先清空显示内容再写入新数字。这个标记是保证连续输入能正确衔接的关键。3.2 数字按钮的点击处理数字按钮的实现非常简单但要注意两个边界条件一是当前是否为新输入的开始二是显示内容是否已经有了小数点。fun onDigit(view: View) { val digit (view as Button).text.toString() val currentText binding.tvDisplay.text.toString() if (isNewInput) { binding.tvDisplay.text if (digit .) 0. else digit isNewInput false } else { if (digit . currentText.contains(.)) { return } if (currentText 0 digit ! .) { binding.tvDisplay.text digit } else { binding.tvDisplay.text currentText digit } } }这里的binding.tvDisplay是视图绑定生成的控件引用用了官方推荐的ViewBinding方式。如果你没用ViewBinding换成findViewById也是一样的效果。有个细节值得专门提一下当显示框里只有单个0而用户按下的是数字键5时我们不应该把显示变成05而是直接替换成5。所以我在代码里做了if (currentText 0 digit ! .)这个判断。这个细节很容易被忽略但体验差异很大。3.3 运算符按钮和等号的处理运算符按钮的职责可以分成两种场景第一次按运算符或者连续按第二个运算符。先说最简单的场景用户还没有输入过运算符那么按下时我们把当前显示值存进firstNumber把存进pendingOperator然后把isNewInput设为true后续输入的数字会作为第二个操作数。fun onOperator(view: View) { val operator (view as Button).text.toString() val currentValue binding.tvDisplay.text.toString().toDoubleOrNull() ?: return if (firstNumber null) { firstNumber currentValue pendingOperator operator } else { // 已经有上一次操作数先计算结果 val result calculate( firstNumber!!, currentValue, pendingOperator!! ) binding.tvDisplay.text formatResult(result) firstNumber result pendingOperator operator } isNewInput true }这里做了一个很关键的处理当用户连续输入5 3 时第二个按下时我们不能直接把它存为新的运算符而是先把5 3的结果算出来将结果作为下一次运算的第一个操作数再把新的存起来。这就是“逐步计算”的本质——表达式永远只保留“第一个数 运算符”第二个数由后续按键提供。等号按钮的逻辑更简单fun onEquals(view: View) { val currentValue binding.tvDisplay.text.toString().toDoubleOrNull() ?: return val first firstNumber ?: return val op pendingOperator ?: return val result calculate(first, currentValue, op) binding.tvDisplay.text formatResult(result) firstNumber null pendingOperator null isNewInput true }等号算完之后要记得把状态清空否则用户接着按运算符时可能会拿到一个过期的firstNumber导致计算错误。3.4 浮点误差处理和显示格式化计算逻辑虽然直接返回Double但如果你天真地把结果直接显示出来一定会遇到类似0.1 0.2 0.30000000000000004的尴尬。这并不是Android的问题而是所有采用二进制浮点数的编程语言都会遇到的精度问题。简单的计算器项目不需要引入高精度BigDecimal但显示格式化是必须做的。calculate方法里有一个formatResult函数来收尾private fun calculate(a: Double, b: Double, op: String): Double { return when (op) { - a b - - a - b × - a * b ÷ - { if (b 0.0) { return Double.NaN } a / b } else - 0.0 } } private fun formatResult(value: Double): String { if (value.isNaN()) { return 错误 } return if (value value.toLong().toDouble()) { value.toLong().toString() } else { String.format(%.8f, value).trimEnd(0).trimEnd(.) } }格式化时先判断结果是否为整数。如果是整数就按整数显示避免出现8.00000000这种难看的结果。如果不是整数则保留8位小数并去掉末尾多余的0。这样一个计算器显示效果就接近系统计算器了。除数为零时我返回了Double.NaN并在格式化时把它转成“错误”提示。用户看到这个提示至少知道是自己操作问题而不是应用崩溃了。3.5 清空、退格和基础状态复位清空按钮是最没有技术含量的但它必须把所有状态全部复位一个都不能漏fun onClear(view: View) { binding.tvDisplay.text 0 firstNumber null pendingOperator null isNewInput true }退格按钮稍微讲究一点。它要做的是去掉当前显示内容的最后一个字符但如果显示的是0或者正准备输入新数字那就什么都不做。退格还有一个容易被忽略的情况如果删除后数字变成空字符串要把它恢复成0不能留着空框。fun onBackspace(view: View) { if (isNewInput) return var text binding.tvDisplay.text.toString() if (text.length 1) { text text.dropLast(1) } else { text 0 } binding.tvDisplay.text text }我还想在处理退格时加上一个“防止-5的负号被误删”的细节。因为我没有实现正负号按钮所以在简单版本里这个情况不会出现。如果你后面加了正负号切换再写退格逻辑时就要判断删除的是负号还是数字。3.6 连续运算的完整流程演示我们把上面的逻辑串起来跑一遍12 5 × 3 这种连续输入看看状态是怎么流转的。用户输入 12显示 12。 按下firstNumber12pendingOperatorisNewInputtrue。 输入 5显示 5。 按下×因为firstNumber不为空先计算12 5 17把结果显示为 17然后firstNumber17pendingOperator×isNewInputtrue。 输入 3显示 3。 按下计算17 × 3 51显示 51清空firstNumber和pendingOperator。注意这个流程其实不遵守数学里的“先乘除后加减”12 5 × 3的结果会按顺序算成 51而不是标准数学的 27。很多新手拿到这个程序后会困惑为什么跟学校学的不一样。原因是“逐步计算”本身就没有做优先级处理它忠实还原的是普通四则计算器的“从左到右”操作习惯。如果你希望支持运算优先级就得引入表达式解析或者把×和÷的优先级单独处理。这类扩展我在后面会讲到。4. 调试记录与常见故障排查4.1 按钮点击没反应这是我见过新手最容易遇到的问题。界面上按钮清清楚楚点击之后却毫无动静。原因通常有三个一是按钮的onClick方法没写好二是布局里id跟代码里的不一致三是ViewBinding没有绑定到当前布局。我的排查顺序是先打开MainActivity看setContentView和binding是否对应同一份布局文件。再检查按钮的id是否在XML里唯一有没有复制粘贴后忘了改。最后看fun onDigit(view: View)这种方法的签名是不是(View)类型漏掉参数的话按钮点击事件压根调不到这个方法。如果你不想在XML里写android:onClickonDigit也可以给每个按钮设置setOnClickListener。但我推荐使用XML标签方式代码里方法名清晰查找方便也不会因为动态改名导致绑定失效。4.2 计算结果总是差一位或变成零结果不对是计算器开发里最常见的逻辑问题。比如输入3 5结果却显示5或者2 × 3结果显示成0。出现这种情况十有八九是firstNumber的赋值时机错了。你可以回顾一下按下运算符时你拿到的是显示框的文字还是上次计算后残留的某个值调试时我习惯在关键节点加上一行日志Log.d(CalculatorDebug, firstNumber$firstNumber, op$pendingOperator, display${binding.tvDisplay.text})然后在模拟器里按几个数字在Logcat里看状态变化。这个方法比猜代码快得多。我自己的经验是把点击运算符时的状态、点击等号时的状态都打出来几乎一眼就能找到是谁没更新、谁没归零。还要注意一点TextView的text是CharSequence类型直接转String时要用toString()否则可能拿到包含其他内容的显示对象导致toDouble解析异常。4.3 布局在不同尺寸下错位模拟器上好好的界面换到真机就按钮挤成一团或间距过大。原因基本都和布局宽度有关。优先检查是不是用了match_parent让按钮填满整行然后没给其他按钮留出空间。或者是直接给按钮设置了固定宽度比如100dp在窄屏上自然放不下5列。解决方案其实在上面已经提过所有一行里的按钮都设为layout_width0dp并且用layout_weight分配比例。这样无论屏幕多宽按钮都会均分。如果出现按钮文字被截断把按钮的layout_width从0dp改成wrap_content但保留权重也是一种思路但不建议。还有一种隐蔽情况某个按钮的layout_height是wrap_content其他按钮是60dp结果它们不对齐。最简单的办法是同一行所有按钮的高度写死成同一值比如60dp保持一致。4.4 真机调试设置备忘使用模拟器调试时可以直接运行但真机调试偶尔会遇到识别不了设备的情况。我遇到过的原因主要是手机没打开开发者选项或者USB连接模式被选成了“仅充电”。解决办法是打开设置里的“开发者选项”开启“USB调试”然后把USB连接方式改成“传输文件”或“PTP”模式。如果插上电脑还是没有识别可以试试更换数据线。USB口的充电线经常只走电不传数据这类问题不是代码问题却会卡掉你一下午。4.5 常见问题速查表现象可能原因排查和解决按钮点了没反应id不一致 / onClick方法签名不对 / 绑定布局错误检查XML和Kotlin代码里的id确认方法带View参数计算结果错乱firstNumber或operator状态残留在等号后清空两个状态补Log查看流转加法结果为小数后一堆0没有做格式化用formatResult统一处理整数和小数显示除数为0崩溃没有判断除数在calculate里对0处理返回特殊值连续运算不生效按运算符时没有先算上一次结果按运算符时若已有firstNumber先计算并覆盖布局错位固定宽度 / 权重丢失统一用0dpweight高度设为固定dp5. 扩展方向与进阶优化建议5.1 加入乘除优先级如果你做完基础版觉得不过瘾最值得做的扩展就是让计算器能按数学规则先算乘除再算加减。这个改造说难不难思路有几种我推荐写一个简单的表达式解析器。思路可以这样把输入拆成数字和运算符先扫描一遍所有×和÷对相邻数字先做计算再把结果替换回原表达式。例如3 5 × 2先算5 × 2 10表达式变成3 10最后从左到右相加得到 13。这种算法叫“调度场算法”的简化版代码量大约三四十行却能大幅提升计算器的实用性。5.2 历史记录功能历史记录是很多人喜欢的扩展方向。实现方式也很直接用一个RecyclerView显示之前计算过的表达式和结果每次点击等号时把“表达式结果”字符串存到一个ArrayList里再刷新列表。如果需要持久化可以选Room数据库或者简单的SharedPreferences。我建议第一版先用SharedPreferences存JSON字符串功能稳定后再考虑数据库。5.3 简单科学计算器给计算器增加科学函数也没有想象中复杂因为系统自带的数学库已经提供了几乎所有函数。比如平方根用Math.sqrt三角函数用Math.sin、Math.cos对数用Math.log。关键是界面上要增加一列函数按钮同时要让计算逻辑支持“函数优先”的处理方式。一个常见的做法是按函数按钮时把当前显示值直接传入函数结果覆盖当前显示不需要改变firstNumber和pendingOperator的状态。5.4 发布前可以做的自检清单项目做完了想装到手机上或者发到应用市场建议做一轮基础自检。我这里列一个自己固定的检查项布局在不同尺寸的模拟器和真机上都跑一遍确认没有重叠或按钮消失测试用例跑一遍00、1÷0、0.10.2、9999999991、连续按多个运算符检查退格是否能把显示归零检查横竖屏切换时状态是否保留如果发现状态丢了用onSaveInstanceState保存第一操作数和运算符最后看一眼打包体积正式包要打开混淆和资源压缩这样APK体积能小不少5.5 精简依赖和资源细节最后再分享一个很多人不注意的小点新建项目默认会带一些用不上的资源比如导航菜单、主题里多余的样式。你这个计算器项目用不到这些可以删掉无关的XML文件和drawable减少无意义的代码量。同时建议把主题从继承默认主题改成继承Theme.MaterialComponents.DayNight.DarkActionBar或Theme.AppCompat.DayNight这样应用能自适应深色模式用户手机开了深色主题计算器不会一亮一暗地闪眼。6. 实际体验中的几个提醒代码写完只是第一步真正把它用起来你才会发现一系列和“逻辑是否对”无关但同样影响体验的细节。按键触控反馈是我比较早发现的一个问题。默认的按钮点击时没有震动也没有明显的按下状态用户容易怀疑自己没点中。解决方式有两种一是给按钮设置stateListAnimator让它在按下时改变透明度或背景色二是在点击事件里调用view.performHapticFeedback触发轻微震动。我用的是第二种方式代价是需要在真机上看一下震动强度别太生硬就行。还有一个细节是显示区域的TextView长度。当用户输入一个很大的数字显示框可能放不下。简单做法是动态调整字体大小比如文本超过12个字符时把字号降到24sp超过20个字符时降到18sp。我在原版代码里没有做这个实际测试输入12345678901234567890时显示直接溢出边框观感很差。后来加了一个adjustTextSize的小方法只在.和数字输入时调用成本很低效果明显。再就是关于代码组织。我见过很多人把一个Activity里塞几百行逻辑计算器项目小看起来问题不大但一旦你要加历史记录、科学函数这个文件就会膨胀到难以维护。建议把“计算逻辑”单独抽一个CalculatorEngine类界面只负责接收点击和刷新显示。这样以后加单元测试也方便直接对CalculatorEngine写测试不用在模拟器里手动点按钮验证。我个人实际写这个项目最大的体会是计算器看起来简单但它几乎是“状态管理”的最佳教学案例。三个变量的状态流转、运算符和数字的交互顺序、连续运算的边界情况覆盖了Android开发里大部分基础而又核心的概念。如果你正在学AS认真把一个简单计算器做透比急着堆砌五六个半成品Demo要有用得多。下一步可以试着用同样的状态机思路去做一个BMI计算器或者购物车结算页面你会发现很多概念都是相通的。把状态变量命名清楚把每次点击时的变化捋顺把异常情况兜住这个计算器就不只是“能算”而是真正“好用”了。