我之前一直觉得,计算器这种项目没什么技术含量,直到带过几个新手把完整的 Android Studio 项目跑起来之后,我才意识到:对刚接触 Android 开发的人来说,计算器恰恰是能把“界面布局 + 事件绑定 + 业务逻辑 + 源码组织”这条链路一次走完的最小项目。它不涉及网络请求、数据库这些容易分心的东西,知识点密度又足够高。这篇实战是基于 Android Studio 来做的简易版计算器,完整源码我会贴出来,每一段代码都会说明为什么这么写,遇到问题怎么排查。文章适合两类人:一是刚装好 Android Studio、想动手做第一个完整 App 的新手;二是以前跟着教程敲过代码,但没搞懂“按钮事件到底怎么流转”“浮点数为什么算不对”的初学者。
1. 环境准备:版本选型决定了你后面会不会踩坑
1.1 热词背后的版本对应关系(AGP、Gradle、JDK)
很多人被 “android studio hedgehog | 2023.1.1 patch 2 支持 agp8 版本吗” 这类问题卡住。这里有个基本概念需要先理清:Android Studio 是 IDE,AGP 是 Android Gradle 插件,两者不是绑定关系,但 IDE 版本对 AGP 有一个最低支持范围。Hedgehog 2023.1.1 对应的默认模板通常会创建 AGP 8.2 左右的项目,配套 Gradle 8.2+ 和 JDK 17。
这个版本对应关系非常关键,因为 AGP 8.x 之后发生了几个比较大的变化:
compileSdk、targetSdk、minSdk的配置方式更严格,targetSdk必须显式声明;- 原来的
package属性被namespace取代,新手很容易在这个地方报错; - AGP 8.x 默认要求 JDK 17,如果本机装了 JDK 8 或 JDK 11,同步 Gradle 时就会提示
Unsupported class file major version。
实际开发中我建议直接按官方推荐来:JDK 17 + AGP 8.2.x + Gradle 8.2 以上。这个组合在 Hedgehog 版本里跑得非常稳,不需要在网上找各种“兼容方案”。
1.2 创建项目时最容易忽略的配置细节
新建项目的时候,绝大多数教程会让选 “Empty Views Activity”,这个选择是对的。需要注意,现在 Android Studio 默认推荐的其实是 “Empty Activity”(基于 Compose),如果你选了这个,后面写布局文件的时候会遇到完全不同的思路。计算器这种小工具用传统的 View 体系更直观,XML 布局对新手也更友好,所以创建项目时一定要选模板名称里带 “Views” 的那个。
几个配置项的参考值:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Name | CalculatorApp | 应用显示名称 |
| Package name | com.example.calculator | 包名建议全小写,避免特殊字符 |
| Language | Java 或 Kotlin | 本文用 Java,便于老读者对照 |
| Minimum SDK | API 24 | 覆盖 Android 7.0 及以上设备,足够 |
| Build configuration language | Groovy DSL | 初学者别选 Kotlin DSL,资料更多 |
Minimum SDK 选择 API 24 的理由很简单:Android 7.0 以下的设备份额已经很低,没必要为了兼容老设备增加适配成本。计算器用到的 API 没有新特性依赖,所以这个选择完全够用。
1.3 虚拟设备失效的排查思路
热词里有一条 “为什么我的 android studio 的虚拟设备无效”,这几乎是每个新手都会遇到的事。AVD(Android Virtual Device)创建成功后,点启动却卡在启动界面、黑屏、或者直接报错退出,通常不是 Android Studio 坏了,而是下面几个原因:
- 系统镜像没装全:创建 AVD 时,如果选了 Google APIs 镜像,但没有下载对应的 System Image,启动时就会提示 fail。解决方法是打开 SDK Manager,在 SDK Platforms 里勾选对应 API Level 的 System Image。
- HAXM 或 Hyper-V 冲突:Windows 上 Intel CPU 需要安装 Intel HAXM,而 AMD CPU 需要依赖 Windows Hypervisor Platform(WHPX)。如果电脑开启过 Hyper-V,HAXM 会失效。这时候可以在控制面板里启用 “Windows Hypervisor Platform”,模拟器会自动走 WHPX,比卸载 Hyper-V 省事很多。
- AVD 的 ABI 与镜像不匹配:创建 AVD 时,ABI 选择
x86_64还是arm64,必须和已安装的 System Image 对应。这个不匹配往往在点击启动时才报错,排查起来很隐蔽。 - 显卡驱动问题:模拟器启动后白屏,多半是显卡渲染模式问题。可以在 AVD 的硬件配置里把 Graphics 改为
Software,虽然性能差一点,但能跑起来再说。
如果按上述顺序检查完仍然无效,还有一个笨但有效的办法:删掉 AVD 重新创建一个。很多情况下就是创建时某个参数和本机环境不匹配,重建一次,选默认配置,通常就好了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 界面布局:计算器 UI 设计的核心取舍
2.1 布局方案选型:GridLayout 还是嵌套 LinearLayout
计算器的界面结构非常规则:上面是显示区,下面是若干排按钮。这种场景有三种主流方案:
- 嵌套 LinearLayout:竖向 LinearLayout 里再放几条横向 LinearLayout,每条横放几个按钮。优点是简单直观,缺点是代码会有较多层级,修改按钮数量不太方便。
- GridLayout:Android 自带网格布局,直接设置
columnCount和rowCount,子元素按顺序填入格子。计算器按钮天然是网格结构,所以这是最优解。 - ConstraintLayout:功能强大,但写计算器需要给每个按钮相对其他按钮的约束,位置固定的场景反而繁琐。
我选 GridLayout,因为它最符合“按钮排布”的直觉。设置两行属性:
columnCount: 4,表示网格列数为 4;rowCount: 6,一行显示区 + 五行按钮(实际上显示区可以跨列,所以行数可以按需调整)。
这里有个容易忽略的细节:GridLayout 的子元素默认占据一个单元格,要让显示区横跨整行,需要给 TextView 设置 layout_columnSpan。按钮要均匀铺满每列,需要设置 layout_columnWeight 和 layout_rowWeight。网格布局里的 weight 和 LinearLayout 里的 weight 不太一样,它需要写完整包名:
xml复制app:layout_columnWeight="1"
app:layout_rowWeight="1"
也就是说,元素会按权重把多余空间分配到每个单元格,这样按钮无论屏幕多大都能均分。
2.2 按钮的尺寸、字体与状态反馈
计算器界面的体验细节主要集中在按钮尺寸、字体大小和点击反馈上。
按钮高度建议用 layout_height="0dp" 配合 layout_rowWeight="1",让 GridLayout 按行权重自动分配高度,而不是写死 60dp 或者 70dp。因为不同手机屏幕高度差异很大,写死尺寸在小屏手机上按钮会挤在一起,在大屏手机上又会显得稀疏。
字体方面,按钮文字用 24sp,显示区文字用 36sp 比较合适。这里的单位要注意:字体用 sp,尺寸用 dp。sp 会跟随系统字体缩放比例变化,dp 不会。如果按钮文字用了 dp,用户把系统字体调大后,文字不会随之变大,界面上可能出现文字互相遮挡的问题。
点击反馈默认的按钮状态变化在 Android 5.0 以后的 Material 主题下已经足够明显:按压会有水波纹效果。不需要额外设置背景选择器,除非你想要自定义颜色。我习惯给按钮加一个 stateListAnimator 属性关掉水波纹(android:stateListAnimator="@null"),让色彩反馈更可控,但这是风格问题,不强制。
2.3 activity_main.xml 完整代码与细节说明
下面是项目的布局文件,我会在关键属性后面加注释说明:
xml复制<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- 显示区:跨四列,显示输入内容和计算结果 -->
<TextView
android:id="@+id/tv_display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F5F5F5"
android:gravity="end|center_vertical"
android:padding="16dp"
android:text="0"
android:textSize="36sp"
android:textStyle="bold" />
<GridLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:columnCount="4"
android:rowCount="5">
<!-- 第一行 -->
<Button
android:id="@+id/btn_c"
android:text="C"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_del"
android:text="DEL"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_divide"
android:text="/"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_multiply"
android:text="*"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<!-- 第二行 -->
<Button
android:id="@+id/btn_7"
android:text="7"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_8"
android:text="8"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_9"
android:text="9"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_minus"
android:text="-"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<!-- 第三行 -->
<Button
android:id="@+id/btn_4"
android:text="4"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_5"
android:text="5"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_6"
android:text="6"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_plus"
android:text="+"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<!-- 第四行 -->
<Button
android:id="@+id/btn_1"
android:text="1"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_2"
android:text="2"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_3"
android:text="3"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_equals"
android:text="="
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<!-- 第五行 -->
<Button
android:id="@+id/btn_0"
android:text="0"
app:layout_columnSpan="2"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<Button
android:id="@+id/btn_dot"
android:text="."
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<!-- 注意:由于第5行只有 0(占两列)、点、占位Button 三个元素,
最后一个格子留空会导致布局不美观,所以这里用第4行的 = 按钮占用右下角位置 -->
</GridLayout>
</LinearLayout>
细心的读者会发现第五行我写了 0 占两列,然后接一个点号,但还差一个格子。这个格子是留给等号按钮的。不过 GridLayout 默认按顺序填充,如果我按上面的写法,等于按钮在第4行第四列已经占了位置,第五行就只有 0、. 和空格三个元素,最后一列空缺。简单处理的方式有两种:一是把等号放在第五行,把 0 放到第四行第三列;二是第五行放 0(占两列)、.、=,然后在第四行最后一个格子放一个空格操作符。实际项目里我建议把布局调整为五行按钮:第一行 C、DEL、/、*,第二行 7、8、9、-,第三行 4、5、6、+,第四行 1、2、3、=,第五行 0(跨两列)、.、以及一个空的占位 Button(可以设置 visibility="invisible")。这样不会因为空格子出现视觉缺口。
上面代码里的注释已经很啰嗦了,实际项目中布局文件里不建议写这么多注释,但如果你是想交作业或者写教程,保留这些注释能帮看代码的人省很多时间。
3. 计算逻辑:从按钮点击到算式求值的完整链路
3.1 事件绑定的三种方式
Java 代码里绑定按钮点击事件,常见方式有三种:
findViewById拿到对象,然后setOnClickListener逐个设置。最传统,代码量大,但直白。- 在 XML 里给 Button 设置
android:onClick="onButtonClick",在 Activity 里写一个同名的public void onButtonClick(View view)方法。代码量最小,但只能在 Activity 里用,不利于封装。 - ViewBinding(AGP 8 之后官方推荐)。需要开启
buildFeatures { viewBinding true },它会自动生成绑定类,避免手写findViewById。
对简易计算器来说,我推荐第 1 种。不是因为第 2、3 种不好,而是因为计算器按钮数量多,如果逐个 setOnClickListener,代码非常冗长。这里可以做一个统一的点击处理器:所有按钮都设置同一个 View.OnClickListener,通过 view.getId() 判断是哪个按钮被点击。这样整个事件绑定代码只需要一个 onClick 方法,里面用 switch 分支处理 17 个按钮。
3.2 浮点数精度问题
这是计算器里最值得展开的一个话题。热词里有 “浮点数计算器在线” 和 “java高级计算器”,说明不少人已经关注到浮点计算的坑。直接说结论:0.1 + 0.2 在 Java 里结果是 0.30000000000000004,不是 0.3。这是因为浮点数使用二进制存储,无法精确表示某些十进制小数。
做计算器时,不能直接把计算公式的结果原样显示出来,否则用户会看到一串怪异的数字。处理方案有两种:
- 显示层格式化:用
DecimalFormat把结果保留 8 位小数,并去掉末尾多余的 0。比如0.30000000000000004格式化成0.3。这是个简单有效的方案,不需要引入额外复杂度。核心代码:
java复制private String formatResult(double value) {
if (value == Math.floor(value) && !Double.isInfinite(value)) {
// 整数直接去掉小数位
return String.valueOf((long) value);
}
DecimalFormat df = new DecimalFormat("#.##########");
return df.format(value);
}
- BigDecimal:适合需要精确计算的场景,比如金额。计算器用
BigDecimal会面临除法除不尽的问题,需要指定精度和舍入模式,反而更复杂。我建议先做浮点版,遇到精度问题再用格式化解决,等后续想扩展科学计算器,再考虑 BigDecimal 策略。
3.3 主逻辑:表达式输入与单步运算
简易计算器有两种实现路线。第一种是“表达式解析”:用户输入一串字符,按等号后整体解析并计算结果;第二种是“单步运算”:每按一次运算符,就把当前显示值和上一个值按运算符先算一次。对初学者来说,表达式解析更贴近自然习惯,下面我按这个思路实现。
核心思路分三步:
- 用一个
StringBuilder保存当前输入的表达式,例如"12+3*4"; - 按
=时,把表达式拆成数字和运算符,先处理乘除,再处理加减; - 计算结果并显示,同时保留表达式,方便用户继续操作。
解析运算的算法很多,最简单可靠的是“双栈法”或者“递归下降”,但这里为了让新手能看懂,我用一个更直观的两轮遍历算法:
- 第一轮:处理所有
*和/,乘除运算优先级最高; - 第二轮:处理所有
+和-,得到最终结果。
这里有个细节:处理 - 时要判断它是二元运算符(a-b)还是一元负号(-5)。为简化,简易计算器里我先把负号当作二元运算符处理,如果要输入负数,可以先按 0 - 5,不完美但能跑通。
3.4 MainActivity 核心代码与流程拆解
下面是 MainActivity.java 的核心代码,我把它分成事件绑定和运算解析两部分。
java复制package com.example.calculator;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TextView tvDisplay;
private StringBuilder expression = new StringBuilder();
private boolean isResultShown = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvDisplay = findViewById(R.id.tv_display);
int[] buttonIds = {
R.id.btn_0, R.id.btn_1, R.id.btn_2, R.id.btn_3, R.id.btn_4,
R.id.btn_5, R.id.btn_6, R.id.btn_7, R.id.btn_8, R.id.btn_9,
R.id.btn_plus, R.id.btn_minus, R.id.btn_multiply, R.id.btn_divide,
R.id.btn_dot, R.id.btn_equals, R.id.btn_c, R.id.btn_del
};
for (int id : buttonIds) {
findViewById(id).setOnClickListener(this);
}
tvDisplay.setText("0");
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_c) {
expression.setLength(0);
tvDisplay.setText("0");
isResultShown = false;
return;
}
if (id == R.id.btn_del) {
if (expression.length() > 0) {
expression.deleteCharAt(expression.length() - 1);
}
tvDisplay.setText(expression.length() == 0 ? "0" : expression.toString());
return;
}
if (id == R.id.btn_equals) {
try {
double result = evaluate(expression.toString());
String formatted = formatResult(result);
expression.setLength(0);
expression.append(formatted);
tvDisplay.setText(formatted);
isResultShown = true;
} catch (Exception e) {
tvDisplay.setText("ERROR");
expression.setLength(0);
}
return;
}
// 数字、运算符、小数点统一追加到表达式
if (isResultShown) {
expression.setLength(0);
isResultShown = false;
}
Button btn = (Button) v;
String text = btn.getText().toString();
expression.append(text);
tvDisplay.setText(expression.toString());
}
private double evaluate(String expr) {
// 表达式分词:只支持 + - * / 和数字、小数点
List<Double> numbers = new ArrayList<>();
List<Character> ops = new ArrayList<>();
StringBuilder currentNumber = new StringBuilder();
for (int i = 0; i < expr.length(); i++) {
char c = expr.charAt(i);
if (Character.isDigit(c) || c == '.') {
currentNumber.append(c);
} else {
numbers.add(Double.parseDouble(currentNumber.toString()));
currentNumber.setLength(0);
ops.add(c);
}
}
numbers.add(Double.parseDouble(currentNumber.toString()));
// 先处理乘除
for (int i = 0; i < ops.size(); i++) {
if (ops.get(i) == '*' || ops.get(i) == '/') {
double left = numbers.get(i);
double right = numbers.get(i + 1);
double temp = ops.get(i) == '*' ? left * right : left / right;
numbers.set(i, temp);
numbers.remove(i + 1);
ops.remove(i);
i--;
}
}
// 再处理加减
double result = numbers.get(0);
for (int i = 0; i < ops.size(); i++) {
double next = numbers.get(i + 1);
if (ops.get(i) == '+') {
result += next;
} else if (ops.get(i) == '-') {
result -= next;
}
}
return result;
}
private String formatResult(double value) {
if (value == Math.floor(value) && !Double.isInfinite(value)) {
return String.valueOf((long) value);
}
DecimalFormat df = new DecimalFormat("#.##########");
return df.format(value);
}
}
我在 evaluate 方法里用了最直接的分词方式:遇到非数字字符就把之前累积的数字入栈,运算符进入另一个列表。这里要注意一个问题:如果表达式以负号开头,比如 -5+3,上面的分词会因为 currentNumber 为空而抛出 NumberFormatException。简易版计算器要规避这个问题,可以在输入阶段就限制“运算符不允许出现在第一位”,或者用户想输入负数时先输入 0,再输入 -,最后输入数字,也就是 0-5 的形式。这在产品的输入限制中属于合理的取舍。
3.5 边界情况的处理思路
实际使用中,用户不会规规矩矩地按顺序输入。下面这些情况我在开发时都处理过,代码里也许没有完全覆盖,但思路值得记录:
- 多个运算符连续输入:比如
1++2。这会导致分词时currentNumber为空。解决方案是在追加运算符前检查表达式末尾,如果已经是运算符,就替换而不是追加。 - 除零:
1/0在 Java 中不会抛异常,结果是Infinity。如果不处理,界面上会显示Infinity,对用户来说不友好。我建议在formatResult里判断Double.isInfinite(value),返回ERROR提示。 - 小数点重复:比如
1.2.3。输入阶段就应该判断:如果当前数字部分已经包含小数点,就不允许再次输入。 - 按等号后再输入数字:如果刚算完
12+3=15,这时用户按数字9,期望的是重新输入,而不是在15后面追加。代码里用isResultShown标志位控制这个行为,每次按非运算符按钮时先清空表达式。
这些边界情况不一定都要在第一个版本里处理完,但至少要知道它们的存在。一个健壮的计算器,不是算对 1+1 就行了,而是无论用户怎么乱按,都不至于崩溃。
4. 从源码到运行:编译调试过程中的常见坑
4.1 AGP 8 之后你必须知道的几个配置变化
热词里 “android studio hedgehog | 2023.1.1 patch 2 支持 agp8 版本吗” 这个问题,本质上是在问版本兼容。我前面说 AGP 8.2 搭配 Hedgehog 没问题,但真正写代码时你还会碰到几个配置上的坑,这些坑在旧教程里基本找不到:
坑一:namespace 取代 package
项目里 build.gradle(Module 级别)的 defaultConfig 下,以前会写 package "com.example.calculator",AGP 8 之后要改成 namespace "com.example.calculator"。如果不改,编译时会直接报错:
code复制Namespace not specified. Specify a namespace in the module's build file.
创建项目时默认已经生成好了 namespace,但如果你是从旧项目迁移,或者从网上下载了老项目源码,这个错误出现的概率很高。
坑二:buildFeatures 控制是否启用 ViewBinding
如果你想用 ViewBinding,必须在 android 节点下加上:
groovy复制buildFeatures {
viewBinding true
}
不加这个配置,代码里引用 ActivityMainBinding 时会报 “cannot find symbol” 类错误。网上很多老代码没有这一段,直接复制容易翻车。
坑三:AGP 8 默认禁用了 compileSdk 的向下兼容
如果在 build.gradle 里指定了 compileSdk 33,但本地 SDK Manager 只安装了 API 34 的 Platform,AGP 8 会直接报错提示你下载对应的 SDK,而不是像旧版本那样自动降级处理。解决办法很简单:打开 SDK Manager 把对应版本的 Platform 装上,别想着绕过。
4.2 模拟器/真机调试中的高频问题
项目写完,点 Run 按钮后,新手最容易卡在部署环节。我总结了几个高频问题及对应的处理顺序:
- Build 卡在 Gradle Sync 不动:大概率是 Gradle 依赖下载慢。国内开发者的常规做法是在
settings.gradle里把仓库地址换成阿里云镜像:
groovy复制pluginManagement {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
mavenCentral()
google()
gradlePluginPortal()
}
}
- 模拟器启动后一直黑屏:先看
Logcat是否有HAXM或Hyper-V相关日志。如果没有明显报错,试试把 AVD 的 Graphics 改成Software,这是最省事的临时方案。 - 真机调试时设备列表为空:手机开启开发者模式和 USB 调试后,还需要在
cmd里执行adb devices看看设备状态。如果显示unauthorized,就在手机上确认授权弹窗;如果显示offline,重新插拔 USB 线或重启 adb 服务(adb kill-server然后adb start-server)。
4.3 显示中文、设置中文这些小事
热词里有人搜 “android studio怎么设置中文” 和 “android studio汉化”。这里有一个实用的小技巧:Android Studio 的界面语言其实跟随系统语言。如果你想强制使用中文,可以在安装目录的 bin 下找到 idea.properties 文件,添加一行:
properties复制user.country=CN
user.language=zh
然后重启 Android Studio 即可。不过我个人建议老手还是保留英文界面,因为报错信息、官方文档都是英文,中文界面可能让你在搜索解决方案时对不上关键词。
4.4 源码结构梳理
最后的源码结构部分,我习惯用这样的目录组织方式:
code复制CalculatorApp/
├── src/main/
│ ├── java/com/example/calculator/
│ │ └── MainActivity.java
│ ├── res/
│ │ ├── layout/
│ │ │ └── activity_main.xml
│ │ └── values/
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── AndroidManifest.xml
└── build.gradle
如果你下载别人的源码参考,建议先看这三个文件:AndroidManifest.xml(入口 Activity 是什么)、build.gradle(依赖和版本)、MainActivity.java(逻辑入口)。很多源码跑不起来的根本原因不是代码问题,而是版本不一致。对照这三个文件检查,通常 10 分钟内能定位问题。
说到源码,我最后想补充一点个人体会:算起来这是我带过的第 N 次计算器项目了,但每次写还是会遇到新问题,比如这次用 AGP 8 就踩了 namespace 的坑。计算器虽小,但它把 Android 开发的主干流程完整串了一遍。如果你能独立把这个 App 从创建项目到装进手机完整跑通,再回头看那些更复杂的 App 源码,思路会清晰很多。建议接下来试着加点功能:括号支持、连续运算结果回填、历史记录,这些都是很好的扩展方向。遇到问题的时候,别急着把整段代码删了重写,先用 Log 看数据流转,再对照我这边的代码结构排查,很多问题其实都藏在你不以为然的细节里。
