鸿蒙 Flutter 应用图标生成与配置指南

不用 Photoshop,不用 Figma------用 Dart 代码生成你的应用图标。

一、背景

E-Brufen 是一个零外部资源的应用------没有设计师、没有图标素材、没有图片文件。所有视觉资产都由代码生成。应用图标也不例外:一个独立的 Dart 脚本 generate_icon.dart,在构建前运行,输出一个带有花瓣纹理和渐变背景的 512x512 PNG 文件。

这个脚本不依赖任何第三方包------PNG 编码、zlib 压缩、CRC32 校验全部用纯 Dart 手写。这意味着你可以在任何安装了 Dart SDK 的环境中运行它,包括 CI/CD 流水线。

★ Insight ─────────────────────────────────────

程序化生成图标不是炫技。在零外部资源策略下,这意味着构建是完全可复现的 ------无论在哪台机器上、无论设计师的 Figma 版本如何变化,dart run generate_icon.dart 始终输出完全相同的 PNG 文件。这是 CI/CD 中"可复现构建"理念在资源层面的落地。

─────────────────────────────────────────────────

二、完整脚本架构

generate_icon.dart 共计 186 行,由以下模块组成:

模块 函数 行数 职责
入口 main() L6-L17 调用生成逻辑,写入 ohos 资源目录
像素渲染 _generatePng() L19-L89 512x512 逐像素绘制花形图案
PNG 编码 _encodePng() L91-L122 组装 PNG 签名 / IHDR / IDAT / IEND
块写入 _writeChunk() L124-L130 通用 PNG 块结构封装
大端写 w32() L132-L137 32 位无符号整数大端写入
Zlib 压缩 _zlibCompress() L140-L175 零压缩模式 Deflate + Adler-32
CRC32 _crc32() L177-L186 IEEE 802.3 多项式 CRC32
dart 复制代码
// generate_icon.dart --- 入口
void main() {
  const size = 512;
  final bytes = _generatePng(size);

  // 输出到 AppScope(应用全局图标)
  final path = 'ohos/AppScope/resources/base/media/app_icon.png';
  File(path).writeAsBytesSync(bytes);
  stderr.writeln('Icon generated: $path (${size}x$size)');

  // 同时输出到 entry 模块(模块级图标)
  final entryPath = 'ohos/entry/src/main/resources/base/media/icon.png';
  File(entryPath).writeAsBytesSync(bytes);
  stderr.writeln('Icon copied: $entryPath');
}

两个输出路径的区别:

  • ohos/AppScope/resources/base/media/app_icon.png --- 应用全局图标,在系统设置中的应用列表、应用信息页显示
  • ohos/entry/src/main/resources/base/media/icon.png --- entry 模块图标,在桌面快捷方式、最近任务中显示

HarmonyOS 的 Stage 模型下,一个应用可以包含多个 module(entry、feature),每个 module 可以有自己的图标。但对于单模块应用,通常两处使用相同的图标文件。

★ Insight ─────────────────────────────────────

注意 main()stderr 写入日志而非 stdout。这是因为 stdout 可能被构建脚本捕获用于解析(例如输出产物路径),而诊断信息应当流向 stderr------这是 Unix 工具链的约定。

─────────────────────────────────────────────────

三、像素绘制算法

图标的视觉效果由四个数学函数叠加生成,每个函数操作 512x512 像素矩阵中的每一格(共 262,144 个像素)。

3.1 坐标系变换

