Android工具箱开发:单Activity多Fragment架构实践
2026/9/7 6:31:28 网站建设 项目流程

简介:面向Android开发者的综合工具箱APP源码,定位为涵盖常用工具模块的实践型项目,适合初、中级开发者学习组件协作与功能集成。资源包为RAR压缩格式,共169个文件,以XML布局、Java源码、PNG图标文件为主,另有Gradle构建配置与属性文件,整体体积仅857KB,便于快速下载与解压阅读。源码覆盖文件管理、系统信息、二维码扫描、网络测速等常见工具场景,涉及Activity与Fragment界面搭建、Service后台任务、OkHttp/Retrofit网络请求、SQLite与SharedPreferences存储、运行时动态权限申请等关键技术点;同时包含ConstraintLayout等布局实践与Material Design视觉规范,可帮助读者理解Android综合应用的架构分层。项目工程结构完整,主界面与构建配置齐备,可直接导入Android Studio进行编译调试。目前已有1211人学习下载,适合想要通过完整项目提升工程能力的开发者参考借鉴。

1. 项目概述:要做就做一个“什么都能干”的安卓百宝袋

先聊个很实在的需求。不管你是刚入行的Android开发,还是已经独立接外包几年的老手,手机里一定会常备几类小工具:二维码扫码、单位换算、JSON格式化、文件MD5、随机密码生成……这些小功能单独拎出来,每个都是几行代码的事,但真要临时用的时候,总得专门去应用商店搜一个工具APP,下载完还发现全是广告、权限一大堆。

“一个工具箱”这个项目的出发点特别简单:把高频小工具聚合到一个APK里,离线可用、无广告、体积小、打开即用。从产品形态上看,这类工具聚合APP在国内外都有过不少爆款,本质是靠“低门槛刚需工具+极简交互”来留住用户。而从开发者角度来看,它又是一个特别适合拿来练手的Android实战项目:覆盖了常见的UI组件、文件读写、加密解密、二维码生成识别、传感器调用、系统Intent交互、Service后台任务等大量知识点,而且每个点都比较独立,非常适合做模块化拆分练习。

我这次开源的项目名为“一个工具箱”,源码结构是标准的Android单工程多Module结构,主工程只负责壳和导航,所有工具类功能都按业务域拆成独立代码块,运行架构上采用单Activity多Fragment模式。下面我会把整套代码的设计思路、核心模块的实现细节、踩过的坑和排查过程完整拆解一遍,有需要的朋友可以直接拿去复用。

2. 整体架构设计与技术选型:为什么用单Activity多Fragment

2.1 方案选型背后的理由

做工具箱类APP,第一反应是做一个抽屉式导航,左边一个列表,右边一堆功能页。这里有一个很关键的架构选择:主界面用单Activity+多Fragment,还是每个工具都开一个独立Activity?

我自己一开始是用多Activity的,每个小工具建一个Activity,结果工具数量一多,光AndroidManifest里注册的Activity就有将近30个,来回跳转时的Intent传参复杂度也直线上升,而且因为每个工具页都有自己独立的生命周期和返回栈,用户操作会显得很割裂。后来重构时彻底改了方案:主界面一个MainActivity,里面用Fragment承载每个工具页面,只留少数几个需要独立启动模式的页面(比如需要全屏扫码的界面)才单独开Activity。

单Activity多Fragment这套方案的好处很实际:

  • 返回栈管理统一,用户按返回键的体验跟原生页面一致
  • 所有工具共享一个顶部栏和主题配置,交互风格容易统一
  • 各Fragment之间传值可以通过Activity层做中转,省去大量Intent传参样板代码
  • 内存占用更可控,Fragment的复用和销毁比Activity轻量

不过这套方案也有代价,最大的坑就是Fragment的状态保存问题。后面我会专门讲我踩过的几个和Fragment状态相关的坑。

2.2 依赖注入与工具注册机制

工具箱APP有一个天然的产品需求:工具数量会不断增加,而且每次发版都会增删几个工具。如果每加一个工具都要去改主界面的入口列表、改搜索索引、改分类标签,那维护成本会很快失控。

我的解法是:给所有工具定义统一的接口ToolEntry,每个工具模块用自己的实现类独立注册,并通过一个默认的注解处理器在编译期扫描所有标注了@ToolRegister的类,往一个静态注册表里写索引。这样新增工具只需要两件事:写实现类、加上注解,主界面完全不用动。

核心接口设计大概是这样的:

interface ToolEntry { fun getName(): String fun getIcon(): Int fun getCategory(): ToolCategory fun createFragment(): Fragment } @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class ToolRegister(val id: String)

