在这里插入图片描述

本文涉及文件

  • lib/app.dart
  • lib/feature_pages.dart
  • lib/models.dart
  • lib/main.dart

1. 先把结构想清楚:这个功能挂在哪里

逆向推理不是一个“孤立页面”,它属于首页四大模块中的“逻辑谜题”。

lib/app.dart,根布局使用 IndexedStack 存放 4 个 Tab。

为什么用 IndexedStack

  • 切换 Tab 时,已打开 Tab 的状态不会丢
  • 对这种训练类应用很友好:用户来回切换不会重置进度

核心代码(来自 lib/app.dart

class ReverseThinkingApp extends StatefulWidget {
  const ReverseThinkingApp({super.key});

  
  State<ReverseThinkingApp> createState() => 
      _ReverseThinkingAppState();
}

核心设计点:

  1. 采用StatefulWidget是为了支撑Tab切换的状态更新,这是动态页面的基础;
  2. 类名遵循大驼峰命名法,符合Dart官方编码规范;
  3. 构造函数添加super.key,保证Widget树的唯一性标识。
class _ReverseThinkingAppState extends State<ReverseThinkingApp> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const LogicPuzzlesPage(),
    const PatternRecognitionPage(),
    const WordProblemsPage(),
    const ProgressStatsPage(),
  ];
}

状态管理设计:

  1. _currentIndex私有变量控制当前显示的Tab页,初始值0对应首个“逻辑谜题”页;
  2. _pages列表存储所有Tab页组件,使用const修饰避免重复构建;
  3. 页面列表顺序与底部导航栏严格对应,防止切换错位。

Widget build(BuildContext context) {
  return MaterialApp(
    title: '逆向思维训练',
    home: Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),

布局核心逻辑:

  1. IndexedStack替代普通Stack,仅渲染当前index对应的页面,节省性能;
  2. MaterialApp作为根组件,提供主题、路由等基础能力;
  3. Scaffold封装页面骨架,包含body和底部导航栏。
      bottomNavigationBar: ConvexAppBar(
        items: const [
          TabItem(icon: Icons.psychology, title: '逻辑谜题'),
          TabItem(icon: Icons.grid_on, title: '模式识别'),
          TabItem(icon: Icons.text_fields, title: '文字推理'),
          TabItem(icon: Icons.analytics, title: '进度统计'),
        ],

底部导航设计:

  1. 使用ConvexAppBar(凸面导航栏)提升视觉体验,区别于原生BottomNavigationBar
  2. 每个TabItem绑定图标+文字,图标选用Material Design内置图标,风格统一;
  3. const修饰items,避免每次build重新创建对象。
        initialActiveIndex: _currentIndex,
        onTap: (index) => setState(() => 
            _currentIndex = index),
        backgroundColor: Colors.blue,
        activeColor: Colors.white,
        height: 60.h,
      ),
    ),
  );
}

交互与适配:

  1. onTap回调仅更新_currentIndex,通过setState触发页面重建;
  2. height: 60.h使用屏幕适配单位,适配不同尺寸设备;
  3. 导航栏配色采用蓝底白字,符合训练类App清晰、专业的视觉定位。

这个 height: 60.h 会用到 flutter_screenutil
因此入口一定要初始化 ScreenUtil,否则会有 LateInitializationError


2. 为什么入口要包一层 ScreenUtilInit

你项目里大量存在 .w/.h/.sp
比如 EdgeInsets.all(16.w)SizedBox(height: 20.h)TextStyle(fontSize: 16.sp)

这些写法背后依赖 flutter_screenutil 的初始化状态。
如果没有 ScreenUtilInit,那么第一次调用 .w/.h/.sp 就可能崩。

入口(lib/main.dart)建议保持下面这种形式

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    ScreenUtilInit(
      designSize: const Size(375, 812),
      minTextAdapt: true,
      splitScreenMode: true,

初始化关键配置:

  1. WidgetsFlutterBinding.ensureInitialized()确保Flutter引擎初始化完成;
  2. designSize设置设计稿基准尺寸(375x812为iPhone X尺寸);
  3. minTextAdapt开启文字最小适配,防止文字过小无法阅读;
  4. splitScreenMode支持分屏模式,提升多设备兼容性。
      builder: (_, __) => const ReverseThinkingApp(),
    ),
  );
}