dart 复制代码
for (var y = 0; y < height; y++) {
  for (var x = 0; x < width; x++) {
    final idx = (y * width + x) * 4;   // RGBA 偏移量
    final cx = x - width / 2;           // 将原点移至图像中心
    final cy = y - height / 2;          // 同上
    final dist = sqrt(cx * cx + cy * cy) / (width / 2);  // 归一化距离 [0, ~1.4]
    final angle = atan2(cy, cx);        // 极坐标角度 [-π, π]

每个像素首先从笛卡尔坐标 (x, y) 转换为极坐标 (dist, angle)dist 经过归一化处理:图像中心为 0,图像边缘(水平/垂直)为 1,四角为 √2 ≈ 1.4。

3.2 层一:径向渐变底色

dart 复制代码
// 柔和的绿色渐变背景
final bgR = (76 + (dist * 30)).toInt();
final bgG = (175 - (dist * 20)).toInt();
final bgB = (80 + (dist * 15)).toInt();

这是一层基础色。从中心向外,R 和 B 通道略微增加,G 通道略微减少------产生从鲜绿到深绿的过渡。基准色 (76, 175, 80) 是 Material Design 的 Green 400。

3.3 层二:花瓣纹理(核心视觉效果)

dart 复制代码
final petalDist = dist * (1 + 0.3 * sin(angle * 3) * sin(dist * pi * 2));

if (petalDist < 0.85 && petalDist > 0.2) {
  final petalAlpha =
      (0.15 * sin(angle * 3) * sin(petalDist * pi * 1.5)).abs();
  r = (r + (180 - r) * petalAlpha).toInt().clamp(0, 255);
  g = (g + (220 - g) * petalAlpha).toInt().clamp(0, 255);
  b = (b + (120 - b) * petalAlpha).toInt().clamp(0, 255);
}

这是整个算法的核心。两重三角函数组合产生三瓣花形图案:

  • sin(angle * 3) --- 乘以 3 在 0, 2π 范围内产生 3 个完整周期,形成三瓣对称结构。每 120° 一个花瓣。如果乘以 4 就是四瓣花,乘以 5 就是五瓣花。
  • sin(dist * pi * 2) --- 随径向距离变化的波纹。在 dist 从 0 到 1 的过程中,正弦波完成两个完整周期,产生同心纹理。
  • petalDist --- 将角度调制和径向调制叠加到基础距离上,使得在花瓣方向上距离"缩短"(图案向外延伸),在花瓣间隙方向上距离"拉长"(图案向内收缩)。

petalAlpha.abs() 保证了非负混合系数,将像素向暖色方向偏移(180, 220, 120 目标色)。

3.4 层三:中心圆形高亮

dart 复制代码
if (dist < 0.25) {
  final alpha = (1 - dist / 0.25).clamp(0.0, 1.0);
  r = (r + (255 - r) * alpha * 0.7).toInt();
  g = (g + (255 - g) * alpha * 0.5).toInt();
  b = (b + (255 - b) * alpha * 0.3).toInt();
}

dist < 0.25 的核心区域,逐步向白色混合。alpha 从中心的 1.0 线性衰减到边缘的 0.0。R 通道混合 70%(向 255),G 混合 50%,B 混合 30%------刻意不平衡使得最终高光偏向暖白(略微偏粉),而非完全中性灰白。

3.5 层四:外环描边

dart 复制代码
if (dist > 0.8 && dist < 0.95) {
  final ringAlpha = (1 - (dist - 0.8) / 0.15).abs().clamp(0.0, 1.0) * 0.5;
  r = (r + (200 - r) * ringAlpha).toInt();
  g = (g + (230 - g) * ringAlpha).toInt();
  b = (b + (200 - b) * ringAlpha).toInt();
}

dist ∈ [0.8, 0.95] 的环带区域叠加淡色描边。ringAlpha 在环带中心最大(0.5),向两边线性衰减。这给花瓣图案增加了一个视觉边界。

3.6 层五:圆角方形裁剪

dart 复制代码
final edgeDist = max(
  (cx.abs() - width * 0.35).clamp(0, width.toDouble()),
  (cy.abs() - height * 0.35).clamp(0, height.toDouble()),
);
if (edgeDist > 0 && edgeDist < 20) {
  final edgeAlpha = (1 - edgeDist / 20.0).clamp(0.0, 1.0) * 0.6;
  a = (255 * (1 - edgeAlpha)).toInt();
}

这层不修改颜色,而是修改 Alpha 通道。它定义了一个 "圆角方形" 区域:X 和 Y 方向上,超出中心 ±35% 范围的像素进入半透明过渡带(20 像素宽),过渡带之外的像素 Alpha 归零,完全透明。

效果是最终图标呈现为柔和的圆角方形------适配 Android(自适应图标)和 HarmonyOS(圆角方形图标)的裁剪规范,避免图标内容被系统级裁剪后丢失关键信息。

★ Insight ─────────────────────────────────────

五层叠加的设计模式在图形编程中非常经典(类似 Photoshop 的图层概念)。每一层只做一件事,叠加后得到复杂效果。这种"组合简单函数得到复杂输出"的方法,在 generate_audio.dart(WAV 编码器)中也被同样使用------多层正弦波叠加生成自然音效纹理。

─────────────────────────────────────────────────

3.7 数学总结

函数 参数域 视觉效果
atan2(cy, cx) 极角 -π, π 控制花瓣方向性
sin(angle * 3) 周期 = 2π/3 三瓣对称结构
sin(dist * pi * 2) 径向波纹 同心圆纹理
clamp(0, 255) 通道值约束 防止溢出
clamp(0.0, 1.0) Alpha 混合系数 线性插值边界

四、PNG 编码器实现

完整的 PNG 编码器在 _encodePng() 函数中,约 30 行。它在二进制层面构造了一个合法的 PNG 文件。

4.1 PNG 文件结构总览

复制代码
┌──────────────────────┐
│  PNG Signature       │  8 bytes: 137 80 78 71 13 10 26 10
├──────────────────────┤
│  IHDR Chunk          │  图像头:宽、高、位深、颜色类型
├──────────────────────┤
│  IDAT Chunk          │  图像数据(原始像素经 zlib 压缩)
├──────────────────────┤
│  IEND Chunk          │  图像结束标记(空数据)
└──────────────────────┘

每个 Chunk 的内部结构完全一致:

复制代码
┌────────────────┬──────────────┬────────────────┬────────────────┐
│  Length (4B)   │  Type (4B)   │  Data (N B)    │  CRC32 (4B)    │
└────────────────┴──────────────┴────────────────┴────────────────┘
  • Length:Data 字段的字节数(不含自身),大端序
  • Type:4 个 ASCII 字符(如 "IHDR"、"IDAT"、"IEND"),编码为 4 字节
  • Data:块类型决定的内容
  • CRC32:对 Type + Data 的 CRC32 校验值(不含 Length 字段)

4.2 通用块写入器

dart 复制代码
void _writeChunk(List<int> out, String type, List<int> data) {
  w32(out, data.length);                     // [4B] 数据长度
  out.addAll(type.codeUnits);                // [4B] 类型码 (ASCII)
  out.addAll(data);                          // [N B] 数据体
  final crc = _crc32(type.codeUnits + data); // 对 type+data 计算 CRC32
  w32(out, crc);                             // [4B] 校验值
}

_writeChunk 是所有块的工厂函数。三个块(IHDR、IDAT、IEND)都通过它写入,保证结构一致。

4.3 PNG 签名(Magic Number)

dart 复制代码
output.addAll([137, 80, 78, 71, 13, 10, 26, 10]);

这 8 字节是 PNG 规范规定的固定签名(hex: 89 50 4E 47 0D 0A 1A 0A)。它同时起到:

  1. 魔法数字:识别文件类型(第 2-4 字节是 ASCII "PNG")
  2. 换行检测0D 0A 是 CRLF,用于检测 DOS/Unix 换行转换
  3. EOF 检测1A 是 DOS 的 EOF 字符,阻止 type 命令输出二进制垃圾

4.4 IHDR(Image Header)

dart 复制代码
final ihdrData = <int>[];
ihdrW32(width);    // 4B: 图像宽度 (512)
ihdrW32(height);   // 4B: 图像高度 (512)
ihdrData.addAll([8, 6, 0, 0, 0]);
//  byte 0: 8  = bit depth (每通道 8 位)
//  byte 1: 6  = color type (RGBA --- Truecolor + Alpha)
//  byte 2: 0  = compression method (0 = deflate, PNG 目前唯一合法值)
//  byte 3: 0  = filter method (0 = adaptive, PNG 目前唯一合法值)
//  byte 4: 0  = interlace method (0 = none, 1 = Adam7)
_writeChunk(output, 'IHDR', ihdrData);

颜色类型 6 (RGBA) 意味着每个像素占 4 字节:R、G、B、A 各 8 位。对于 512x512 图像,IHDR 数据只有 13 字节,但它宣告了后续 IDAT 的格式。

4.5 IDAT(Image Data)

dart 复制代码
// 构建原始像素数据(逐行,每行前缀 filter byte)
final raw = <int>[];
for (var y = 0; y < height; y++) {
  raw.add(0);  // filter type: None(不应用任何过滤)
  final rowStart = y * width * 4;
  raw.addAll(rgba.sublist(rowStart, rowStart + width * 4));
}
final compressed = _zlibCompress(raw);
_writeChunk(output, 'IDAT', compressed);

关键点:

  • Filter byte :每行数据前加一个 0(None 过滤器),表示像素数据原样存储。PNG 支持 5 种过滤器(None / Sub / Up / Average / Paeth),用于提高压缩率,但我们选择最简单的。
  • Zlib 压缩:原始数据(含 filter byte)通过 zlib 压缩后存入 IDAT。不压缩直接存是不合法的------PNG 规范要求 IDAT 数据必须是 deflate 压缩的。
  • 对于 512x512 RGBA 图像,原始数据大小 = 512 * 512 * 4 + 512(filter bytes)= 1,049,088 字节 ≈ 1MB。

4.6 IEND(Image End)

dart 复制代码
_writeChunk(output, 'IEND', []);

IEND 块的数据长度为 0------它只是一个"文件结束"的标记。任何 PNG 解析器看到 IEND 后停止读取。

4.7 w32:大端序写入

dart 复制代码
void w32(List<int> out, int v) {
  out.add((v >> 24) & 0xFF);  // 最高字节先写
  out.add((v >> 16) & 0xFF);
  out.add((v >> 8) & 0xFF);
  out.add(v & 0xFF);          // 最低字节后写
}

PNG 规范要求所有多字节整数使用大端序(Big-Endian / Network Byte Order)。这与 WAV 格式的小端序不同------编写二进制格式时需要仔细确认字节序。

五、zlib 压缩的简易实现

PNG 的 IDAT 必须经过 zlib 压缩。真正的 Deflate 算法需要 LZ77 + Huffman 编码,实现复杂度极高。E-Brufen 选择了一个务实的方案:零压缩模式(BTYPE=00)。

dart 复制代码
List<int> _zlibCompress(List<int> data) {
  // ── Zlib Header ──
  final out = <int>[0x78, 0x01];
  // 0x78 = CMF: deflate method, 32K window size
  // 0x01 = FLG: no dictionary, compression level 0 (no compression)

  // ── Deflate Block (BTYPE=00, No Compression) ──
  out.add(1);  // BFINAL=1 (final block), BTYPE=00 (no compression)

  // LEN (2 bytes, little-endian)
  final len = data.length;
  out.add(len & 0xFF);
  out.add((len >> 8) & 0xFF);

  // NLEN (2 bytes, one's complement of LEN)
  final nlen = len ^ 0xFFFF;
  out.add(nlen & 0xFF);
  out.add((nlen >> 8) & 0xFF);

  // Raw data
  out.addAll(data);

  // ── Adler-32 Checksum ──
  var s1 = 1, s2 = 0;
  for (final b in data) {
    s1 = (s1 + b) % 65521;
    s2 = (s2 + s1) % 65521;
  }
  out.add((s2 >> 8) & 0xFF);  // s2 hi
  out.add(s2 & 0xFF);         // s2 lo
  out.add((s1 >> 8) & 0xFF);  // s1 hi
  out.add(s1 & 0xFF);         // s1 lo

  return out;
}

5.1 Zlib 头(CMF + FLG)

字段 含义
CMF bits 0-3 8 Compression Method: deflate
CMF bits 4-7 7 Window size: 2^(7+8) = 32KB
FLG bits 0-4 1 Check bits (使 CMF*256+FLG 能被 31 整除)
FLG bit 5 0 FDICT: no preset dictionary

5.2 Deflate 块头

BTYPE=00 是无压缩存储模式。它将原始数据直接写入,前面加 LEN 和 NLEN(LEN 的 1 补码)用于完整性校验。如果 len != (~nlen & 0xFFFF),说明数据已损坏。

Deflate 支持三种块类型:

BTYPE 模式 压缩率 实现复杂度
00 无压缩 0% 极低
01 固定 Huffman ~50% 中等
10 动态 Huffman ~70% 很高

5.3 Adler-32 校验

Adler-32 = (s2 << 16) | s1,其中:

  • s1 = sum of all bytes (mod 65521)
  • s2 = sum of all intermediate s1 values (mod 65521)

65521 是关键------它是小于 2^16 的最大质数。使用质数模运算减少哈希碰撞。在 zlib 中,Adler-32 以大端序存储。

★ Insight ─────────────────────────────────────

零压缩方案用约 1MB(而非 300KB)的文件大小换来了约 30 行(而非 3000 行)的代码。对于一个只生成一次的应用图标,1MB 完全可以接受。如果未来需要更高压缩率(例如批量生成大量图片),可以替换为 dart:ioZLibCodec------但在此之前,保持简单的实现更有利于维护。参见 post-40 获得 zlib 的完整分析。

─────────────────────────────────────────────────

六、CRC32 校验

dart 复制代码
int _crc32(List<int> data) {
  var crc = 0xFFFFFFFF;
  for (final b in data) {
    crc ^= b;
    for (var i = 0; i < 8; i++) {
      crc = (crc & 1) != 0
        ? (crc >> 1) ^ 0xEDB88320  // 多项式反射
        : crc >> 1;
    }
  }
  return crc ^ 0xFFFFFFFF;
}

6.1 算法剖析

  1. 初始值0xFFFFFFFF(全 1)
  2. 逐字节处理:每个字节与当前 CRC 的低 8 位 XOR
  3. 逐位处理 :对每个 bit 循环 8 次
    • 如果 LSB 是 1:右移 1 位后 XOR 0xEDB88320
    • 如果 LSB 是 0:仅右移 1 位
  4. 最终 XORcrc ^ 0xFFFFFFFF

6.2 多项式 0xEDB88320

这是 IEEE 802.3 标准 CRC32 多项式的反射(reversed)形式

复制代码
标准形式: 0x04C11DB7
反射形式: 0xEDB88320(bit 反转)

PNG 规范指定使用反射多项式------这对应 zlib 库的默认行为,也是几乎所有 PNG 库使用的多项式。使用非反射多项式生成的 CRC32 会导致 PNG 查看器拒绝打开文件。

6.3 CRC32 生成表 vs 逐位计算

典型的 CRC32 实现会预计算一个 256 项的查找表以加速:

dart 复制代码
// 优化版 CRC32(使用查找表,速度提升 8 倍)
final _crcTable = List<int>.generate(256, (n) {
  var c = n;
  for (var k = 0; k < 8; k++) {
    c = (c & 1) != 0 ? (c >> 1) ^ 0xEDB88320 : c >> 1;
  }
  return c;
});

int _crc32Fast(List<int> data) {
  var crc = 0xFFFFFFFF;
  for (final b in data) {
    crc = _crcTable[(crc ^ b) & 0xFF] ^ (crc >> 8);
  }
  return crc ^ 0xFFFFFFFF;
}

E-Brufen 使用逐位计算版本------对于 1MB 的数据,CRC32 计算时间约 0.2 秒,完全可接受。参见 post-39 获得 PNG 格式的完整分析。

七、HarmonyOS 图标资源配置

7.1 目录结构

生成的图标需要放置到 HarmonyOS 项目规定的资源目录中:

复制代码
ohos/
├── AppScope/
│   └── resources/
│       └── base/
│           ├── element/
│           │   └── string.json         # 应用名称等字符串
│           └── media/
│               └── app_icon.png        # ← 生成到这里(应用全局图标)
├── entry/
│   └── src/
│       └── main/
│           ├── module.json5            # entry 模块配置
│           └── resources/
│               └── base/
│                   ├── element/
│                   │   └── string.json
│                   └── media/
│                       └── icon.png    # ← 生成到这里(entry 模块图标)
└── build-profile.json5

7.2 app.json5 配置

json5 复制代码
// ohos/AppScope/app.json5
{
  "app": {
    "bundleName": "com.ebrufen.app",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",   // 通过 $media 引用 app_icon.png
    "label": "$string:app_name"
  }
}

$media:app_icon 引用的是 resources/base/media/app_icon.png$media: 前缀是 HarmonyOS 的资源引用语法,类似于 Android 的 @mipmap/@drawable/

7.3 module.json5 配置

json5 复制代码
// ohos/entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "abilities": [
      {
        "name": "EntryAbility",
        "icon": "$media:icon",            // 桌面图标
        "label": "$string:EntryAbility_desc",
        "startWindowIcon": "$media:icon",  // 启动页图标(冷启动时显示)
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      }
    ]
  }
}