之所以用编译期注解而不是运行时反射扫描,原因很简单:工具类是固定的,不是动态加载的插件,编译期把它们收拢到一个注册表里,性能和稳定性都比运行时反射强,而且省电(开玩笑,主要是避免挨个反射类名带来的崩溃风险)。

2.3 状态保存和Fragment复用

单Activity多Fragment的模式下,用户从“二维码工具”切到“汇率换算”,再切回来时,原来的输入内容应该还在。这时如果用的是add() + show() + hide()切换Fragment,那么状态天然保留;如果用的是replace(),就得靠FragmentManager的saveState和restoreState来恢复。

我的实现里选择了一个折中方案:使用show()/hide()管理常规工具页,因为工具页本身就轻量,全部常驻内存也才几十个对象,不会带来什么性能问题。只有扫码这种需要独立横竖屏方向的页面,才单独用replace()方式处理。

这里有一个值得注意的细节:多个Fragment使用show()/hide()时,最好给每个Fragment设置独立的tag,并手动记录当前显示的是哪个,因为这个方案在进程被杀后恢复时很容易出现Fragment状态错乱的系统Bug。我在代码里对每个工具Fragment加上了一个自定义state变量,在onSaveInstanceState里手动保存当前工具Id,恢复时再按Id重建。

3. 核心功能模块拆解:工具箱的"四梁八柱"

3.1 工具列表与分类导航

工具列表是主界面的门面,也是用户感知最直接的模块。我设计了双层的展示结构:顶部是全部工具的分类网格(如常用、文件、网络、加密、开发辅助),每个分类下面可折叠地列出所属工具;底部一个搜索框,可以对所有工具的名称和描述做实时过滤。

列表这块用RecyclerView + 多类型Item实现。分类标题是头布局,工具条目是正常的列表项。因为要支持折叠展开,我维护了一个记录每个分组展开状态的有序Map,点击头布局时更新状态并刷新分组内的可见项。

搜索过滤这块有一个细节容易忽略:工具名称往往很短,用户可能只记得"二维码"或"MD5"这种词,所以要同时匹配名称和描述字段。我直接在ToolEntry接口里加了getDescription()方法,搜索时对名称和描述做双重包含匹配,实测下来比只搜名称的体验好很多。

3.2 通用工具容器页

上面提到所有工具通过ToolEntry.createFragment()创建页面,但这只是在主界面入口处使用。耗内存比较大的工具(比如"系统清理"这种需要扫描全盘文件的),更应该做懒加载:只在用户真正点开时初始化,而不是在主界面启动阶段就一次性创建全部子Fragment。

懒加载方案不复杂,用一个懒加载容器Fragment作为所有工具页的宿主,容器对外按工具Id创建对应的子Fragment,容器内用childFragmentManager管理。关键是要在容器所在页面可见时才触发真正的数据加载,我用的是setUserVisibleHint + 生命周期回调双重判断,避免在ViewPager预加载时误加载数据。

3.3 各工具模块的公共基建

既然工具种类众多,肯定会有大量重复代码:检查权限、Toast提示、复制到剪贴板、打开系统分享面板……这些我全部抽成了BaseFragment里的公共方法。子类只管业务逻辑,不用每次写权限请求那一套样板代码。

