十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android工具箱开发:单Activity多Fragment架构实践

Android工具箱开发:单Activity多Fragment架构实践 简介面向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多Fragment2.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就更新一次MessageDigestfun 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.0API 24开始直接使用file:// URI打开系统文件选择器会抛FileUriExposedException。所以文件选择部分必须用FileProvider转换URI。我封装了一个FilePickerFragment用Storage Access Framework打开系统文档选择器用户选完文件后拿到的是一个content:// URI再通过ContentResolver打开输入流这样既绕开了FileUriExposedException又天然适配了分区存储。4.2 文本工具JSON格式化与时间戳转换JSON格式化是开发者高频工具我直接基于org.json库做了两层封装输入原始JSON字符串点击格式化后输出带缩进的等级结构如果JSON解析出错会提示错误信息并尽量精确到出错位置。时间戳转换工具做成了双向的支持10位秒级和13位毫秒级输入输出可读日期反过来也支持把日期字符串转回时间戳。实现时有一个容易忽略的时区问题默认解析使用系统默认时区但也可以手动指定GMT8或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二维码、条形码解析体量小只编译核心模块不引入相机UIkotlinx.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看似功能零散但实际上对代码组织能力的要求比想象中高很多。每一次新增工具如果都要动主界面代码、改注册逻辑、改搜索索引时间久了必然维护不下去。好的架构不是堆多少框架而是后续加功能不加负担。如果你准备拿这个项目练手我建议从克隆源码开始先跑起来再加一个自己最常用的工具模块对比一下代码需要动哪些地方。等你加完三五个工具、踩过几次坑之后再回头看架构设计会比直接读完这篇分享更有收获。后续我还会继续补充更多工具模块有想一起加功能或者提需求的随时提过来。本文还有配套的精品资源点击获取
返回列表