ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android Studio计算器开发实战:从界面布局到状态管理

2026/9/9 6:49:10 拓冰建站 浏览量
Android Studio计算器开发实战:从界面布局到状态管理 简介这是一份基于Android Studio开发的计算器App项目源码包源自南邮电院Android课程设计适合刚接触Android开发的大学生完成移动端UI交互与基础运算逻辑作业。资源共2910个文件压缩包大小26.19MB包含java源码、xml布局与配置、gradle构建脚本、apk安装包等其中java类与xml文件对应界面和逻辑实现gradle及相关配置文件便于直接编译运行dex/class为构建产物另有少量图片资源。核心功能覆盖加、减、乘、除、小数的连续运算并扩展了回退、清空、sin、cos、tan、括号、lg、ln等常见科学计算能力界面基于Android原生控件搭建代码结构完整可作为课程报告、演示Demo或二次开发的参考基座。该资源已有8300人学习浏览验证了其对同类计算器课题的参考价值直接解压后可按build.gradle导入Android Studio快速查看工程组织方式与计算结果校验逻辑。 很多刚接触Android开发的朋友搜“Android Studio实现计算器”后第一反应是找一段现成代码复制进去。结果通常是一编译就是一片红色报错或者好不容易跑起来按几个按钮就崩完全不知道哪里出了问题。计算器看起来简单但它覆盖了Android开发最核心的三件事——界面布局怎么写、按钮事件怎么接、业务逻辑怎么组织。这三个问题本质上就是一个App开发的全部骨架。从一个能用的计算器项目里你能同时把布局适配、事件分发、状态管理、异常处理这些课补上。这篇文章写给两类人一类是完全零基础、刚装好Android Studio想找第一个练手项目的另一类是有点Java或Kotlin基础、但还没独立跑通过完整App的。我会按我自己带新手时的完整流程来写从新建项目到真机调试包括那些教程里不会提醒你的坑。1. 先拆项目这个“简单”项目到底难在哪1.1 为什么几乎所有入门教程都拿计算器练手计算器是少有的“功能边界清晰”的App。它不需要网络请求、不需要数据库、不需要权限申请界面和逻辑都围着一块屏幕转。这意味着你能把所有注意力放在Android最核心的机制上布局怎么声明、点击怎么传递、状态怎么保存。相比之下一上来就做“记账本”“聊天室”这类项目光网络框架和环境配置就够劝退一批人了。另一个原因是计算器天然适合拆分模块。显示区和按钮区边界明确四则运算的规则你自己就能定义清楚不需要依赖任何第三方库。这是非常理想的“可控复杂度”项目——复杂度刚好够你体会到工程组织的重要性又不会复杂到让人放弃。1.2 用“三层”思维划分代码边界在动手写代码前我先建议你把整个项目拆成三层这是后面少走弯路的根本界面层负责长什么样。在Android里就是XML布局文件最多再加一点颜色、圆角、阴影之类的资源文件。交互层负责按钮点下去之后怎么办。这段逻辑写在Activity或Fragment里把按钮点击转成对应的动作。逻辑层负责“怎么算出结果”。纯计算逻辑不接触任何TextView、Button甚至不依赖Android环境。很多新手习惯把所有代码都堆在MainActivity里一个计算器写出上千行界面控件直接改来改去。结果就是改一个按钮的样式要小心翼翼半天生怕碰坏哪段逻辑。而把逻辑层抽出来之后计算规则可以单独测试将来换成Compose重写界面计算代码也一行都不用动。1.3 Kotlin还是JavaXML还是Compose现在的Android Studio新建项目默认就是Kotlin直接用它就行。Kotlin语法简洁空安全机制能帮新手挡掉一大批崩溃问题。至于界面写法我建议初学阶段老老实实写XML。这不是说Compose不好而是XML方案的资料存量最大、报错解决方案最好搜新手遇到问题能很快找到答案。Compose是值得学的方向但算完计算器之后再迈进那条新赛道会顺畅得多。2. 界面搭建权重和单位决定了面板是否好看2.1 显示区和按钮区的整体划分计算器的界面结构非常标准上方一块显示区下方一组按钮网格。显示区我用一个TextView靠右对齐字体偏大底部留点padding让文字不贴边。按钮区是4列5行的网格第一行放C、%、÷、×第二行7、8、9、-第三行4、5、6、第四行1、2、3、第五行0、小数点、退格、正负号。这样四个运算符各占一行末尾等号在右下角区域符合大多数人的使用习惯。外层用垂直方向的LinearLayout上半部分显示区下半部分按钮区各自通过layout_weight占据比例。按钮区内部再嵌套5个水平方向的LinearLayout每行放4个按钮。2.2 用layout_weight实现按钮自动等宽按钮等宽这件事最稳妥的方案是layout_weight。原理很简单当多个控件的宽高设为0dp并设置了不同layout_weight时系统会把剩余空间按权重比例分配。比如一行4个按钮每个按钮layout_width0dp、layout_weight1那4个按钮的宽度就完全一样。这样做的好处是不同屏幕尺寸下按钮会自动伸缩不用写死像素值也不用自己做屏幕适配。外层垂直方向也一样显示区layout_height0dp配合layout_weight1按钮区layout_height0dp配合layout_weight2显示区占三分之一左右高度按钮区占三分之二。这里有个经验值按钮区的整体高度建议略大于宽度的一半否则按钮会显得扁手指按起来不舒服。2.3 dp、sp、px到底怎么选新手最容易乱用的就是单位。px是真实物理像素写死之后在不同密度屏幕上要么太大要么太小dp是屏幕无关的像素1dp在不同密度屏幕上对应的物理尺寸基本一致控件间距、宽高都用它sp专门用于字号它除了适配屏幕密度还会跟随系统字体缩放设置。所以显示区的文字大小一定要用sp不要用dp。你也不希望用户把系统字体调大之后计算器上的数字还是原来那么小吧。2.4 一份可以直接用的activity_main.xml下面这份布局是完整可跑的。控件ID我起得比较直白后面代码里会直接用到。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding8dp TextView android:idid/tvDisplay android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:background#F2F2F2 android:ellipsizestart android:gravitybottom|end android:maxLines1 android:padding16dp android:text0 android:textColor#222222 android:textSize42sp / LinearLayout android:layout_widthmatch_parent android:layout_height0dp android:layout_weight2 android:orientationvertical LinearLayout android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:orientationhorizontal Button android:idid/btnClear android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 android:textC / Button android:idid/btnPercent android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 android:text% / Button android:idid/btnDivide android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 android:text÷ / Button android:idid/btnMultiply android:layout_width0dp android:layout_heightmatch_parent android:layout_weight1 android:text× / /LinearLayout !-- 后面的四行按钮结构完全相同ID依次是 btn7 btn8 btn9 btnMinus btn4 btn5 btn6 btnPlus btn1 btn2 btn3 btnEqual btn0 btnDot btnDelete btnSign -- /LinearLayout /LinearLayout后面按钮行严格按照前四行的结构补齐就行。两个细节第一Button默认自带textAllCapstrue属性英文字母会强制大写对C、%这些符号没有影响不用管第二如果想去除Button默认的阴影和多余边距可以给根布局加一个主题或者给按钮加style?android:attr/borderlessButtonStyle不追求这个效果的话忽略即可。3. 交互绑定别再用findViewById刷屏了3.1 传统写法和它的痛点早期写Android拿到一个控件要这样写先findViewById(R.id.btnClear)再强转成Button最后才设置点击监听。计算器这种二十来个按钮的界面光绑定控件就要写二十多行样板代码而且很容易出现复制粘贴后ID写错的情况。现在的Android Studio默认开启ViewBinding它会为每个XML布局自动生成对应的绑定类控件直接变成类的属性类型安全也没有findViewById的性能损耗。3.2 开启并初始化ViewBinding在项目模块的build.gradle.kts新版默认Kotlin DSL里android块中加一行android { buildFeatures { viewBinding true } }同步之后布局文件activity_main.xml会自动生成一个名为ActivityMainBinding的类。在MainActivity里这样初始化class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // 之后的控件引用直接写 binding.tvDisplay、binding.btnClear } }注意binding.root才是整个布局的根节点传给setContentView的是它不是原来的R.layout.activity_main。3.3 按钮监听的三种写法以及推荐方式给按钮绑定点击事件常见有匿名内部类、Lambda、实现接口三种写法。在Kotlin里匿名内部类现在基本被Lambda取代了binding.btnClear.setOnClickListener { clearAll() }最核心的一点是计算器这么多按钮如果每个按钮都单独写一段setOnClickListener代码会非常冗长。更好的做法是帮所有数字按钮绑定到同一个处理方法然后根据按钮ID判断是谁被按了。你可以在初始化时统一绑定也可以按类型分组绑定binding.btn0.setOnClickListener { onNumberClick(0) } binding.btn1.setOnClickListener { onNumberClick(1) } // ... 其余数字按钮同理 binding.btnClear.setOnClickListener { clearAll() } binding.btnDivide.setOnClickListener { onOperatorClick(÷) } binding.btnMultiply.setOnClickListener { onOperatorClick(×) } binding.btnEqual.setOnClickListener { onEqualClick() }这样每个按钮的代码只有一行逻辑全收敛到几个核心方法里后面排查问题非常轻松。等到按钮数量特别多比如几十个还可以再用with(binding){}简化但计算器这个规模不需要过度设计。4. 点击之后一个稳得住的计算逻辑状态机4.1 新手最容易翻车的三个场景计算器逻辑的坑几乎都集中在“状态”上。我见过最常见的三个问题连续点运算符输入5点再输入3这时候又点×结果直接乱套因为它没有把53先算出来。按了等号再输数字算完538之后再按数字7屏幕变成87而不是重新开始输入7。除零崩溃或显示Infinity6÷0没有拦截直接崩掉或者显示一个让人摸不着头脑的无穷大。这三个问题的根源都是缺少一个清晰的状态管理机制。4.2 用“三态”思维设计计算流程我把计算器的状态拆成几个变量第一个操作数firstNumber、当前运算符currentOperator、是否正在等待输入第二个数isWaitingForSecondNumber、以及是否刚显示完结果isResultShown。用户输入第一个数时isWaitingForSecondNumber为false数字正常往显示区追加。点击运算符后保存当前数到firstNumber记录运算符把isWaitingForSecondNumber置为true。此时用户再按数字显示区会清空重来重新输入第二个操作数。连按运算符时如果已经存在firstNumber和运算符就先把前一个运算算完把结果作为新的firstNumber再更新运算符。点击等号时执行运算显示结果清空运算符并标记isResultShown为true。此后用户再按数字会开启一个新输入而不是把数字拼到结果后面。这套设计相当于一个简单的状态机把每种用户操作都对应到明确的状态转换上逻辑就稳定了。4.3 核心逻辑代码实现下面的代码可以直接放进MainActivity里逻辑部分我刻意抽成了独立方法方便你看清楚每一步是怎么流转的class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private var firstNumber 0.0 private var currentOperator private var isWaitingForSecondNumber false private var isResultShown false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnClear.setOnClickListener { clearAll() } binding.btnEqual.setOnClickListener { onEqualClick() } binding.btn0.setOnClickListener { onNumberClick(0) } binding.btn1.setOnClickListener { onNumberClick(1) } // 其余数字按钮同理 binding.btnDot.setOnClickListener { onNumberClick(.) } binding.btnDivide.setOnClickListener { onOperatorClick(÷) } binding.btnMultiply.setOnClickListener { onOperatorClick(×) } binding.btnPlus.setOnClickListener { onOperatorClick() } binding.btnMinus.setOnClickListener { onOperatorClick(-) } } private fun onNumberClick(value: String) { if (isWaitingForSecondNumber || isResultShown) { binding.tvDisplay.text if (value .) 0. else value isWaitingForSecondNumber false isResultShown false return } val current binding.tvDisplay.text.toString() if (current 0 value ! .) { binding.tvDisplay.text value } else if (value . current.contains(.)) { // 防止出现 1.2.3 这种非法输入 return } else { binding.tvDisplay.text current value } } private fun onOperatorClick(op: String) { val displayValue binding.tvDisplay.text.toString().toDouble() if (currentOperator.isNotEmpty() !isWaitingForSecondNumber) { // 连续运算先把已有的计算完再把结果作为新的起点 val result calculate(firstNumber, displayValue, currentOperator) binding.tvDisplay.text formatResult(result) firstNumber result } else { firstNumber displayValue } currentOperator op isWaitingForSecondNumber true isResultShown false } private fun onEqualClick() { if (currentOperator.isEmpty()) return val secondNumber binding.tvDisplay.text.toString().toDouble() val result calculate(firstNumber, secondNumber, currentOperator) binding.tvDisplay.text formatResult(result) firstNumber result currentOperator isWaitingForSecondNumber false isResultShown true } private fun clearAll() { firstNumber 0.0 currentOperator isWaitingForSecondNumber false isResultShown false binding.tvDisplay.text 0 } 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 错误 // 去掉浮点误差产生的长尾巴同时保留整数显示 val trimmed String.format(%.10f, value) .trimEnd(0) .trimEnd(.) return if (trimmed.isEmpty()) 0 else trimmed } }这段代码已经能处理大部分场景连续运算、先乘除后加减之外的顺序问题暂且不讨论最简单的计算器不涉及优先级按输入顺序算想支持优先级需要引入表达式解析那个复杂度你后面再研究除零会显示“错误”等号后再输数字会重新开始重复输入小数点会被拦截。4.4 浮点精度的坑以及BigDecimal方案0.1 0.2在Java和Kotlin里都算不出精确的0.3结果是0.30000000000000004。上面代码里我用String.format(%.10f, value)把浮点数尾巴截短再trim掉末尾的0这样90%的日常计算都能正常显示。如果你做的是需要高精度的工具类计算器建议把计算部分换成BigDecimal加法和减法直接用add和subtract除法要指定精度和处理无限循环小数的情况。对入门练习来说Double加格式化处理已经够用等你把整个项目跑通了再回来替换成BigDecimal也不迟。5. 把它跑起来环境问题和真机调试5.1 新建项目卡在Gradle下载这是“Android Studio实现计算器”相关搜索里出现频率最高的问题。每次新建项目都要下载Gradle本质是因为Gradle Wrapper指定的版本在本地不存在需要从网上拉取。卡住通常不是网络完全不通而是下载连接不稳定。解决办法有几个打开项目根目录的gradle/wrapper/gradle-wrapper.properties把distributionUrl里的地址换成国内镜像地址。或者直接用浏览器/下载工具手动下载对应版本的zip包放到本机~/.gradle/wrapper/dists目录下对应的文件夹里重启Android Studio让它识别。依赖仓库repositories的下载慢可以把google()和mavenCentral()前面加阿里云镜像仓库地址。另外Android Studio里的SDK Manager、Gradle JDK版本也要注意新版要求JDK 17如果本机没有会自动配置一般不需要手动干预。遇到“Could not find gradle-xxx.zip”这类报错十有八九就是distributionUrl配错了版本号或下载不完整把目录下的.part临时文件删掉重新下载即可。5.2 用真机调试以小米手机为例模拟器启动慢、占内存入门阶段我更喜欢直接用安卓手机调试。以小米手机为例连接步骤是打开“设置”进“我的设备”再进“全部参数与信息”连续点击“MIUI版本”7次直到提示已开启开发者模式。回到设置找到“更多设置”进入“开发者选项”打开“USB调试”和“USB安装”。用数据线连接电脑手机弹窗选择“允许USB调试”并勾选“始终允许”。手机通知栏的USB连接方式要改成“文件传输”模式很多新手卡在这一步只充电是识别不了的。Android Studio上方设备列表里出现手机型号后点绿色运行按钮App会自动安装并启动。为什么老强调“连续点击版本号7次”因为Android系统里开发者选项默认隐藏这是所有品牌手机统一的隐藏入口只是各家的菜单位置略有不同。真机调试还有个好处是能感受到真实的操作体验——按钮按下去的手感、显示区字体是否过大、深色模式下看是否刺眼这些在模拟器里感受不到。5.3 闪退时先用Logcat定位问题这可能是入门阶段最值钱的一句话App闪退时不要慌点Android Studio底部“Logcat”窗口在过滤栏输入“AndroidRuntime”。红色的异常堆栈信息里会直接指出崩溃发生在哪个类的哪一行代码。比如at com.example.calculator.MainActivity.onOperatorClick(MainActivity.java:65)直接双击这行编辑器就跳到出错位置排查效率极高。我见过不少新手自己debug半个小时反复改代码、反复运行其实问题早就被Logcat明明白白打印出来了。养成先看日志再动手改代码的习惯比多会几个API都重要。6. 从能用走向好用计算器的进阶玩法6.1 旋转屏幕数据丢失问题默认情况下旋转屏幕会让Activity重建你算到一半的进度会全部清零。快速解决办法是在AndroidManifest.xml里给Activity加android:configChangesorientation|screenSize|keyboardHidden这样旋转时Activity不会重建。但更规范的做法是用ViewModel把firstNumber、currentOperator这些状态保存起来。ViewModel的生命周期和Activity不同系统配置变化时不会销毁适合放界面状态。入门阶段先处理到“转屏不清零”就可以了等学到架构组件再迁移到ViewModel。6.2 补全退格和正负号如果你照着前面布局做了第五行的退格和正负号按钮逻辑也很简单binding.btnDelete.setOnClickListener { val text binding.tvDisplay.text.toString() if (text.length 1) { binding.tvDisplay.text text.dropLast(1) } else { binding.tvDisplay.text 0 } } binding.btnSign.setOnClickListener { val value binding.tvDisplay.text.toString().toDouble() binding.tvDisplay.text formatResult(-value) }退格要小心一个场景当前输入“0.”的时候按退格应该回到“0”不能把小数点去掉变成空字符串。上面这段代码在长度为2时dropLast(1)得到的是“0”逻辑正好没问题。正负号的坑在于当用户在等待第二个操作数时按正负号应该对当前显示的数字取反上面的写法天然满足这一点。6.3 体验细节与个性改造当功能没问题之后让项目从“能跑”变成“好用”才是一个作品的开始。我的个人建议是按这个顺序改造给按钮加按压反馈比如使用ripple效果或者performHapticFeedback轻震。把“错误”状态下的按钮行为处理干净比如显示错误后再按任意数字要能重新开始而不是继续在错误文本上追加。学完RecyclerView之后把每次的计算过程追加到一个历史列表里这就是一个很实用的功能点。把App的应用名改成自己起的名字配一个简单的图标完全可以作为第一个作品安装到手机桌面上。从实用主义的角度看做完一个带历史记录、能处理错误状态、支持退格和正负号的计算器你其实已经把Activity生命周期、布局适配、事件处理、状态管理、列表展示这些Android核心基础都练过一遍了。这个项目的价值不在于计算器本身而在于它逼着你去思考状态、处理边界、关注体验。我当初带第一批新人时总结出的规律是能独立把一个计算器做成“别人愿意用”的程度的人后面接网络请求、接数据库、学架构组件都特别快。别嫌这个项目土也别急着跳过去直接学高级框架。你亲手把这一路报错解决一遍那些错误信息以后见到第一眼就能认出来。本文还有配套的精品资源点击获取