构建逻辑:

  1. builder回调返回主应用组件,避免额外嵌套层级;
  2. 匿名参数___表示未使用,符合Dart编码规范;
  3. 主应用组件用const修饰,减少初始化开销。

这里的参数含义

  • designSize: 你的设计稿基准尺寸
  • minTextAdapt: 字体缩放时更稳
  • splitScreenMode: 兼容分屏布局

这样做之后,后续页面使用 .w/.h/.sp 才是可靠的。


3. 逆向推理的入口:从“逻辑谜题”列表进入

逆向推理入口在 LogicPuzzlesPage 里。
它会构建一个 ListView,每个功能用一张卡片表示。

你现在的项目中,逆向推理对应这一行(在 lib/feature_pages.dart

_buildFeatureCard(context, '逆向推理', 
  Icons.refresh, const ReverseReasoningPage()),

导航设计:

  1. _buildFeatureCard是封装的通用卡片组件,参数包含上下文、标题、图标、目标页面;
  2. 图标选用Icons.refresh,隐喻“逆向、刷新思维”,贴合功能定位;
  3. 目标页面用const修饰,避免提前创建实例。
Widget _buildFeatureCard(BuildContext context, String title,
    IconData icon, Widget targetPage) {
  return Card(
    margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
    child: ListTile(
      leading: Icon(icon, color: Colors.blue),
      title: Text(title, style: TextStyle(fontSize: 16.sp)),
      onTap: () => Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => targetPage),
      ),
    ),
  );
}

卡片封装细节:

  1. Card组件提供阴影和圆角,提升视觉层次;
  2. ListTile封装leading-icon+title布局,减少自定义代码;
  3. onTap通过Navigator.push跳转页面,使用MaterialPageRoute保证过渡动画。

_buildFeatureCard 内部的关键逻辑是 Navigator.push
也就是用户点击卡片后,打开对应的功能页。

这种结构的好处

  • 主页只负责“导航”,功能页只负责“功能本身”
  • 增加新功能时,只需要新增一个 Page 和一条 _buildFeatureCard

4. 先把题目模型定义好:LogicPuzzle

逆向推理是一组选择题。
你已经在 lib/models.dart 定义了 LogicPuzzle

这一步非常重要。
如果没有模型抽象,页面里会充满零散字段,后续维护很难。

class LogicPuzzle {
  final String id;
  final String question;
  final String correctAnswer;
  final List<String> options;
  final String explanation;

模型设计原则:

  1. 所有字段使用final修饰,保证不可变,符合Dart不可变对象设计理念;
  2. 字段命名采用小驼峰,符合Dart编码规范;
  3. 核心字段全覆盖:题干、选项、正确答案、解析,满足训练场景需求。
  LogicPuzzle({
    required this.id,
    required this.question,
    required this.correctAnswer,
    required this.options,
    required this.explanation,
  });
}

构造函数设计:

  1. 使用required关键字强制所有参数必须传入,避免空值;
  2. 无默认参数,保证每个题目实例的完整性;
  3. 简洁的构造函数写法,便于快速创建题目实例。

这个模型定义了题目的核心字段,结构清晰。
id 用于唯一标识,方便后续持久化或统计。
correctAnswer 直接存储选项文本,判断时无需映射。
options 为列表,支持任意数量选项。
explanation 为解析文本,训练反馈的核心。

字段解释

  • id: 题目唯一标识
  • question: 题干
  • options: 选项列表
  • correctAnswer: 正确选项(用文本对齐选项值)
  • explanation: 解析文本

注意一个实现细节

你把 correctAnswer 直接写成某个 option 的字符串。
这让判断非常简单

selectedAnswer == puzzle.correctAnswer

这样不需要额外做映射。


5. ReverseReasoningPage:为什么它必须是 StatefulWidget

逆向推理不是“展示静态内容”,它有三个动态状态

  • currentPuzzle: 当前题目下标
  • selectedAnswer: 当前选择
  • showResult: 是否显示解析