abstract class BaseFragment : Fragment() { protected fun requestPermission(permission: String, callback: (Boolean) -> Unit) { // 使用Activity Result API包装权限请求 } protected fun showToast(msg: String) { // 统一Toast样式 } protected fun copyToClipboard(text: String) { val cm = requireContext().getSystemService(ClipboardManager::class.java) cm.setPrimaryClip(ClipData.newPlainText(null, text)) } protected fun shareText(text: String) { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, text) } startActivity(Intent.createChooser(intent, null)) } }

这块看起来简单,但却是整个项目里最实用的部分。等工具数量超过十个以后,这些公共方法的复用价值会体现得淋漓尽致。

4. 典型工具模块的开发实录

4.1 文件校验工具:MD5、SHA1、SHA256

文件类工具是工具箱里需求量比较大的一个,主要功能是计算文件的MD5、SHA1、SHA256值,方便用户在下载大文件后校验完整性。

实现思路不难,核心是分块读取,因为大文件不能一次性读入内存,否则容易OOM。我用流式计算的方式,每读8KB就更新一次MessageDigest:

fun calculateFileHash(file: File, algorithm: String): String { val digest = MessageDigest.getInstance(algorithm) FileInputStream(file).use { fis -> val buffer = ByteArray(8192) var len = fis.read(buffer) while (len > 0) { digest.update(buffer, 0, len) len = fis.read(buffer) } } return digest.digest().joinToString("") { "%02x".format(it) } }

这里有一个特别容易踩的坑:Android 7.0(API 24)开始,直接使用file:// URI打开系统文件选择器会抛FileUriExposedException。所以文件选择部分必须用FileProvider转换URI。我封装了一个FilePickerFragment,用Storage Access Framework打开系统文档选择器,用户选完文件后拿到的是一个content:// URI,再通过ContentResolver打开输入流,这样既绕开了FileUriExposedException,又天然适配了分区存储。

4.2 文本工具:JSON格式化与时间戳转换

JSON格式化是开发者高频工具,我直接基于org.json库做了两层封装:输入原始JSON字符串,点击格式化后输出带缩进的等级结构;如果JSON解析出错,会提示错误信息并尽量精确到出错位置。

时间戳转换工具做成了双向的:支持10位秒级和13位毫秒级输入,输出可读日期;反过来也支持把日期字符串转回时间戳。实现时有一个容易忽略的时区问题,默认解析使用系统默认时区,但也可以手动指定GMT+8或UTC,避免跨时区用户看到的结果和自己预期的不一样。

4.3 编码工具:Base64编解码与URL编解码

这类工具代码量很少,但产品上有个细节值得说一下:Base64编解码要做到"自动判断输入内容是文本还是Base64",并在界面上给出明确的转换方向提示,而不是让用户自己选“编码/解码”按钮。因为在真实使用场景里,用户只知道自己有一串莫名奇妙的字符,需要知道它到底是什么。我在输入框内容变化时做了一次探测:如果输入串符合Base64字符集且长度是4的倍数,优先展示“解码”结果,同时保留“编码”入口。这样做用户体验会顺滑很多。

4.4 随机工具:密码生成器

密码生成器是我个人比较偏爱的一个小模块。它支持自定义长度、字符集合(大写、小写、数字、特殊符号),并阻止用户选择不含任何字符集的空配置。实现时要做的关键操作用SecureRandom而不是Random,因为密码场景下的随机数安全性很重要。

fun generatePassword( length: Int, useUpper: Boolean, useLower: Boolean, useDigit: Boolean, useSpecial: Boolean ): String { val allChars = buildString { if (useUpper) append("ABCDEFGHIJKLMNOPQRSTUVWXYZ") if (useLower) append("abcdefghijklmnopqrstuvwxyz") if (useDigit) append("0123456789") if (useSpecial) append("!@#\$%^&*()-_=+[]{};:,.?") } require(allChars.isNotEmpty()) { "至少选择一种字符类型" } val secureRandom = SecureRandom() return buildString { repeat(length) { val index = secureRandom.nextInt(allChars.length) append(allChars[index]) } } }

这里还有个坑:虽然安全上优先使用SecureRandom,但它生成密码时容易连续出现多个同类字符,比如全数字或全大写。为了保证生成的密码在不同字符集合间尽量均匀分布,我采用了“先确保每种选中字符类型至少出现一次,再随机填充剩余位,最后洗牌”的策略,实战效果好很多。

4.5 单位换算工具:长度、重量、温度

单位换算的逻辑其实是一个动态公式引擎。我在代码里没有为每一种单位组合写switch-case,而是给每个单位定义了一个换算系数和偏移量,统一转成基准单位,再换算到目标单位:

data class UnitDef(val name: String, val factor: Double, val offset: Double = 0.0) fun convert(value: Double, from: UnitDef, to: UnitDef): Double { val baseValue = value * from.factor + from.offset return (baseValue - to.offset) / to.factor }

温度这个稍微特殊一点,因为摄氏、华氏、开尔文之间是线性关系但基点和斜率不同,我也统一用上面的模型处理了。这套设计的扩展性在于:以后如果要加压力、功率等新类别,只需注册对应的UnitDef列表,不需要改任何换算逻辑。

5. 打包发布、混淆策略与常见问题排查

5.1 ProGuard/R8混淆规则

工具箱这类开放源码的项目有一个特点:代码结构对用户可见,所以混淆的优先级不在于防破解,而在于减小包体和避免反射问题。我用的是R8全量模式,并按模块维护了不同的keep规则:

-keep class com.toolbox.entry.** { *; } -keep class com.toolbox.api.** { *; } -keepclassmembers class * { @com.toolbox.annotation.ToolRegister <fields>; }

注意工具注册相关类一定要keep,因为编译期注解生成的索引在运行时要通过反射读取类名,混淆后类名一变就会找不到对应工具导致崩溃。

5.2 存储路径适配:分区存储的坑

项目里有几个工具会访问外部存储,比如“文件清理”和“大文件扫描”。在Android 10之前可以直接用Environment.getExternalStorageDirectory(),Android 10以后分区存储强制生效,直接访问路径大概率会报权限拒绝。我的做法是优先走MediaStore API,只有处理自有目录或用户明确选择的文件时才用原始路径。另外在Android 11及以上还需要在 里声明 ,并跳转到系统设置页引导用户授予“所有文件访问”权限,否则MediaStore查不到全部文件。

5.3 崩溃排查实录:扫描文件时的OOM

项目开发过程中,我最头疼的一个问题是大文件扫描时经常OOM。原因很典型:扫描目录栈里保存了太多DirectoryInfo对象,每个对象还持有子文件列表,累积起来稳超内存阈值。后来我把递归遍历改成迭代式,并且只保存文件路径字符串,不持有文件对象,内存占用一下降了约70%。这个改动也顺带解决了扫描进度不好更新的问题,因为迭代式循环里可以精准插入进度回调。

5.4 二维码识别模块的常见Bug

二维码扫码模块用的是ZXing核心库的精简版。常见问题有两个:一是相机权限被拒导致黑屏,二是对焦后仍然无法解析模糊图片。我的处理方式:权限申请不通过时显示“无法使用相机”的引导页,并附跳转设置页的按钮;对焦问题则把CameraManager里连续对焦的触发事件绑定到预览回调上,同时允许点击预览界面手动对焦。

有一点要特别提醒:ZXing的扫码页面在全屏模式下,预览图像是有拉伸的,不同的屏幕比例下二维码的识别区域和预览画面可能对不上。我的解决方案是在解析图中指定解码区域,用取景框矩形做个裁剪,识别率会明显提升。

6. 工具链与依赖选型的心得

架构说完了,把依赖选型这块的心得也分享一下。工具箱APP不适合无脑引入重型框架,因为工具功能散而轻,框架太重反而拖慢启动速度、增加包体、提升混淆出错率。我的核心依赖只有这几个:

依赖用途为什么不换
AndroidX + Material ComponentsUI基础官方标准,跟随大版本更新
ZXing core二维码、条形码解析体量小,只编译核心模块,不引入相机UI
kotlinx.coroutines后台任务与线程切换比手写线程池优雅
Coil图片加载如果用到图片展示的话,轻量且兼容性好

原计划里也有Retrofit和OkHttp,后来发现工具箱里真正涉及网络请求的工具非常少,大多数功能本地就能完成,因此网络层只保留了一个可选的HTTPS接口,用来做版本更新检查,用HttpURLConnection就够了,没有引入完整网络库的必要。

另外,数据库方案我最终没有用Room,因为工具箱里几乎没有复杂关系型数据,唯一用到存储的是“历史记录”和“自定义工具配置”,用SharedPreferences配合简单对象序列化就能满足需求。少一层ORM意味着少一个编译期注解处理器,构建速度也会快一些。

7. 项目目录结构参考

下面是我实际维护的目录结构,大家可以参考一下这种按业务模块划分的方式,好处是每新增一个工具类别,不会牵动头部代码。

app/ src/main/java/com/toolbox/ base/ BaseFragment.kt BaseToolEntry.kt entry/ QrCodeToolEntry.kt HashToolEntry.kt JsonToolEntry.kt UnitConvertToolEntry.kt ... framework/ FilePickerFragment.kt PermissionHandler.kt ToolRegistry.kt ui/ main/MainActivity.kt main/ToolListFragment.kt tools/ hash/HashFragment.kt qrcode/QrCodeFragment.kt json/JsonFormatFragment.kt ...

主目录里每一类工具对应一个pkg,包名和工具Id保持一致,这样检索源码时非常高效。

8. 常见问题速查表

问题现象排查思路解决方案
Fragment状态错乱进程被系统回收后重建onSaveInstanceState中手动保存当前工具Id
文件选择器崩溃7.0以上使用file:// URI改用FileProvider或SAF
模糊二维码识别不了预览图像拉伸或对焦不准裁剪取景框区域,支持手动对焦
大文件Hash耗时主线程卡顿放协程IO线程,用8KB分块更新
混淆后工具不显示工具注册类名被混淆keep注册类和注解字段
扫描文件OOM递归遍历持有太多对象改为迭代式栈遍历,只存路径串

最后说点实际的

“一个工具箱”这个项目从立项到开源,前前后后改了三个大版本。最大的体会是:工具类APP看似功能零散,但实际上对代码组织能力的要求比想象中高很多。每一次新增工具,如果都要动主界面代码、改注册逻辑、改搜索索引,时间久了必然维护不下去。好的架构不是堆多少框架,而是后续加功能不加负担。

如果你准备拿这个项目练手,我建议从克隆源码开始,先跑起来,再加一个自己最常用的工具模块,对比一下代码需要动哪些地方。等你加完三五个工具、踩过几次坑之后再回头看架构设计,会比直接读完这篇分享更有收获。后续我还会继续补充更多工具模块,有想一起加功能或者提需求的,随时提过来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询