简介:BMI健康计算器是一款用于评估人体体重与身高比例的安卓应用源码,压缩包内提供完整的Android Studio工程,适合初学安卓开发的读者通过实战项目理解健康类应用的开发要点。资源共26个文件,包括Java核心逻辑、XML界面布局、PNG图标、class字节码以及可直接安装的apk文件,整体仅50KB,轻量易读,特别适合作为源码级学习材料。截至目前已有549人浏览学习,在同类资源中具备一定热度。透过源码可以掌握BMI计算(体重kg/身高㎡)在Android中的落地方式,涉及用户输入校验、计算按钮的点击监听、结果文本的动态刷新等关键交互流程;同时可学习Activity生命周期管理、res资源划分、AndroidManifest配置、字符串引用与基本国际化处理等工程化知识,有助于初学者串联起界面设计、数据处理和资源组织的完整环节,为后续独立开发工具类应用打下扎实基础。压缩包中还附带简单的说明文档,方便快速上手。
1. 从“BMI健康计算器_android源码.zip”开始:先别急着改代码
很多人的下载目录里都躺着这样一个压缩包。文件名写得清清楚楚:BMI健康计算器,附带android源码,后缀是zip。解压之后也确实看得到Activity、布局文件和AndroidManifest,但真要把这套源码变成自己手机里能跑的App,中间隔着不少隐形门槛:Gradle同步失败、SDK版本对不上、计算逻辑散落在按钮点击事件里、输入框随手一填就崩。这里处理的就是这类经典资源包的标准落地路径:从zip解压到工程结构确认,从BMI公式落到数据模型,再从输入校验做到release打包与真机验证。适合手头有这类源码包、打算改成自己项目的工程师,也适合刚接触Android源码想搞清“zip里到底该改哪里”的人。先构建通过,再谈改代码。
2. 拆开zip源码包的第一步:验证工程结构,对齐Android Studio编译环境
2.1 先看zip里装的是什么:目录、Gradle外壳与资源文件
拿到zip的常见做法是先解压,再把整个工程目录拖进Android Studio,而不是直接在IDE里双击zip。Android Studio只认目录,不认压缩包,直接打开zip会在同步阶段报找不到Gradle文件之类的错误。更实际的问题是,很多源码zip解压后并不是标准工程结构,顶层直接就是MainActivity.java,连settings.gradle都没有,这种只能算代码片段,需要手工补外壳。
unzip -l BMI健康计算器_android源码.zip | head -40参数说明:-l表示只列出压缩包内容,不解压;head -40限制输出行数,避免文件多时刷屏。重点看列表里有没有settings.gradle、build.gradle、gradle/wrapper/gradle-wrapper.properties和app/src/main/AndroidManifest.xml。如果是com/example/bmi/MainActivity.java直接顶在最前面,说明压缩时把工程内部目录当成了根目录,解压后需要手动调整层级。
确认结构后,解压到独立目录:
unzip BMI健康计算器_android源码.zip -d ./bmi_src cd ./bmi_src && ls -la-d指定输出目录很关键,能避免zip解压后把一堆文件散落在当前工作目录。ls -la看是否有app和gradlew,有gradlew说明工程带Gradle Wrapper,依赖版本被锁在gradle-wrapper.properties里;没有则要从其他Android工程拷贝gradle/wrapper整套目录,否则本机Gradle版本会直接影响编译行为。
2.2 版本三件套:compileSdk、targetSdk 与 distributionUrl
同步失败的常见原因不是代码错误,而是版本错位。老zip源码包最常见的状态是compileSdkVersion停留在 25 或 28,而本机Android SDK只装了较新的Platform;也可能是本机JDK太新,Gradle 4.x 无法解析。此时先别急着卸载重装,按这张表逐项核对。
| 配置项 | 常见老值 | 当前常用值 | 说明 |
|---|---|---|---|
compileSdkVersion | 25 | 34 | 改高后需在SDK Manager里下载对应Platform |
targetSdkVersion | 22 | 34 | 影响运行时权限与文件访问策略 |
distributionUrl | gradle-4.6-all.zip | gradle-8.x-all.zip | 决定是否兼容当前Android Studio版本 |
| JDK版本 | 1.8 | 17 | Android Studio内置JBR一般无需手动改 |
app/build.gradle里的compileSdkVersion与targetSdkVersion建议对齐到当前Android SDK官网能下载到的Platform版本;gradle/wrapper/gradle-wrapper.properties里的distributionUrl则指定构建工具链版本。这里有一个常被忽略的点:Android Studio界面想设置成中文属于语言偏好,不影响构建成功率。真正决定同步结果的是上面这几个参数,不要在设置面板里反复横跳。
注意:构建窗口出现
error read zip archive时,多半是下载的gradle-*-all.zip文件损坏或下载不完整。先删除用户目录下~/.gradle/wrapper/dists里对应的缓存目录,再重新同步;大多数情况比换Gradle版本更直接。
修改完在Android Studio里执行File -> Sync Project with Gradle Files,同步通过后再尝试运行。如果报错信息指向Build Tools缺失,回到SDK Manager勾选对应版本即可,不必重复下载整套Android Studio安装包。
2.3 解压乱码与content://授权:两条容易被忽略的小路
Windows上以中文命名压缩的zip,在macOS或Linux下解压经常出现乱码文件名。Java源文件乱码还能在IDE里重新保存,res/values下的XML资源文件名如果变成乱码,Gradle会在资源合并阶段直接失败。处理方式是解压时指定编码:unzip -O CP936 文件名.zip,或解压后把资源文件名改回规则命名。
另一个场景是:在Android 11及以上设备上,用系统文件选择器去选zip文件时,返回的访问路径是content://形式的一次性授权URI,而不是本地文件路径。Android Studio的工程导入向导面对这种URI往往解析失败。更可靠的做法仍然是先把zip同步到本地固定目录,解压成标准工程结构,再用Open打开目录。工程能编译能跑之后,才进入真正的业务改造。
3. BMI 计算的数据模型:从一行除法到可测试的领域逻辑
3.1 公式与输入边界:身高厘米转米必须显式处理
很多源码笔记里只会贴BMI公式本身:体重kg / 身高m²。公式只有一行,但真实输入不会那么干净。有人把体重输入成2000,把身高输入成16而不是160,EditText甚至可能传空字符串。如果不加边界,这些值会算出0.08或781.25之类的“合法浮点数”,用户看到只会觉得程序坏了。
public final class BmiCalculator { private static final double MIN_WEIGHT_KG = 20.0; private static final double MAX_WEIGHT_KG = 300.0; private static final double MIN_HEIGHT_CM = 50.0; private static final double MAX_HEIGHT_CM = 250.0; public static Double calculate(double weightKg, double heightCm) { if (!isValidInput(weightKg, heightCm)) { return null; } double heightM = heightCm / 100.0; return weightKg / (heightM * heightM); } private static boolean isValidInput(double weightKg, double heightCm) { return weightKg >= MIN_WEIGHT_KG && weightKg <= MAX_WEIGHT_KG && heightCm >= MIN_HEIGHT_CM && heightCm <= MAX_HEIGHT_CM; } }calculate返回Double而不是double,是为了让调用方能区分“输入非法”和“正常计算结果”,否则只能用魔法数字当错误码。身高厘米转米统一收在公式内部,杜绝界面层漏除100的风险。体重范围20kg到300kg,身高50cm到250cm,覆盖正常人群及极端体型训练者,但能挡住负数、零和天文数字进入运算。isValidInput单独抽出来,后续做输入框失焦校验可以直接复用。
为什么不选BigDecimal?BMI只需要保留一位小数展示,double的浮点误差远小于展示精度,BigDecimal只会让公式和比较逻辑变得拖沓。没有金额类精度需求,就不要引入高精度运算的负担。
3.2 健康分级:把魔法数字收敛到枚举里
原始源码里最常见的写法是在按钮点击事件里层层if-else判断BMI分界值。结果就是修改分级标准时要逐行翻代码,还要担心边界写成<还是<=。更稳的做法是把区间判断独立成枚举,把“计算”和“判断”拆开。
| 区间 | 国内标准 | WHO标准 |
|---|---|---|
| 偏瘦 | BMI < 18.5 | BMI < 18.5 |
| 正常 | 18.5 ~ 23.9 | 18.5 ~ 24.9 |
| 超重 | 24.0 ~ 27.9 | 25.0 ~ 29.9 |
| 肥胖 | BMI >= 28.0 | BMI >= 30.0 |
public enum Standard { CN, WHO } public enum BmiLevel { UNDERWEIGHT, NORMAL, OVERWEIGHT, OBESE; public static BmiLevel from(double bmi, Standard standard) { if (bmi < 18.5) { return UNDERWEIGHT; } if (standard == Standard.CN) { return bmi < 24.0 ? NORMAL : (bmi < 28.0 ? OVERWEIGHT : OBESE); } return bmi < 25.0 ? NORMAL : (bmi < 30.0 ? OVERWEIGHT : OBESE); } }参数说明:Standard是分级标准枚举,CN与WHO分别对应国内标准与世卫标准。from方法接受BMI值和标准类型,内部只做区间映射,界面层不再关心23.9还是24.9。以后如果产品要求切换标准或增加儿童BMI百分位曲线,只改这里,不碰Activity。这也是这一章标题里“数据模型”的实际含义:把数字判断收敛到一个可替换的领域对象里。
3.3 用单元测试固定规则:test 目录比反复点按钮更快
纯计算逻辑放在app/src/test/java下,运行时不依赖模拟器,一条 Gradle 命令就能验证。这是老zip源码几乎不会带的东西,但对后续改代码的人来说,一组可靠的边界测试比注释更可信。
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; public class BmiCalculatorTest { @Test public void normalCase_shouldReturnExpectedValue() { Double bmi = BmiCalculator.calculate(61.44, 160); assertEquals(24.0, bmi, 0.01); } @Test public void extremeHeight_shouldReturnNull() { assertNull(BmiCalculator.calculate(70, 12)); } @Test public void cnBoundary_overweightStartsAt24() { assertEquals(BmiLevel.OVERWEIGHT, BmiLevel.from(24.0, Standard.CN)); } }assertEquals(24.0, bmi, 0.01)的第三个参数是误差容忍范围,浮点运算存在尾差,不允许误差会写出永远跑不过的脆弱断言。assertNull验证输入边界拦截是否生效。运行命令是./gradlew testDebugUnitTest,测试结果会生成到app/build/reports/tests/testDebugUnitTest/index.html。以后任何人改分级逻辑或公式,只要跑一遍测试就知道有没有碰坏边界。
4. 输入与界面改造:让BMI计算器的源码包更像正式产品
4.1 EditText 的输入约束与空值兜底
布局层面的输入限制先做一道:
<EditText android:id="@+id/et_height" android:inputType="numberDecimal" android:digits="0123456789." android:maxLength="6" />参数说明:inputType="numberDecimal"调起数字键盘,digits="0123456789."进一步过滤字符,只允许数字和小数点。需要注意,digits只能限制键盘输入字符,阻止不了用户在已有1.2后面再补一个.,所以maxLength="6"是防止超长数字的物理手段,真正的格式校验仍要靠解析逻辑。
String heightText = etHeight.getText().toString(); if (heightText.isEmpty()) { etHeight.setError("请输入身高"); return; } double heightCm; try { heightCm = Double.parseDouble(heightText); } catch (NumberFormatException e) { etHeight.setError("身高格式不正确"); return; }流程说明:先查空字符串,再执行parseDouble。parseDouble("1.2.3")会抛NumberFormatException,捕获后以setError提示,不让非法值流动到计算层。即使布局里加了android:digits,try-catch 也必须有,因为代码路径不一定只来自这个输入框。
4.2 结果展示:用进度条表达范围比单行数字更有信息量
源码包里的结果页通常是一行 “您的BMI为24.0”。信息量够,但缺反馈。用水平ProgressBar把BMI值标在健康区间里,用户一眼能看出结果落在偏瘦、正常还是超重区域。
progressBar.setMax(600); int normalized = (int) Math.round((bmi - 10.0) / 45.0 * 600); progressBar.setProgress(Math.max(0, Math.min(600, normalized)));setMax(600)是一个自定义精度因子,配合(bmi - 10.0) / 45.0把BMI的10到55区间映射到0到600的进度范围,进度条位移对BMI变化的敏感度由这个系数决定。Math.max和Math.min做截断,避免BMI超出显示范围时进度条溢出。颜色分段可以用progressTint配合一个简易的ColorStateList,或者用9-patch背景实现绿黄红渐变,视觉层次比单色条清楚得多。
这节真正值得做的是无障碍语义。ProgressBar的contentDescription不建议写“进度60%”,建议写成“体重指数24,处于中国标准下的正常范围”。屏幕阅读器用户最反感听到无意义的进度数字,直接给出结论性描述更有用。
4.3 记录历史:SharedPreferences 与 Room 怎么选
如果只是算完看结果,什么存储都不用加。但很多改造需求希望保留最近几条记录,这时第一反应别是上Room,可以先算算代价。
| 方案 | 适合场景 | 代价 |
|---|---|---|
| SharedPreferences | 存最近10条记录 | 并发写入需串行化 |
| Room | 按时间筛选、图表联动 | 增加依赖与代码量 |
| DataStore | 偏好型数据迁移 | 不适合复杂查询 |
用 SharedPreferences 存一个 JSON 数组,够用且改动小:
private void saveRecord(double bmi, long timestamp) { SharedPreferences sp = getSharedPreferences("bmi_cache", MODE_PRIVATE); String json = sp.getString("records", "[]"); try { JSONArray arr = new JSONArray(json); JSONObject obj = new JSONObject(); obj.put("bmi", bmi); obj.put("time", timestamp); arr.put(obj); while (arr.length() > 10) { arr.remove(0); } sp.edit().putString("records", arr.toString()).apply(); } catch (JSONException e) { e.printStackTrace(); } }MODE_PRIVATE限制本应用才能读取;arr.remove(0)保证历史最多10条,避免JSON体量无限膨胀;apply()异步写盘,不使用主线程同步写文件的commit()。等到真需要按时间查历史或画趋势曲线,再迁到Room也不迟。
5. 打包与验证:拿到这个 zip,先把发布链路补齐
5.1 命令行构建:绕过 Android Studio 的图形界面
拿到zip源码后,第一件事不是打开IDE界面东点西点,而是先验证命令行能不能把产物编出来。图形界面的开关项太多,且报错可能被缓存,命令行输出才是第一手事实。
cd ./bmi_src chmod +x gradlew ./gradlew assembleDebug --stacktracechmod +x gradlew修复Linux/macOS下脚本无执行权限的问题;assembleDebug直接生成app/build/outputs/apk/debug/app-debug.apk;--stacktrace让Gradle在失败时打印完整调用链,而不是只给一句“Build failed”。如果当前目录根本没有gradlew,说明zip缺少Gradle Wrapper,需要从另一个标准工程整体拷贝gradlew、gradlew.bat和gradle/wrapper/三个部分。
5.2 release 签名:不要让 jks 跟着 zip 走
很多源码包里带着作者自己的release.jks,直接用它签出来的包既不能安全上架,也无法在密钥泄露后做版本更新。正确做法是在本地为当前项目生成独立密钥:
keytool -genkey -v -keystore bmi-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias bmi参数说明:-validity 10000约为27年有效期,覆盖常见产品生命周期;-alias bmi是发布后不可修改的别名,后续升级签名必须使用一致参数。生成的bmi-release.jks不应打进任何zip或提交到代码仓库,一旦丢失,已上架的应用将无法覆盖安装升级。
5.3 崩溃日志的本地兜底
如果还没接在线崩溃上报,可以先在Application里加一道兜底:
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { try (FileOutputStream fos = openFileOutput("crash.log", MODE_APPEND)) { fos.write((DateUtils.formatDateTime(this, System.currentTimeMillis(), DateUtils.FORMAT_SHOW_TIME) + " " + Log.getStackTraceString(throwable) + "\n") .getBytes()); } catch (IOException ignored) { } finish(); });MODE_APPEND以追加模式写日志,多次崩溃不会互相覆盖;Log.getStackTraceString把异常堆栈转成字符串,配合时间戳记录现场。测试机上直接拉取崩溃日志:
adb shell run-as 你的包名 cat files/crash.logrun-as仅对debug签名生效,release包建议先adb root(仅模拟器)或导出整个files目录。当桌面又出现一个某某项目_android源码.zip时,先命令行构建,再看崩溃日志,比漫无目的刷logcat高效得多。
本文还有配套的精品资源,点击获取