因此页面使用 StatefulWidget

在你目前的项目规模下,直接 setState 足够清晰。
如果后续要做“跨页面共享进度”,再考虑把状态上提或引入状态管理也不晚。


6. 页面核心实现:题目 + 单选 + 解析

下面这段代码就是你当前仓库里的逆向推理完整实现(位于 lib/feature_pages.dart)。

class ReverseReasoningPage extends StatefulWidget {
  const ReverseReasoningPage({super.key});

  
  State<ReverseReasoningPage> createState() => 
      _ReverseReasoningPageState();
}

页面结构设计:

  1. 遵循StatefulWidget标准写法,分离组件壳与状态;
  2. 构造函数传递key,保证Widget树的正确更新;
  3. 状态类命名以_开头,私有访问,符合封装原则。
class _ReverseReasoningPageState extends State<ReverseReasoningPage> {
  final List<LogicPuzzle> puzzles = [
    LogicPuzzle(
      id: '1',
      question: '如果所有鸟都会飞,企鹅是鸟,那么企鹅会飞。这个结论错在哪里?',
      correctAnswer: '前提错误:并非所有鸟都会飞',

题目数据设计:

  1. puzzles列表用final修饰,保证初始化后不可变;
  2. 题目实例直接初始化在页面中,便于快速调试和演示;
  3. 题干设计贴合“逆向思维”核心,引导用户反推逻辑漏洞。
      options: ['前提错误:并非所有鸟都会飞', 
                '推理错误', 
                '结论正确', 
                '企鹅不是鸟'],
      explanation: '逆向思维:从结论反推前提,发现初始前提"所有鸟都会飞"是错误的。',
    ),

选项与解析设计:

  1. 选项列表包含正确答案和干扰项,干扰项覆盖常见错误认知;
  2. 解析文本明确点出“逆向思维”核心方法,强化训练目标;
  3. 字符串换行采用逗号分隔,保持代码整洁。
    LogicPuzzle(
      id: '2',
      question: '一个房间里有3个开关,外面有3个灯泡。你只能进房间一次,如何找出每个开关对应的灯泡?',
      correctAnswer: '先开一个开关等几分钟,关掉再开另一个',
      options: ['随机尝试', '先开一个等几分钟再换', 
                '无法确定', '需要工具'],
      explanation: '逆向思维:利用温度信息,先开一个开关让灯泡发热,关掉后开另一个,进房间通过温度和亮度判断。',
    ),
  ];

多题目扩展设计:

  1. 第二个题目聚焦“逆向利用信息”(温度+亮度),丰富训练维度;
  2. 选项表述简洁,避免冗余,降低用户阅读成本;
  3. 解析明确说明逆向思维的应用场景,帮助用户迁移能力。
  int currentPuzzle = 0;
  String? selectedAnswer;
  bool showResult = false;

核心状态设计:

  1. currentPuzzle初始值0,默认显示第一道题;
  2. selectedAnswer为可空字符串,初始null表示未选择;
  3. showResult初始false,默认不显示解析,保证流程闭环。
  
