Android工具箱源码:单Activity多Fragment架构与工具注册机制
发布时间:2026/10/19 21:34:41 锦皓数字建站

简介一份名为“一个工具箱”的安卓应用完整源码包面向具备基础安卓开发知识、希望进阶学习综合应用开发的程序员可作为从单一页面走向多模块项目的过渡案例。资源共169个文件压缩包约857KB包含56个XML布局文件负责界面定义、54个Java源文件承载功能逻辑、44张PNG图片提供图标素材另有Gradle构建脚本与权限配置目录结构清晰便于按模块查阅。项目集成文件管理、系统信息查看、二维码扫描、网络测速等实用工具覆盖Intent组件通信、广播接收、内容提供者、动态权限申请、SQLite存储、OkHttp网络请求、事件总线与异步任务调度等核心知识点。界面部分展示了ConstraintLayout等布局与Material Design规范并涉及自定义View、RecyclerView列表、Glide图片加载、内存管理与动画效果的实践。已有1211人学习下载适合拆解多模块应用的架构设计与性能优化思路提升安卓工程能力。1. 一个工具箱几十个高频小工具装进一个 APK 的源码样本日常开发里经常遇到这种场景手机里为了一个“单位换算”装一个 APP为了“二维码生成”再装一个时间戳转换还要开网页。真正动起手来不如自己维护一个聚合型工具箱把高频小工具全部收进同一个工程里。这份“Android工具箱APP源码”就是这样一个项目——它不依赖复杂架构而是用一套工具注册机制管理几十个 Fragment每加一个新工具只需写一个页面类再注册一行配置即可。对想快速出一版工具合集、学习单 Activity 多 Fragment 组织方式、或者需要一套可二次开发的工具面板源码的开发者来说这套结构能省不少时间。2. 技术选型与目录结构Material Design 组件与单 Activity 多 Fragment 的分层方式2.1 技术栈全景Kotlin ViewBinding 为主第三方依赖控制在三个以内这份源码整体采用 Kotlin 编写UI 部分基于 Android 官方 Material Components 库实现布局文件与逻辑代码之间通过 ViewBinding 绑定避免了一堆 findViewById 的样板代码。工程构建用的是 Gradle Kotlin DSL版本上对应 Android Gradle Plugin 7.x 与 Gradle 7.4 这一档兼容现在主流 Android Studio 版本。第三方依赖控制得很克制只有三个方向二维码生成使用的是 ZXing 核心库的 QRCodeWriter不引入整个扫码包只取生成部分体积小。Material 组件库负责控件样式主面板的滚动列表、分类头、卡片效果都依赖它。RecyclerView 作为列表容器搭配一个自定义适配器渲染工具项。没有引入网络框架没有数据库因为工具箱这类项目离线可用是最重要的体验。所有工具项的配置都写在内存常驻的注册表里启动时读取一次运行时状态靠 Fragment 自己维护。选这套组合的原因很直接单模块工程结构简单新手改起来不迷路老手拿到手也能快速替换自己的页面。提示如果之前只做过单页 Demo这个工程可以作为理解“单 Activity 多 Fragment 容器”的参考样本后面加工具时不需要改 MainActivity 任何逻辑。2.2 工程目录逐层拆解从 app 模块到工具页面的包位置拿到源码后首先看目录这个工程是标准的单模块结构没有多余的 library 工程。主代码全部位于 app/src/main/java/com/mock/toolbox 包下面按职责划分成四个子包app/src/main/java/com/mock/toolbox/ ├── MainActivity.kt // 主面板入口承载工具列表 ├── ToolHostActivity.kt // 通用工具容器动态装载 Fragment ├── data/ │ ├── ToolEntry.kt // 工具项数据模型 │ ├── Category.kt // 分类模型一组工具项的集合 │ └── ToolRegistry.kt // 工具注册表集中配置所有工具 ├── ui/ │ ├── MainAdapter.kt // 列表适配器渲染分类头与工具项 │ ├── MainViewModel.kt // 主面板 ViewModel持有列表数据 │ └── tool/ │ ├── TimestampFragment.kt // 时间戳转换工具页 │ ├── QrCodeFragment.kt // 二维码生成工具页 │ ├── BubbleLevelFragment.kt // 水平仪工具页 │ └── RandomPickerFragment.kt // 随机数生成工具页 ├── util/ │ ├── QrCodeGenerator.kt // 封装 ZXing 二维码生成逻辑 │ └── TimeUtils.kt // 时间格式转换工具方法 └── res/layout/ ├── activity_main.xml // 主面板布局一个 RecyclerView ├── activity_tool_host.xml // 工具容器布局一个 FragmentContainerView ├── item_tool.xml // 单个工具项卡片布局 ├── item_category_header.xml // 分类标题布局这个包结构里最值得留意的是 data 包与 ui/tool 包的边界data 包里的 ToolRegistry 不依赖任何 Activity 或 Fragment它只维护“有哪些工具、叫什么名字、对应哪个类”ui/tool 包下的每个 Fragment 则完全不知道其他工具的存在各自实现各自的业务逻辑。这样划分之后新增工具不会碰到主面板代码删除工具只需要从注册表里移除一条配置。2.3 工具注册机制用“元数据 反射实例化”替代 if-else 跳转最初的版本里主面板的点击事件是维护一个 when(key) 分支每加一个工具就多一条 case十几条分支之后代码散发着难闻的味道。这份源码的做法是把“工具”抽象成一条元数据再通过反射创建对应的 Fragment 实例。工具项的数据模型长这样data class ToolEntry( val key: String, val name: String, val iconRes: Int, val fragmentClass: Classout Fragment )每个字段的定位很明确key 是全局唯一的字符串标识用于页面跳转时传递name 是展示在列表卡片上的名称iconRes 是图标资源 IDfragmentClass 是这个工具对应的 Fragment 字节码对象。这种设计让工具项变成纯数据列表只需要遍历数据源容器只需要解析 key 找到 entry。注册表则把分类和工具项组织成“两级列表”代码中大致是这种形式object ToolRegistry { val categories listOf( Category( title 常用工具, tools listOf( ToolEntry(timestamp, 时间戳转换, R.drawable.ic_time, TimestampFragment::class.java), ToolEntry(qrcode, 二维码生成, R.drawable.ic_qr, QrCodeFragment::class.java), ToolEntry(bubble, 水平仪, R.drawable.ic_bubble, BubbleLevelFragment::class.java) ) ), Category( title 随机工具, tools listOf( ToolEntry(random, 随机数生成, R.drawable.ic_random, RandomPickerFragment::class.java) ) ) ) fun find(key: String): ToolEntry? { categories.forEach { category - category.tools.forEach { tool - if (tool.key key) return tool } } return null } }这里有个值得注意的细节fragmentClass 存的是类对象而不是字符串类名。保存类对象的好处是编译期就能检查类是否存在避免运行到一半才发现类名写错反射只是通过 newInstance 创建实例成本可以忽略。实际点击跳转时MainActivity 只做一件事——把 key 传给 ToolHostActivity所有工具页的加载逻辑都收拢到容器里这是整个工具箱扩展性的根基。3. 核心模块拆解从点击工具到 Fragment 容器渲染的完整链路3.1 主面板列表RecyclerView 双条目类型实现分类与工具混排主面板 MainActivity 的布局极其简单只有一个 RecyclerView 撑满屏幕。列表数据由 MainViewModel 对外暴露一个“列表条目”集合这个集合同时包含两种类型——分类标题对象和工具项对象以此实现在同一个列表里交替展示分类头与卡片项。适配器内部通过 getItemViewType 区分两种条目onCreateViewHolder 里根据类型 inflate 不同的布局。工具卡片的点击回调做一个转发把被点击项的 key 收集起来统一交给 Activity 层启动 ToolHostActivity。Fragment 的创建被刻意延迟到容器中列表侧不持有任何 Fragment 引用保证工具项元数据可以被轻松复用和测试。在实际的列表构造代码中MainViewModel 会做一次数据映射操作class MainViewModel : ViewModel() { val items: MutableLiveDataListMainListItem MutableLiveData() init { loadItems() } private fun loadItems() { val result mutableListOfMainListItem() ToolRegistry.categories.forEach { category - result.add(MainListItem.Header(category.title)) category.tools.forEach { tool - result.add(MainListItem.Tool(tool)) } } items.value result } }MainListItem 是一个密封类Header 持有 StringTool 持有 ToolEntry。这样设计的好处是适配器的 onBindViewHolder 不需要做“instanceof 判断后强转”以外的复杂逻辑数据源本身已经是排好序的扁平结构。如果后续想调整分类顺序只需要改变注册表 categories 的顺序列表数据会同步变化。3.2 工具容器一个 Activity 管理所有工具 Fragment 的创建与传参ToolHostActivity 是这份源码里承担“路由”职责的类。它接收两个输入用于定位工具的唯一 key和需要传递给工具页的初始参数。页面布局中有一个 FragmentContainerView 作为 Fragment 的占位容器。该 Activity 的核心逻辑集中在 onCreate 中class ToolHostActivity : AppCompatActivity() { companion object { private const val EXTRA_KEY extra_tool_key private const val EXTRA_PARAM extra_tool_param fun start(context: Context, toolKey: String, param: String? null) { val intent Intent(context, ToolHostActivity::class.java) intent.putExtra(EXTRA_KEY, toolKey) intent.putExtra(EXTRA_PARAM, param) context.startActivity(intent) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_tool_host) val toolKey intent.getStringExtra(EXTRA_KEY) ?: run { finish(); return } val entry ToolRegistry.find(toolKey) ?: run { finish(); return } supportActionBar?.title entry.name val fragment entry.fragmentClass.newInstance() val bundle Bundle().apply { putString(tool_key, toolKey) putString(tool_param, intent.getStringExtra(EXTRA_PARAM)) } fragment.arguments bundle supportFragmentManager.commit { replace(R.id.tool_container, fragment) } } }这整段流程的关键点有三个一是传入的 key 找不到对应工具时直接 finish避免空白页写在主线程上二是 Fragment 实例通过无参构造器创建参数全部挂在 arguments 上这样旋转屏幕后系统重建 Fragment 时能正确恢复三是 replace 操作使用 commit 系扩展函数处于 ktx 标准写法。参数传递上采用 Bundle 方案而不是在 Fragment 中暴露 setter 方法。原因很现实Activity 被系统回收后重建时Fragment 的 arguments 会原样保留而 setter 设置的字段必然丢失这是 Fragment 状态恢复机制中最常见的坑。3.3 典型工具实现以时间戳转换和二维码生成为例时间戳转换工具可以说是“工具箱必备选手”逻辑简单但覆盖了 Fragment 页面生命周期的全套模式。页面包含一个 EditText 输入框、一个转换按钮、一个结果展示 TextView。用户输入 10 位秒级或 13 位毫秒级时间戳点击按钮后执行转换class TimestampFragment : Fragment() { private var _binding: FragmentTimestampBinding? null private val binding get() _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding FragmentTimestampBinding.inflate(inflater, container, false) binding.btnConvert.setOnClickListener { convertTimestamp() } return binding.root } private fun convertTimestamp() { val input binding.etTimestamp.text.toString().trim() if (input.isEmpty()) { binding.tvResult.text 请输入时间戳 return } val timestamp parseTimestamp(input) val date SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.getDefault()) .format(Date(timestamp)) binding.tvResult.text date } private fun parseTimestamp(input: String): Long { val raw input.toLongOrNull() ?: return 0L return if (input.length 10) raw * 1000 else raw } }这段代码里有两个值得记的行为第一个是 _binding 声明为可空、对外用非空代理目的是在 Fragment 销毁时把绑定对象置空避免内存泄漏第二个是时间戳位数判断10 位按秒处理并乘 100013 位直接按毫秒处理这是工具类需求里最常见的边界情况。二维码生成工具则是这套架构里“引入第三方能力”的代表。它单独封装了一个 QrCodeGenerator 工具类把 ZXing 的编码过程包起来Fragment 页面只负责获取输入内容、调生成方法、把 Bitmap 设置到 ImageViewobject QrCodeGenerator { fun generate(content: String, size: Int 512): Bitmap? { val hints mapOf( EncodeHintType.CHARACTER_SET to UTF-8, EncodeHintType.MARGIN to 1 ) return try { val matrix QRCodeWriter().encode( content, BarcodeFormat.QR_CODE, size, size, hints ) val bitmap Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565) for (x in 0 until size) { for (y in 0 until size) { bitmap.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE) } } bitmap } catch (e: WriterException) { null } } }生成流程可以拆成三个阶段配置参数阶段通过 hints 设置字符集和四周留白编码阶段调用 encode 得到 BitMatrix 矩阵像素填充阶段遍历矩阵的每个坐标点按黑白状态设置 Bitmap 的像素颜色。如果输入文本过长超出二维码容量encode 会抛 WriterException外层捕获后返回 null页面再做一次判空提示即可。3.4 参数约定工具项尺寸、图标资源与描述字段对接说明一套工具源码如果只给模型不给约定二次开发者很容易踩错。这个工具箱在数据层规定了三个明确约定加工具时按此执行即可字段类型约定说明keyString全局唯一建议用英文小写与下划线如 random_passwordnameString列表显示的可见名称最多 8 个中文字符iconResInt引用 drawable 图标推荐 24dp 的 vector 图标fragmentClassClass必须继承 Fragment且提供无参构造器图标资源这一项容易忽略。列表卡片是 96dp 高度如果直接丢一张 512px 的 PNG 进去加载是能加载但内存占用和首屏速度都会变差。源码中的工具图标全部采用 vector drawable一套 XML 描述矢量路径体积小且适配不同屏幕密度。新增工具时如果暂时找不到合适图标可以先从 Material Icons 官方库复制一个 vector 资源。主面板列表的卡片布局里有几个固定元素左侧图标、中间工具名称、右侧一个箭头指示符。名称过长时通过 ellipsize 截断避免撑破卡片导致列表滚动出现跳动。4. 避坑清单编译通过不等于真机可用四条典型翻车记录4.1 问题一ViewBinding 类生成失败布局文件找不到现象同步工程后写 binding.inflate 的地方全部报错“Unresolved reference: ActivityMainBinding”但 res/layout 下的布局文件明明存在。原因buildFeatures 里没有开启 viewBinding或者开启语句写错了位置。Android Studio 新建工程模板默认不开这个开关拿到源码第一件事先检查 app 模块的 build.gradle.kts 有没有下面这段android { buildFeatures { viewBinding true } }解决把配置加到 android 块内部而不是 dependencies 里然后重新 Sync 工程。如果加了配置后依旧报错检查 styles.xml 主题是否以 Theme.MaterialComponents 开头Material 组件库的命名空间会影响部分布局属性解析但不影响绑定类生成可以先排除这个干扰项。注意viewBinding 开启后每个布局都会生成一个绑定类即使页面没有用到也必须保留布局文件否则编译期会报资源缺失错误。4.2 问题二Android 12 及以上安装后启动闪退现象targetSdkVersion 31 或更高安装成功但点击图标后进程直接崩溃日志中能看到引发致命异常的语句指向 Activity 的启动校验。原因Android 12 开始如果 Activity 在 Manifest 中声明了 intent-filter必须同时声明 android:exported这个属性用于告知系统该 Activity 是否可以被其他应用唤醒。主面板作为入口 Activity 含有 LAUNCHER intent-filter漏写 exported 会被系统拒绝启动。解决给 MainActivity 和 ToolHostActivity 分别检查 Manifest 声明。MainActivity 设为 true 是必然的ToolHostActivity 因为被应用内部显式 Intent 启动同样需要设置 true两个都别落下否则配置了 exported 但仍用隐式 Intent 拉起另当别论这里都是显式跳转所以没问题。4.3 问题三ZXing 二维码生成出来扫不出内容现象生成的二维码图片能显示但扫码枪和手机扫码都识别失败或者识别出乱码。原因这是字符集和容错等级配置不全导致的典型翻车。只传了二维码内容没有传 CHARACTER_SET 和 ERROR_CORRECTION 参数ZXing 在某些设备上默认编码行为不稳定。另一个隐蔽原因是设置 MARGIN 为 0二维码四周没有任何留白图像边界处像素被裁剪扫码时定位角点残缺。解决按源码中 QrCodeGenerator 的写法设置 UTF-8 和 MARGIN 为 1并保持默认容错等级。如果二维码里包含中文或特殊符号字符集参数是必须显式指定的否则部分三星和鸿蒙机型会复现这个问题。4.4 问题四Fragment 旋转屏幕后恢复崩溃现象工具页中旋转手机瞬间闪退日志提示“Unable to instantiate fragment ... could not find Fragment constructor”。原因Fragment 在页面重建时通过反射调用无参构造器创建新实例。如果工具类里写了带参数的构造函数比如 QrCodeFragment(size: Int)这个有参构造不会出现在默认构造列表中系统反射 newInstance 时直接抛异常。解决所有工具 Fragment 一律保持默认无参构造器页面所需的初始化参数全部通过 arguments Bundle 传入。在 companion object 里写一个 newInstance(key, param) 静态方法方法内部创建实例并挂载 Bundle这是 Fragment 规范写法的底线约束。修改之后旋转屏幕的恢复逻辑由 FragmentManager 自动处理。4.5 问题五工具数量超过 20 个后首屏加载卡顿现象注册表工具项增加到 20 个以上主面板打开时明显掉帧进入列表要等一两秒才能滑动。原因主线程上加载了过多的位图资源或列表数据组装过程中把 Fragment 直接实例化。虽然 ToolRegistry 里只存 Class 对象不会触发加载但在 onCreateViewHolder 里如果有额外 init 操作比如预创建 Fragment 实例或读取大图标就会拖慢首屏。解决严格保持四个原则条目只绑定元数据图标全部使用 vector 类型列表滚动时不做任何反射调用Fragment 延迟到 ToolHostActivity 的 replace 中创建。如果后续工具量级再扩大一个数量级可以考虑用 RecyclerView 的复用池和协程异步预加载图标做进一步优化。5. 二次开发半小时新增一个工具并完成真机验证拿到这套源码后最实际的改造是“添加一个自己的工具”。以新增“JSON 格式化工具”为例整个过程只要走四步第一步创建一个 JsonFormatFragment 类页面放一个 EditText 输入原始 JSON一个按钮触发格式化一个 TextView 显示美化结果第二步在 ToolRegistry 的合适分类下追加一行 ToolEntry指定 key 为 json_format、名称“JSON 格式化”、图标复用现有 vector 资源、fragmentClass 指向新页面类。第三步是关键——把工具类文件放到 ui/tool 包下同时确认 Fragment 只有无参构造器输入内容与输出结果都定义成页面内的局部状态。格式化逻辑本身没有难度无非是新写一个 JSON 解析与重新序列化合并的过程。如果不想新建 Fragment也可以直接复制现有 TimestampFragment 改造但要注意把 binding 类文件名的前缀一并替换否则 ViewBinding 生成的类不存在。完成代码修改后进行验证有三步必做编译安装后逐个点击所有工具确认新工具能打开、返回后列表位置不跳动进入新工具输入一条测试数据执行格式化再旋转屏幕确认结果没有丢失最后用 adb 命令观察崩溃日志重点确认 Fragment 生命周期中没有异常重建adb logcat -v time | grep -E (FATAL|AndroidRuntime|toolbox)这套源码最实用的地方在于它把“工具”这个业务概念拆成了注册表 容器 页面的三段结构新增功能时不需要碰列表适配器也不需要碰主 Activity。从那以后我每次新增工具都强制走一遍“编译 → 安装 → 逐个点击 → 返回重进”的完整流程顺手看一遍日志确认没有 FragmentManager 相关异常这套习惯帮我躲过了很多只在真机上暴露的恢复性问题。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。