icon vs startWindowIcon 的区别:

  • icon:桌面快捷方式、最近任务列表中显示的图标
  • startWindowIcon:应用冷启动时,在 Flutter 引擎初始化完成之前显示的占位图标(防止白屏闪现)

两者通常指向同一文件。如果 startWindowIcon 缺失,冷启动时会显示一个默认的灰色图标。

★ Insight ─────────────────────────────────────

HarmonyOS 使用 $media:filename 引用媒体资源(不带扩展名)。这与 Android(@mipmap/ic_launcher)和 iOS(Assets.xcassets)的资源引用机制都不同。Flutter 的 HarmonyOS 适配层(flutter_ohos)在编译时会自动解析这些引用并打包到 HAP 中。

─────────────────────────────────────────────────

7.4 资源限定符

HarmonyOS 支持资源限定符来适配不同设备:resources/base/media/(默认)、resources/dark/media/(深色模式)、resources/tablet/media/(平板)等。对于 E-Brufen,只在 base 目录放置一份图标即可覆盖所有场景。

7.5 构建时自动生成

生成脚本应在每次构建前运行,确保图标始终与代码同步:

bash 复制代码
# 开发构建
dart run generate_icon.dart
flutter run -d <device_id>

# 生产构建
dart run generate_icon.dart
flutter build hap --release