  Widget build(BuildContext context) {
    final puzzle = puzzles[currentPuzzle];
    return Scaffold(
      appBar: AppBar(title: const Text('逆向推理')),
      body: Padding(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [

页面构建逻辑:

  1. 先获取当前题目实例,简化后续代码引用;
  2. Scaffold+AppBar构建标准页面骨架,符合Material Design规范;
  3. Padding统一页面内边距,16.w适配不同屏幕;
  4. Column作为主布局,CrossAxisAlignment.start左对齐,符合阅读习惯。
            LinearProgressIndicator(
              value: (currentPuzzle + 1) / puzzles.length,
              backgroundColor: Colors.grey[200],
              color: Colors.blue,
            ),
            SizedBox(height: 20.h),
            Text('谜题 ${currentPuzzle + 1}/${puzzles.length}', 
                 style: TextStyle(fontSize: 18.sp, 
                 fontWeight: FontWeight.bold)),

进度与标题设计:

  1. LinearProgressIndicator自定义背景色和进度色,提升视觉辨识度;
  2. 进度计算(currentPuzzle + 1)/总数,符合用户“第N题/共M题”的心智模型;
  3. 标题文字加粗+大号字体,突出当前题目进度。
            SizedBox(height: 16.h),
            Text(puzzle.question, 
                 style: TextStyle(fontSize: 16.sp)),
            SizedBox(height: 24.h),

题干展示设计:

  1. 多层SizedBox分隔,控制间距节奏(20h→16h→24h),提升阅读体验;
  2. 题干字体大小16.sp,兼顾可读性和屏幕空间;
  3. 直接绑定puzzle.question,数据驱动UI。
            ...puzzle.options.map((option) => RadioListTile<String>(
              title: Text(option, style: TextStyle(fontSize: 14.sp)),
              value: option,
              groupValue: selectedAnswer,
              onChanged: (value) => setState(() => 
                  selectedAnswer = value),
            )),

选项渲染设计:

  1. 使用map遍历选项列表,动态生成RadioListTile,无需重复代码;
  2. RadioListTile自带单选逻辑,无需自定义选中状态;
  3. onChanged回调通过setState更新选中状态,简单高效;
  4. 选项文字14.sp,略小于题干,形成视觉层级。
            SizedBox(height: 20.h),
            if (showResult) ...[
              Container(
                padding: EdgeInsets.all(12.w),
                decoration: BoxDecoration(
                  color: selectedAnswer == puzzle.correctAnswer 
                      ? Colors.green[100] 
                      : Colors.red[100],
                  borderRadius: BorderRadius.circular(8.r),
                ),

结果展示设计:

  1. 条件渲染showResult,未确认答案时不显示解析;
  2. Container自定义背景色,正确为浅绿、错误为浅红,直观反馈;
  3. 8.r圆角适配不同屏幕,保持视觉一致性;
  4. 12.w内边距保证文本与边框间距。
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text('答案:${puzzle.correctAnswer}', 
                         style: TextStyle(fontWeight: FontWeight.bold)),
                    SizedBox(height: 8.h),
                    Text(puzzle.explanation, 
                         style: TextStyle(fontSize: 14.sp)),
                  ],
                ),
              ),
            ],

解析内容设计:

  1. 答案文字加粗,突出核心信息;
  2. 8.h间距分隔答案和解析,提升阅读节奏;
  3. 解析文字14.sp,与选项文字大小一致,保持视觉统一;
  4. CrossAxisAlignment.start左对齐,符合文本阅读习惯。
            Spacer(),
            Row(
              children: [
                if (currentPuzzle > 0)
                  ElevatedButton(
                    onPressed: () => setState(() {
                      currentPuzzle--;
                      selectedAnswer = null;
                      showResult = false;
                    }),
                    child: const Text('上一题'),
                  ),

上一题按钮设计:

  1. 条件渲染,仅当不是第一题时显示;
  2. 点击回调重置所有状态,保证切换题目后页面干净;
  3. ElevatedButton自带点击反馈,符合Material Design交互规范。
                Spacer(),
                ElevatedButton(
                  onPressed: selectedAnswer != null 
                      ? () => setState(() => showResult = true) 
                      : null,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
                  ),
                  child: const Text('确认答案'),
                ),

确认答案按钮设计:

  1. 按钮禁用逻辑:未选择答案时不可点击;
  2. 自定义按钮样式,背景色与主题一致,内边距适配屏幕;
  3. 点击仅更新showResult状态,逻辑单一,便于维护。
                if (currentPuzzle < puzzles.length - 1)
                  SizedBox(width: 12.w),
                if (currentPuzzle < puzzles.length - 1)
                  ElevatedButton(
                    onPressed: showResult 
                        ? () => setState(() {
                          currentPuzzle++;
                          selectedAnswer = null;
                          showResult = false;
                        }) 
                        : null,
                    child: const Text('下一题'),
                  ),
              ],
            )
          ],
        ),
      ),
    );
  }
}

下一题按钮设计:

  1. 条件渲染,仅当不是最后一题时显示;
  2. 按钮禁用逻辑:未确认答案时不可点击,强制用户完成当前题;
  3. 点击重置所有状态,保证新题目从初始状态开始;
  4. 12.w间距分隔按钮,避免视觉拥挤。

