AI生成Flutter UI代码实践(二)

在这里插入图片描述
紧接上篇 AI生成Flutter UI代码实践(一),本篇更多是Cursor + MCP在实际使用中的优化。

1. 代码规范

一般情况下,每次生成的代码都是“重头开始”的。比如AI会将相关代码写到一个文件中,“随机”的创建一个文件夹放入其中,并在main中引用。如果只是体验效果,我认为完全没有问题。

但实际项目中,我们有自己的项目规范,命名规范。比如有些人会把页面放在page目录下,有的叫uiscreen。还有一些颜色,样式的定义,方便复用。但是AI生成的代码,基本不会复用,每次都是重新生成的,一定程度上无法复用。

如果每次写提示词强调又显得啰嗦。这个时候就可以使用cursor 的Rules,编写我们得项目说明。具体的示例可以参考:awesome-cursorrules

下面是我写的部分规则,仅供参考:

- **目录划分规则**:
  - **lib/**:包含所有Dart代码。
  - **screen/**:应用所有页面,按功能分组。
  - **widget/**:可重用UI组件。
  - **utils/**:工具类和辅助函数。
  - **entity/**:数据模型。
  - **res/**:资源文件(颜色、主题等)。
  - **l10n/**:本地化资源。
  - **routes/**:应用路由管理。
  - **gen/**:自动生成的代码,不要修改。
  - **assets/**:存放项目中的资源文件。

- 命名约定
  - **文件命名**:使用snake_case(如`splash_page.dart`)。
  - **类命名**:使用PascalCase(如`SplashPage`)。
  - **变量/函数**:使用camelCase(如`loadData`)。
  - **颜色**:使用camelCase,规则color{HexValue}{Opacity?}(如`colorF1F1F1`,`colorFFFFFF60`)。
  - **文字样式**:使用camelCase,规则text{Size}{Weight}{ColorHex}(如`text20BoldFFFFFF`)。
  - **图片命名**:使用snake_case(如`icon_back.png`)。

- 核心原则
  - **关注点分离**:UI、数据和业务逻辑分离。
  - **单一职责**:每个类/文件只有一个目的。
  - **DRY原则**:避免代码重复。
  - **可测试性**:代码应易于测试。
  - **一致性**:整个项目保持一致的代码风格和组织。
  - **图片引用**:图片引用使用Assets类中的常量(如:Assets.images.iconBack.image(width: 20, height: 20))。
  - **颜色引用**:颜色引用使用Colours类中的常量(如:Colours.white),如果没有先添加再引用。
  - **文字样式**:`MaterialApp`中有设置全局字体,`TextStyle`中不需要重复指定,不用设置`height`属性。字重只用Regular(w400)、Medium(w500)、ExtraBold(w700)和Black(w900)四种。
  - **文字样式引用**:`TextStyle`引用使用TextStyles类中的常量(如:TextStyles.textBold14),如果没有先添加再引用。
  - **其他**:添加图片或是路由后,执行命令`flutter packages pub run build_runner build --delete-conflicting-outputs`,生成代码。

编写的规则放在项目的.cursor\rules目录下,可以在设置中看到,同时也支持添加多个。

在这里插入图片描述
最后打开文件,设置成Always,保证每次都使用。

在这里插入图片描述

每次对话时,这里就能看到生效的规则:

在这里插入图片描述

看下运行效果。

请添加图片描述

  • 按照要求将页面,图片放到对应的目录。
  • 颜色,字体样式按要求命名并添加到对应的类中并引用。
  • 自动添加页面路由,自动执行flutter_gen_runner命令等等。

2. 宽高信息

Figma-Context-MCP中有一个转换层,它的作用是简化figma api返回的信息,隐藏不必要的数据,以减少上下文大小,帮助大模型更好地理解设计中最重要的部分。举个例子,200多kb的json信息,简化后大约十几kb。

figma api给到的信息中有各个组件的宽高以及x,y值。但是Figma-Context-MCP作者好像有意的去掉这部分信息。但实际开发中,高度是比较重要的,宽度更多是屏幕宽度减去间距,或是自适应,相对重要度低一些。所以我就将这部分信息又加上了。大模型在实际的代码输出中,有时还是比较聪明的,没有都去写死宽高。

  if (hasValue("absoluteBoundingBox", n)) {
    const { width, height } = n.absoluteBoundingBox || {};
    simplified.boundingBox = {
      width: width || 0,
      height: height || 0,
      x: x || 0,
      y: y || 0,
    };
  }

3. 图片下载

3倍图

目前代码中下载的是2倍图,而我需要的是三倍图,所以做了一个简单修改。

请添加图片描述

自定义节点切图下载

目前Figma-Context-MCP的下载图片工具只能下载画板上的原图。但实际上这张图可能在设计时做了裁剪,镜像等操作。另外还有些背景图实际是图片和一个渐变背景组成。但实际使用时,我们需要的是这部分的最终效果切图。不会自己拿着原图做裁剪或是自己写个渐变色去实现这些效果。

好在figma api支持任意节点的图片导出下载,所以我修改了一下下载部分的代码,改为寻找节点名称中包含icon_image_的节点,进行下载。

import type { SimplifiedDesign, SimplifiedNode } from "./services/simplify-node-response.js";
// 查找带有"切图"标识的节点
function findCutImageNodes(nodes: SimplifiedNode[], result: SimplifiedNode[] = []): SimplifiedNode[] {
  for (const node of nodes) {
    if (node.name && (node.name.includes("icon_") || node.name.includes("image_"))) {
      result.push(node);
    }
    
    if (node.children && node.children.length > 0) {
      findCutImageNodes(node.children, result);
    }
  }
  
  return result;
}

server.tool(
    "download_figma_images",
    "Download SVG and PNG images used in a Figma file based on the IDs of image or icon nodes",
    {
      fileKey: z.string().describe("The key of the Figma file containing the node"),
      nodeId: z
      .string()
      .optional()
      .describe(
        "The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided",
      ),
      depth: z
      .number()
      .optional()
      .describe(
        "How many levels deep to traverse the node tree, only use if explicitly requested by the user",
      ),
      localPath: z
        .string()
        .describe(
          "The absolute path to the directory where images are stored in the project. If the directory does not exist, it will be created. The format of this path should respect the directory format of the operating system you are running on. Don't use any special character escaping in the path name either.",
        ),
    },
    async ({ fileKey, nodeId, depth, localPath }) => {
      try {
        let file: SimplifiedDesign;

        if (nodeId) {
          file = await figmaService.getNode(fileKey, nodeId, depth);
        } else {
          file = await figmaService.getFile(fileKey, depth);
        }

         // 查找所有带有"切图"标识的节点
         const cutImageNodes = findCutImageNodes(file.nodes);
          
         if (cutImageNodes.length === 0) {
           return {
             content: [{ type: "text", text: "未找到切图" }],
           };
         }
         let renderRequests;
         // 将找到的节点转换为下载请求
         renderRequests = cutImageNodes.map(node => ({
           nodeId: node.id,
           fileName: `${node.name.replace(/\s+/g, '_')}.png`,
           fileType: "png" as "png" | "svg",
         }));

         Logger.log(`Created ${renderRequests.length} download requests for cut images`);

         const renderDownloads = figmaService.getImages(fileKey, renderRequests, localPath);

         const downloads = await Promise.all([renderDownloads]).then(([r]) => [
           ...r
         ]);

        // If any download fails, return false
        const saveSuccess = !downloads.find((success) => !success);
        return {
          content: [
            {
              type: "text",
              text: saveSuccess
                ? `成功,已下载 ${downloads.length} 个图片: ${downloads.join(", ")}`
                : "下载失败",
            },
          ],
        };
      } catch (error) {
        Logger.error(`Error downloading images from file ${fileKey}:`, error);
        return {
          isError: true,
          content: [{ type: "text", text: `Error downloading images: ${error}` }],
        };
      }
    },
  );
}

这样,我们可以自己去figma上找到需要的切图部分,按照规则命名即可。毕竟命名这部分免不了,现在下载的图标也不还是自己命名好后放到了项目中。

请添加图片描述

同时还修改了转换figma返回的页面json信息代码,不在处理自定义切图节点的子节点了。这样给到AI的数据也就不会有这些信息,生成的代码就是图片的引用。如果不做这步操作,AI聪明的时候没问题,抽风的时候还会实现这些“图片效果”。

  if (hasValue("children", n) && n.children.length > 0 && n.name && !(n.name.includes("icon_") || n.name.includes("image_"))) {
    let children = n.children
      .filter(isVisible)
      .map((child) => parseNode(globalVars, child, n))
      .filter((child) => child !== null && child !== undefined);
    if (children.length) {
      simplified.children = children;
    }
  }

  if (n.name && (n.name.includes("icon_") || n.name.includes("image_"))) {
    simplified.type = "IMAGE";
  }

4. 已知问题

截止目前(25年5月5日)发现的已知问题:

  • 个别颜色的引用没有遵循规则创建并引用,尤其是一些渐变用到的颜色。
  • 有时命名没有遵循规则。
  • Figma MCP工具下载图片时需要模型给到下载的路径和下载的节点。试了几个模型,这块存在不稳定的情况,比如有时没有给到节点,有时给的相对路径,Windows下给的路径有转义等。
  • 对于间距和控件的排列位置,目前还是需要手动去调整。
  • figma相较蓝湖使用起来还是不方便,查看属性不直观。

不过前三点相信随着模型能力的提升都会得到解决,目前来说影响也不是很大。如果第一次输出不达标,打不了就再进行几轮对话,让它再次修改。

最后总结一下这套方案的优点:

  • 代码可直接运行,书写规范。
  • 快速生成页面的框架,并适当进行功能封装。
  • 图片下载方便,并直接引用。
  • 文字信息(字号,字色,字重),布局信息(背景色,圆角,宽高,阴影)准确。

后面如果有新的进展也会继续同步这里,有这方面需要的收藏关注起来~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值