也可以集成到 Makefile 或 Gradle 任务中,将生成步骤与构建绑定。

关于零外部资源的完整打包策略,参见 post-41

八、与传统方式对比

维度 传统方式(Figma/PS + 导出) 程序化生成
工具依赖 需要设计工具 仅需 Dart SDK
可复现性 取决于设计师的操作系统/版本 100% 可复现
版本控制 二进制 PNG 文件入 Git(bloat) 只有 ~200 行 Dart 源码
参数化定制 设计师重新出图 修改几个常量即可
文件大小 取决于导出设置 完全可控
CI/CD 集成 需要确保二进制文件存在 构建时自动生成
学习成本 需要设计技能 需要数学 + 二进制格式知识

九、扩展:修改图案

如果你希望生成不同的图标图案,只需修改 _generatePng() 中的数学公式:

  • 五瓣花 :将 sin(angle * 3) 改为 sin(angle * 5)
  • 不同配色 :修改 bgRbgGbgB 的基准值
  • 正方形而非圆角方形:移除第五节(edgeDist)的 Alpha 处理
  • 更大尺寸 :修改 const size = 5121024

所有修改仅涉及代码中的数值常量,无需重新导出任何文件。

十、验证生成的图标

bash 复制代码
# 1. 检查 PNG 签名(应为 89 50 4E 47 0D 0A 1A 0A)
xxd ohos/AppScope/resources/base/media/app_icon.png | head -1