到这里,你已经拥有一个最小闭环

  • 有题目
  • 能选择
  • 能确认
  • 能展示解析
  • 能上一题/下一题

7. 进度条的细节:为什么用 (current + 1) / total

LinearProgressIndicator(value: (currentPuzzle + 1) / puzzles.length)

进度计算设计:

  1. 加1处理:第一题显示1/总数,而非0/总数,符合用户直觉;
  2. 浮点计算:自动转换为0-1之间的数值,适配进度条value要求;
  3. 无额外计算逻辑,保持代码简洁。

如果写成 currentPuzzle / puzzles.length,第一题会显示 0%,用户直觉上会觉得“还没开始”。
currentPuzzle + 1 能让进度条对齐“我正在做第几题”的心智模型。

这里还隐含一个约束:puzzles.length 不能为 0。
你现在把题目写成常量列表,天然不会出现 0。
后续如果要做动态题库,建议至少保证

  • 没有题目时给一个空态
  • 或者在 build 前提前 return 一个提示
if (puzzles.isEmpty) {
  return Center(child: Text('暂无题目,请稍后再试', 
                           style: TextStyle(fontSize: 16.sp)));
}

空态处理设计:

  1. 提前判断题目列表是否为空,避免数组越界;
  2. 居中显示提示文本,提升用户体验;
  3. 字体大小适配,保证不同屏幕可见性。

8. RadioListTile 的选中逻辑:groupValue 就是“唯一真相”

RadioListTile<String>(
  value: option,
  groupValue: selectedAnswer,
  onChanged: (value) => setState(() => selectedAnswer = value),
)

单选逻辑设计:

  1. value绑定选项文本,groupValue绑定选中状态,天然匹配;
  2. 无需维护多个bool值,仅用一个字符串管理选中状态;
  3. onChanged直接更新状态,逻辑无冗余。

可以把它理解成:

  • 每个选项有一个 value
  • 当前选中项由 groupValue 决定

groupValue == value 时就会显示选中。

这套机制的好处是:

  • 你不需要给每个选项维护一个 bool
  • 状态始终是一个字符串,很容易做比较和存储

你现在把 selectedAnswer 定义成 String?,配合按钮禁用也很自然。


9. showResult 的价值:用最少的状态,构建清晰的流程

你把“展示解析”这个流程做成一个 bool:showResult

触发点只有一个:点击“确认答案”。

onPressed: selectedAnswer != null ? () => setState(() => showResult = true) : null,

流程控制设计:

  1. 单一状态控制解析显示,无复杂分支;
  2. 按钮禁用逻辑与选中状态强关联,防止无效操作;
  3. setState仅更新必要状态,性能最优。

这会带来两个明显好处。

第一,按钮天然具备引导性。

  • 用户必须先选一个
  • 才能确认

第二,页面逻辑不会分叉。

  • 没确认就不显示解析
  • 确认了就显示解析

你没有引入额外枚举或状态机,但流程依然很清晰。


10. 为什么“下一题”必须依赖 showResult

你的下一题按钮写法是

onPressed: showResult ? () => setState(() {
  currentPuzzle++;
  selectedAnswer = null;
  showResult = false;
}) : null,

流程约束设计:

  1. 强制用户先确认答案,再进入下一题,保证训练效果;
  2. 切换题目时重置所有状态,避免状态污染;
  3. 禁用状态直观,用户明确知道操作顺序。

这个约束其实是体验设计。

如果不依赖 showResult,用户可能连续点“下一题”直接跳过思考。
而你现在的约束强制用户“先确认,再进入下一题”。

这个方式的“真实性”在于它贴近训练类产品的基本目标:

  • 强制反馈闭环
  • 不让用户无意识跳题

并且实现上很朴素。

  • 不需要额外计时
  • 不需要拦截手势
  • 一个 bool 足够

11. 状态重置:为什么每次切题都清空 selectedAnswer

上一题/下一题都有这三句

selectedAnswer = null;
showResult = false;

