
调试对象这件事做 Flutter 开发的朋友应该都深有体会日志里打出来的对象是一串看不懂的内存地址Instance of UserInfo想确认某个字段到底赋没赋值只能手动去点断点、翻变量表或者写一大堆手拼字符串的toString()。要命的是这种模板代码跟业务代码耦合在一起加一个字段就要同步改一处漏改了又是一阵排查。class_to_string 这个库我关注了很久它的思路是把对象打印这件事自动化用注解配合代码生成自动帮你生成格式化后的toString()实现。最近我在做鸿蒙端的适配落地整个过程踩了不少坑也积累了一点经验这篇就把它完整拆开讲讲。文章适合正在做鸿蒙应用开发、或者准备把已有 Flutter 工程往鸿蒙迁移的朋友参考核心是怎么让对象调试在鸿蒙端不再盲目。1. 为什么需要 class_to_string先搞懂这个库解决的问题1.1 传统 toString() 写法的三个痛点先回顾一下常规操作。业务里有个用户信息类大概长这样class UserInfo { final String name; final int age; final ListString tags; final MapString, dynamic extra; }想打印它的完整内容通常有两种做法。第一种是手动重写toString()把每个字段拼进去。写法倒是不复杂override String toString() { return UserInfo(name: $name, age: $age, tags: $tags, extra: $extra); }问题在于这个类字段一多拼起来就特别啰嗦而且每加一个字段都要回来改这处代码。更麻烦的是如果项目里这种 DTO 类有几十个每个类都要维护这么一段纯粹是体力活。第二种做法是依赖 IDE 的生成功能让工具帮你把toString()自动生成出来。这样省了手写的功夫但生成一次就固定了后面新增字段还要重新生成或者手动补。本质上的问题没有解决一旦字段变化toString()的维护成本和遗漏风险是持续存在的。用起来还有个隐性痛点就是拼接格式不统一。有人用UserInfo(name: xxx, age: 18)这种带字段名的风格有人直接返回 JSON 字符串还有人把字段用逗号一拼不带标签。日志格式不统一在排查问题的时候很费劲尤其是从线上日志里 grep 某个对象的时候不同的格式意味着不同的检索技巧。1.2 class_to_string 的核心设计思路class_to_string 的解决方式很直接用注解声明一个类“需要生成 toString()”然后通过代码生成器在编译前自动产出格式化好的toString()实现。平时只需要写一行注解import package:class_to_string/class_to_string.dart; ToString() class UserInfo { final String name; final int age; final ListString tags; final MapString, dynamic extra; }跑完代码生成命令之后它会在同目录下生成一个user_info.g.dart文件里面带着自动生成的toString()。关键点是生成逻辑是读源代码里的字段定义来的所以类里有什么字段生成结果就包含什么字段天然跟最新代码保持一致不会出现手写版本那种忘了补新字段的情况。这个机制要解释一下为什么它能自动感知字段因为代码生成器会先对源文件做静态分析拿到当前类里声明的所有字段、类型、泛型信息再按模板拼接出toString()的完整实现。这跟手写最大区别在于字段清单是分析出来而不是凭记忆填的所以漏字段的概率大幅下降。1.3 鸿蒙化适配到底在适配什么聊到鸿蒙很多人的第一反应是“Flutter 能跑在鸿蒙上吗”。这个问题现在基本不是问题了不管是开源鸿蒙还是商用发行版都有对应的 Flutter 引擎适配方案开发者可以用 DevEco Studio 配合专门的 Flutter SDK 来构建鸿蒙 Flutter 应用。但“能跑 Flutter”和“三方库能正常用”是两回事鸿蒙化适配的核心工作就是解决三方库在鸿蒙的 Flutter 运行环境里能不能正常解析、生成、编译、运行的问题。class_to_string 这类库的特殊之处在于它涉及代码生成阶段。这意味着适配工作除了看运行时兼容性还要看构建链路是否走得通依赖能不能拉取、build_runner 能不能跑、生成的 Dart 代码能不能被鸿蒙工程正常编译。这个链条上任何一个环节断开库就用不起来。2. 鸿蒙适配前的环境梳理与依赖链路分析2.1 鸿蒙 Flutter 开发环境的基本现状在动手之前先把环境现状说清楚。鸿蒙侧的 Flutter 开发目前用的是面向 OpenHarmony 的 Flutter 引擎适配版本开发工具主要是 DevEco Studio 加对应的 Flutter 插件。如果电脑上原本装着官方 Flutter SDK需要把 flutter 命令指向鸿蒙适配版 SDK或者用独立的工具链总之不能拿官方版直接构建鸿蒙应用。我自己的环境是这样的DevEco Studio 负责鸿蒙工程构建和签名Flutter SDK 用鸿蒙适配版Dart SDK 随 Flutter SDK 内置。命令行的flutter doctor在鸿蒙版下也能跑但输出的检查项里会有一些鸿蒙特有的内容比如 hdc 工具链、鸿蒙 SDK 路径这些跟官方版略有差异。这里有个容易踩坑的点鸿蒙适配版 Flutter 的版本号往往不是最新的它追的是某个经过验证的 Flutter 稳定版。这就意味着内置的 Dart SDK 版本也相对固定不会像官方 Flutter 那样频繁升级。所以你在查三方库兼容性的时候不能只看“最新版本能不能用”还要看“跟我这个 Dart SDK 版本匹配的版本是什么”。2.2 class_to_string 的依赖链三部分要分开看要把 class_to_string 适配到鸿蒙工程先把它拆成三个部分来理解。因为这三部分在鸿蒙环境下的风险完全不同。第一部分是注解库本身。它定义了ToString()这样的注解运行时要被代码读取。这部分是纯 Dart 代码不涉及任何平台通道和原生代码理论上在任何 Flutter 环境都能用鸿蒙也不例外。第二部分是代码生成器。它依赖 source_gen、analyzer 这些工具链跑在开发机上而不是设备上。生成器的作用是解析源码、生成.g.dart文件它最可能出问题的地方在于版本要求如果生成器的最新版要求较新的 Dart SDK而鸿蒙 Flutter 内置的 Dart SDK 版本不够高就会在运行build_runner的时候直接报错。第三部分是生成出来的代码。生成结果是普通的 Dart 代码里面包含的是字段访问、字符串插值这类基础语法几乎没有平台相关性。所以这部分在鸿蒙端的编译风险其实很低真正需要关注的是前面两环节能不能顺利跑通。这三个部分的风险分布不一样适配的策略也不一样。第一部分基本白送第二部分可能要锁版本第三部分主要是验证。理解了这条依赖链后面遇到报错才能快速定位问题出在哪一段而不是无头苍蝇乱试。2.3 动手前先做一次版本选型因为鸿蒙 Flutter 的 Dart SDK 版本跟官方不是同一条更新线所以动手之前建议先确认三件事。第一查一下当前鸿蒙 Flutter SDK 对应的 Dart 版本命令很简单flutter --version输出里有一行Dart 3.x.y记住这个版本号后面选三方库版本就以它为准。第二去 pub.dev 上看 class_to_string 的发布历史重点关注它最近的版本更新说明了什么特别是有没有“requires Dart 3.x”这种标记。如果最新版要求太高就翻历史版本列表找一个跟当前 Dart 版本兼容的稳定版。我这个工程的 Dart 版本是 3.3 左右class_to_string 我选的是满足这版本要求的稳定版没有追最新。第三确认代码生成器链路的版本组合。有时候不是 class_to_string 本身要求高而是它依赖的 source_gen 或 analyzer 要求高。这种间接的版本约束最容易忽略建议在 pubspec.yaml 里把关键依赖的版本范围放宽一点让 pub 自己解析出兼容组合而不是写死具体版本号。3. 鸿蒙化适配实操全流程从依赖到真机验证3.1 在鸿蒙 Flutter 工程里引入依赖环境准备好之后开始实际操作。假设你已经有一个能正常跑起来的鸿蒙 Flutter 工程下面直接在工程目录里操作。先在 pubspec.yaml 的dependencies节添加注解库dependencies: flutter: sdk: flutter class_to_string: ^2.2.1然后在dev_dependencies节添加代码生成器dev_dependencies: build_runner: ^2.4.0 class_to_string_generator: ^2.2.1需要注意注解库和生成器通常拆成两个包一个是运行时要引用的一个是构建时用的。如果你只加了注解库而漏了生成器后面跑 build_runner 的时候会提示找不到生成逻辑。添加完之后执行flutter pub get这里有个细节鸿蒙 Flutter 环境下的pub get可能会比官方版慢因为 pub 镜像或者 SDK 路径配置不一样属于正常现象。如果卡住不动超过几分钟检查一下网络配置和 pub 镜像设置换成国内镜像一般能解决。3.2 给业务类加注解并运行代码生成以我实际调试过的一个业务类为例。当时我要排查一个问题登录返回的LoginResult对象里某个字段在真机上一直不符合预期。我先在这个类上加上注解import package:class_to_string/class_to_string.dart; ToString() class LoginResult { final bool success; final String? token; final int? userId; final MapString, dynamic? extra; LoginResult({ required this.success, this.token, this.userId, this.extra, }); }注意我故意把token和userId设计成可空类型这种字段在打印的时候最容易出问题一不小心就打出null或者空字符串误导排查方向。class_to_string 在处理可空字段上的表现后面我专门说。然后运行代码生成命令dart run build_runner build --delete-conflicting-outputs这里有几种情况。如果你的鸿蒙 Flutter SDK 是完整内置 Dart SDK 的直接dart run就可以。如果执行器提示找不到 dart 命令改用flutter pub run build_runner build --delete-conflicting-outputs也能达到同样效果。跑完之后工程里会多出一个login_result.g.dart文件。拿编辑器打开看一眼核心内容大概是这个形态// GENERATED CODE - DO NOT MODIFY BY HAND part of login_result.dart; mixin _$LoginResultMixin on LoginResult { override String toString() { return LoginResult(success: $success, token: ${this.token}, userId: ${this.userId}, extra: ${this.extra}); } }生成完要做的第一件事不是急着去真机上跑而是在本地写一个临时 main 函数手动构造一个LoginResult对象直接print出来验证输出格式。这一步能省掉后面很多真机调试的时间。3.3 生成结果的校验与手工兜底方案刚才说生成文件里出现的是 mixin 而不是直接改原类这是这类代码生成器的常见设计不直接修改你的源文件而是生成一个 companion mixin你在原类里with它一下就把toString()混进来了。具体来说原类要改成这样ToString() class LoginResult with _$LoginResultMixin { ... }有人可能会觉得多此一举为什么不直接在.g.dart里生成一个覆盖toString()的子类因为 Dart 不支持多继承而 mixin 可以自由组合。这样设计的好处是如果类本身已经有别的继承关系mixin 不会产生冲突。校验的时候重点看几个方面。第一字段顺序跟声明顺序是否一致。第二可空字段的打印方式是否明确标出了null。第三嵌套对象的打印格式是不是可读的。我当时验证的时候就发现嵌套的extra是个 Map默认打出来是一行超长的 JSON 串看着很费劲。这种场景下我会临时把 Map 转成格式化后的多行字符串再打印但这个属于业务侧的打印策略class_to_string 本身管不到这么细。再补一句实际心得如果 build_runner 在鸿蒙环境里实在跑不通还有一个手工兜底方案。你可以在本地开发机上单独建一个纯 Dart 环境装好 class_to_string 的生成器把需要生成的文件拷贝过去跑一遍生成完再拷贝回鸿蒙工程。这不是最优解但胜在不受鸿蒙 Flutter 的工具链版本限制。3.4 在鸿蒙模拟器与真机上验证打印效果代码生成没问题之后把工程跑起来。鸿蒙 Flutter 的真机调试跟 Android 不太一样设备连接用的是 hdc 而不是 adb。先确认设备已经连上hdc list targets能列出设备后再用flutter run启动应用。日志输出在 IDE 的控制台里可以直接看到但如果你想在命令行里看完整 dart 侧日志也可以用 hdc 抓取。实际调试的时候我一般两个工具配合用IDE 里看 Flutter 侧的 print 输出真机出问题时再用 hdc 里的 hilog 过滤关键字。验证的目标是在某个业务动作触发后控制台里能看到格式化好的LoginResult(...)完整输出并且字段值跟预期一致。我当时要查的token字段问题最终就是在输出里发现它在某些场景下被赋了空字符串而非null一眼就锁定了问题。这种效率上的提升对比之前靠打断点一个个查确实不是一个量级。4. 常见问题与排查技法实录4.1 build_runner 跑不起来的系统排查思路鸿蒙环境下最常遇到的问题就是 build_runner 跑不起来报错各异但根因大多集中在两个方向。第一个方向是 Dart SDK 版本不满足要求。报错信息里如果出现requires SDK version x.x.x这类字样基本就是版本问题。解决方式不是硬升级 Dart SDK鸿蒙适配版动 SDK 风险很高而是把 class_to_string 及相关依赖降级到与当前 Dart 版本兼容的版本。我在里面踩过的坑是在 pubspec.yaml 里写^2.2.1这个上限制会把 pub 引向最新版如果最新版要求更高 Dart SDK就直接冲突。后来改成允许 pub 在解析时选择更早的兼容版本范围才顺利拉下来。第二个方向是 build_runner 自身的临时文件冲突。很多时候报错信息里会出现Invalid depfile或者.dart_tool目录下的缓存异常。这种问题一般是工程从别的环境复制过来、或者升级过依赖版本导致的。处理办法简单粗暴删除.dart_tool目录和pubspec.lock里的相关条目重新flutter pub get再跑一次生成命令大概率能恢复。另外还有一个细节运行 build_runner 的时候如果同时开着多个终端或者 IDE 的 analyzer 在后台做代码补全偶尔会碰到文件锁冲突。所以生成代码的时候最好把 IDE 里的自动保存和自动分析暂时关掉或者等提示出现后再操作。4.2 版本兼容性报错的完整排查链路版本兼容性问题我整理了一个排查链路照着走一般能快速定位。先看 class_to_string 自身的版本要求。打开 pubspec.lock找到 class_to_string 那一项看它锁定的版本和解析出的兼容版本范围。然后看生成器依赖的 source_gen 和 analyzer确认这两个间接依赖是否也在当前 Dart SDK 的兼容范围内。这里有个实际的例子。我之前尝试用 class_to_string 最新版的时候报错来自 analyzer 包里的代码用了比较高版本的 Dart 语法鸿蒙 Flutter 内置的 Dart 解析不了。而 analyzer 是 source_gen 的底层依赖class_to_string 的生成器只是间接引用它表面上看会以为是 class_to_string 的问题。所以排查的时候要一层层往下查class_to_string → class_to_string_generator → source_gen → analyzer → Dart SDK。哪一层断掉就去 pub 上找这一层的旧版兼容。用表格汇总一下我遇到过的版本问题类型和解决策略报错特征直接原因处理方案requires SDK version 3.xDart SDK 版本低于依赖要求降低 class_to_string/source_gen 到兼容版type ... is not a subtype of ...analyzer 版本与 Dart 语法不匹配锁定 analyzer 到当前 SDK 支持的旧版Cannot run with sound null safety混用了空安全与非空安全依赖全工程统一开启空安全避免混用4.3 生成代码编译不过的案例分析生成出来的代码理论上不会有大问题但我还是遇到过一次编译失败的案例值得说一下。当时的类里有个字段类型是自定义枚举生成器生成的代码直接把这个枚举的实例用字符串插值打印出来。本身的语法没有错但因为我那个枚举类没有重写toString(),默认打出来的是枚举名加内存地址的混合格式可读性很差。更麻烦的是如果枚举值内部持有敏感信息这种默认打印会把不友好的内容带进日志。这类问题的解法其实不在 class_to_string 这边而是要把打印关注点回到业务类上。给这个枚举类补一个友好的toString()或者在业务类上用ToString(skipFields: {secretField})之类的配置把不打印的字段排除掉。具体支持哪些配置项看你选的 class_to_string 版本文档不同版本略有差异。另外生成代码如果报undefined class多半是因为.g.dart文件的part of声明没有和原文件配对。检查两个地方原文件末尾有没有part xxx.g.dart;声明.g.dart文件里的part of是不是引用了正确的原文件名。大多数情况下是手贱改了文件名或者移动了目录导致的。4.4 鸿蒙端的日志查看与调试技巧适配做完最终目的是在鸿蒙端把对象日志用好。这里分享几个实际调试中的技巧。第一用debugPrint而不是print。在 Flutter 里print输出在大字符串场景下可能被截断debugPrint会自动分块打印保证长日志完整。class_to_string 生成的toString()在字段多的时候字符串会很长直接用print经常只看到后半段用debugPrint就没这问题。第二在鸿蒙真机上Flutter 的日志跟系统日志是分开的。IDE 控制台里能看到 Dart 侧输出但如果应用崩溃或者需要结合 native 日志分析要用 hdc 抓 hiloghdc shell hilog | grep flutter这样能看到 Flutter 引擎层和 Dart 层的日志混排再配合关键字过滤定位效率会高很多。第三字段特别多的对象建议在toString()输出后接一个分隔线再打印比如在调用侧统一加debugPrint(当前登录结果 ${result.toString()});这样日志里的对象边界很清楚尤其是一次性打印多个对象的时候不会互相串行。第四临时在 UI 上展示调试信息。有时候真机没法连 log或者设备在别人手上可以直接把toString()的结果放到一个临时页面的 Text 组件上展示截图回传也能达到排查目的。这个方案听着土但在现场调试时经常是最快的。5. 聊聊这次适配过程中的一些体会这次把 class_to_string 用到鸿蒙工程里前后花了小半天时间主要时间都耗在版本兼容性排查上。回头看最大的收获其实不是“这个库能用”而是摸清了一套鸿蒙环境下三方库适配的基本套路先查依赖链再锁版本组最后验证生成结果。这套思路对鸿蒙生态里其他代码生成类库同样适用。如果你是刚开始做鸿蒙 Flutter 开发我的建议是别一上来就追新版本先把基础工程跑通再引入你日常必不可少的三方库。遇到版本冲突优先考虑降级而不是升级 SDK。鸿蒙适配版的 Flutter SDK 更新节奏有自己的规划你很难强迫它跟官方版保持同步最好的策略是你去适配它支持的版本范围。最后再分享一个小技巧把 class_to_string 生成的.g.dart文件提交进版本控制不要加进.gitignore。这种生成文件虽然不是手写的但提交之后能保证团队里任何人拉下来都能直接编译不用每个人都在本地跑一遍 build_runner。在鸿蒙这种工具链差异比较大的协作环境下这一点尤其重要。