# 2. 确认文件大小(约 1MB)
ls -lh ohos/AppScope/resources/base/media/app_icon.png

# 3. 用任何图片查看器打开,确认显示绿色渐变带花瓣纹理的圆角方形图标

小结

generate_icon.dart 用 186 行纯 Dart 代码完成了一个完整的 PNG 图标生成流水线:像素级数学绘制(五层叠加)、PNG 二进制编码(IHDR/IDAT/IEND 三个块)、zlib 压缩(BTYPE=00 零压缩模式)、CRC32 校验(IEEE 802.3 反射多项式)、以及 HarmonyOS Stage 模型的资源目录输出。

这个脚本体现了 E-Brufen "零外部资源 " 设计的核心原则------任何可能变化的二进制资源都用代码生成,将"资源文件"退化为"构建产物"。这不仅减少了 Git 仓库体积,更重要的是保证了构建的完全可复现性


延伸阅读post-39 PNG 格式详解 / post-40 手写 zlib 压缩 / post-41 零资源打包策略
作者简介:E-Brufen Dev,Flutter & 鸿蒙开发者,专注于跨平台移动应用开发与心理健康数字化。
项目源码https://gitcode.com/PengXiansheng/E-Brufen

相关推荐
不才难以繁此生5 小时前
ArkUI 列表性能实战|中式美食菜谱列表如何拆组件、控重绘和稳住滑动体验
harmonyos·arkts·状态管理·foreach·arkui·列表性能·组件拆分
Ww.xh5 小时前
DevEco Studio虚拟机部署指南
华为·harmonyos
yuanlaile5 小时前
前端转 Flutter 踩坑实录:一套可落地 Flutter 鸿蒙 App 完整学习路线,建议收藏!
flutter·harmonyos·flutter开发鸿蒙·跨平台开发app·flutter深入学习
爸爸6195 小时前
08 ArkTS 类与静态方法设计:单例、DAO 与设计令牌
华为·harmonyos·鸿蒙系统
ZZZMMM.zip5 小时前
基于鸿蒙HarmonyOS NEXT开发AI天气助手应用:智能出行新体验与鸿蒙Flutter框架跨端实践
人工智能·flutter·华为·harmonyos·鸿蒙系统
心中有国也有家6 小时前
AtomGit Flutter 鸿蒙客户端:一键记录情绪的极致体验
学习·flutter·华为·harmonyos
ZZZMMM.zip6 小时前
营养方程式-营养餐单规划的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙系统
不才难以繁此生6 小时前
HarmonyOS RDB 实战|中式美食菜谱表设计:食材、步骤与购物清单怎么落库
flutter·harmonyos·rdb·中式美食·本地持久化
心中有国也有家6 小时前
AtomGit Flutter 鸿蒙客户端: 周情绪柱状图
学习·flutter·华为·harmonyos
恋猫de小郭6 小时前
Flutter material/cupertino 解耦最新进展,已经在做新样式了
android·前端·flutter