状态重置设计:

  1. 切题时清空选中状态和解析显示,保证新题初始状态;
  2. 两行代码完成重置,逻辑清晰;
  3. 避免旧题状态污染新题,提升用户体验。

这是一个容易被忽略但非常关键的细节。

如果你不重置 selectedAnswer,会出现两个问题。

第一,新题可能会默认选中某个选项

第二,用户会误以为“系统替我选了答案”。

因此重置是更稳的。

后续如果你希望“返回上一题保留上次选择”,那就需要把状态改成

  • 以题目 id 为 key 的 Map

Map<String, String?> answersById = {};
Map<String, bool> showResultById = {};

selectedAnswer = answersById[puzzle.id] ?? null;
showResult = showResultById[puzzle.id] ?? false;

answersById[puzzle.id] = selectedAnswer;
showResultById[puzzle.id] = true;

历史状态设计:

  1. 用Map存储每个题目的选中状态和解析显示状态;
  2. 切换题目时从Map读取历史状态,无则使用默认值;
  3. 确认答案时保存状态,保证返回上一题可见。
  • Map<String, String?> answersById
  • Map<String, bool> showResultById

这样上一题/下一题就不会互相污染。


12.只改 puzzles 列表

LogicPuzzle(
  id: '3',
  question: '一个人从5楼掉下来没事,从1楼掉下来却受伤了,为什么?',
  correctAnswer: '从5楼掉下来掉进了泳池,1楼掉在水泥地',
  options: ['体重不同', '从5楼掉泳池/1楼掉水泥地', 
            '运气好', '楼层高度测量方式不同'],
  explanation: '逆向思维:跳出"掉落高度=伤害程度"的固定思维,考虑掉落环境的差异。',
),
  1. 延续id递增规则,保证唯一性;
  2. 题干设计贴近生活,降低理解成本;
  3. 解析强化逆向思维核心,保持训练一致性。

因为 UI 完全依赖 puzzles[currentPuzzle]

也就是说

  • 加 10 道题,UI 不用动
  • 改题干、改选项、改解析,UI 不用动

这就是模型抽象带来的收益。

如果你计划后续接入本地存储(例如记录用户做题结果),建议保持 LogicPuzzle 不变,新增一个“结果模型”。
项目里已经有一个 PuzzleResult(在 lib/models.dart),后续可以把它用起来。

class PuzzleResult {
  final String puzzleId;
  final String? userAnswer;
  final bool isCorrect;
  final DateTime completedAt;

  PuzzleResult({
    required this.puzzleId,
    required this.userAnswer,
    required this.isCorrect,
    required this.completedAt,
  });
}

结果模型设计:

  1. 包含题目ID、用户答案、是否正确、完成时间,覆盖统计需求;
  2. 所有字段final,保证不可变;
  3. 构造函数强制参数,保证数据完整性。

13. 这一页与其他页的一致性:布局与适配保持统一

你会发现 ReverseReasoningPage 的布局风格跟其他训练页很一致:

  • Scaffold + AppBar
  • Padding(EdgeInsets.all(16.w))
  • SizedBox(height: xx.h) 做间距
Widget buildCommonLayout({required Widget child}) {
  return Padding(
    padding: EdgeInsets.all(16.w),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        LinearProgressIndicator(value: progressValue),
        SizedBox(height: 20.h),
        Text(title, style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
        SizedBox(height: 16.h),
        child,
        Spacer(),
        buildCommonButtonArea(),
      ],
    ),
  );
}

通用布局封装:

  1. 提取重复布局逻辑,减少代码冗余;
  2. 暴露child参数,支持自定义内容;
  3. 统一进度条、标题、间距,保证风格一致。

这种一致性非常重要。
它不只是“看起来统一”,更是降低维护成本。

当你要统一调整全局间距时,你会发现很多页面都只用改一个数值即可。


14. 小结:把“逆向推理实现”落到工程里,靠的是三个点

  • 入口初始化适配ScreenUtilInit
  • 入口导航清晰LogicPuzzlesPage 只负责 push 到功能页
  • 功能页状态闭环selectedAnswer + showResult + currentPuzzle

这三个点组合起来,代码不复杂,但完成度很高。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