简介:一份名为“一个工具箱”的安卓应用完整源码包,面向具备基础安卓开发知识、希望进阶学习综合应用开发的程序员,可作为从单一页面走向多模块项目的过渡案例。资源共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: Class<out Fragment> )每个字段的定位很明确:key 是全局唯一的字符串标识,用于页面跳转时传递;name 是展示在列表卡片上的名称;iconRes 是图标资源 ID;fragmentClass 是这个工具对应的 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: MutableLiveData<List<MainListItem>> = MutableLiveData() init { loadItems() } private fun loadItems() { val result = mutableListOf<MainListItem>() ToolRegistry.categories.forEach { category -> result.add(MainListItem.Header(category.title)) category.tools.forEach { tool -> result.add(MainListItem.Tool(tool)) } } items.value = result } }MainListItem 是一个密封类,Header 持有 String,Tool 持有 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 位按秒处理并乘 1000,13 位直接按毫秒处理,这是工具类需求里最常见的边界情况。二维码生成工具则是这套架构里“引入第三方能力”的代表。它单独封装了一个 QrCodeGenerator 工具类,把 ZXing 的编码过程包起来,Fragment 页面只负责获取输入内容、调生成方法、把 Bitmap 设置到 ImageView:object 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 相关异常,这套习惯帮我躲过了很多只在真机上暴露的恢复性问题。希望帮到你。本文还有配套的精品资源,点击获取
简介:一份名为“一个工具箱”的安卓应用完整源码包,面向具备基础安卓开发知识、希望进阶学习综合应用开发的程序员,可作为从单一页面走向多模块项目的过渡案例。资源共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: Class<out Fragment> )每个字段的定位很明确:key 是全局唯一的字符串标识,用于页面跳转时传递;name 是展示在列表卡片上的名称;iconRes 是图标资源 ID;fragmentClass 是这个工具对应的 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: MutableLiveData<List<MainListItem>> = MutableLiveData() init { loadItems() } private fun loadItems() { val result = mutableListOf<MainListItem>() ToolRegistry.categories.forEach { category -> result.add(MainListItem.Header(category.title)) category.tools.forEach { tool -> result.add(MainListItem.Tool(tool)) } } items.value = result } }MainListItem 是一个密封类,Header 持有 String,Tool 持有 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 位按秒处理并乘 1000,13 位直接按毫秒处理,这是工具类需求里最常见的边界情况。二维码生成工具则是这套架构里“引入第三方能力”的代表。它单独封装了一个 QrCodeGenerator 工具类,把 ZXing 的编码过程包起来,Fragment 页面只负责获取输入内容、调生成方法、把 Bitmap 设置到 ImageView:object 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 相关异常,这套习惯帮我躲过了很多只在真机上暴露的恢复性问题。希望帮到你。本文还有配套的精品资源,点击获取