🐟 Flutter 摸鱼指南(二):告别 Ctrl+C/V,3 秒生成文件

59 阅读9分钟

Ctrl+C 一时爽,改名改到火葬场。

右键 → 输名字 → 回车 → 3 秒搞定


😤 Ctrl+C/V 工程师的日常

场景操作用时内心 OS
写新页面找老页面→复制→改名⌨3处→删代码3 分钟我是来写业务的,不是当复读机!
写 Model新建文件→写字段→写fromJson→嵌套对象再写一遍10 分钟2024年了还手写fromJson??
写 Widget找类似的→复制→改名→删多余代码2 分钟又是复制粘贴...

🚀 Flu CLI:让机器干这些破事

核心思想:重复性工作,交给工具。


🎯 功能一:生成 Page(3 秒)

最简单的用法

generate-page.gif 操作

  1. lib/pages 目录右键
  2. 选择 Flu: 生成文件
  3. Page → 输入 profile
  4. Stateful

等 3 秒,文件自动生成并打开

// lib/pages/profile_page.dart
import 'package:flutter/material.dart';

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

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: const Center(
        child: Text('Profile Page'),
      ),
    );
  }
}

对比

方式用时操作
复制粘贴3 分钟找文件 → 复制 → 改名 → 删代码
Flu CLI3 秒右键 → 选类型 → 输名字

省时:2 分 57 秒 🐟


进阶:自动带 ViewModel

generate-page-with-vm.gif 场景:写页面时 99% 需要配套的 ViewModel。

传统方式

  1. 生成 ProfilePage
  2. 再右键一次
  3. 生成 ProfileViewModel
  4. 在 Page 里 import ViewModel
  5. 写绑定代码

Flu CLI 方式

生成 Page 时,勾选 ✅ 同时生成 ViewModel

自动生成两个文件

文件 1:profile_page.dart

import 'package:flutter/material.dart';
import '../viewmodels/profile_viewmodel.dart';  // 自动 import

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

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  late ProfileViewModel _viewModel;  // 自动声明

  @override
  void initState() {
    super.initState();
    _viewModel = ProfileViewModel();  // 自动初始化
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: const Center(
        child: Text('Profile Page'),
      ),
    );
  }

  @override
  void dispose() {
    _viewModel.dispose();  // 自动释放
    super.dispose();
  }
}

文件 2:profile_viewmodel.dart

import 'package:flutter/material.dart';

class ProfileViewModel extends ChangeNotifier {
  // TODO: Add your business logic here

  @override
  void dispose() {
    super.dispose();
  }
}

省时:又省 2 分钟 🐟


高级:自动继承 BasePage

auto-inherit-base.gif

场景:团队有统一的 BasePage 基类,包含:

  • 统一的 Loading 状态
  • 统一的错误处理
  • 统一的生命周期日志

问题:每次生成的 Page 都要手动改继承。

解决方案:配置 .flu-cli.json

{
  "generators": {
    "page": {
      "withBasePage": true,
      "basePageClass": "BasePage",
      "basePageImport": "package:my_app/core/base/base_page.dart"
    }
  }
}

配置后,生成的代码自动继承

import 'package:flutter/material.dart';
import 'package:my_app/core/base/base_page.dart';  // 自动 import

class ProfilePage extends BasePage {  // 自动继承
  const ProfilePage({super.key});

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends BasePageState<ProfilePage> {
  @override
  Widget build(BuildContext context) {
    return buildScaffold(  // 使用基类的方法
      title: 'Profile',
      body: const Center(
        child: Text('Profile Page'),
      ),
    );
  }
}

配置一次,永久生效 🐟🐟🐟


🎯 功能二:生成 Model(1 秒)

最强大的功能:从 JSON 生成

generate-model-from-json.gif

后端给的 JSON

{
  "id": 1001,
  "nickname": "张三",
  "avatar": "https://example.com/avatar.png",
  "level": 5,
  "profile": {
    "gender": "male",
    "birthday": "1995-01-01",
    "city": "深圳"
  },
  "stats": {
    "followers": 1234,
    "following": 567
  }
}

操作

  1. 把 JSON 保存为 user.json
  2. 右键这个文件 → Flu: 生成文件Model
  3. 选择 从 JSON 生成

自动生成 3 个类

user_model.dart

class UserModel {
  final int? id;
  final String? nickname;
  final String? avatar;
  final int? level;
  final ProfileModel? profile;
  final StatsModel? stats;

  UserModel({
    this.id,
    this.nickname,
    this.avatar,
    this.level,
    this.profile,
    this.stats,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'] as int?,
      nickname: json['nickname'] as String?,
      avatar: json['avatar'] as String?,
      level: json['level'] as int?,
      profile: json['profile'] != null
          ? ProfileModel.fromJson(json['profile'] as Map<String, dynamic>)
          : null,
      stats: json['stats'] != null
          ? StatsModel.fromJson(json['stats'] as Map<String, dynamic>)
          : null,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'nickname': nickname,
      'avatar': avatar,
      'level': level,
      'profile': profile?.toJson(),
      'stats': stats?.toJson(),
    };
  }
}

class ProfileModel {
  final String? gender;
  final String? birthday;
  final String? city;

  ProfileModel({this.gender, this.birthday, this.city});

  factory ProfileModel.fromJson(Map<String, dynamic> json) {
    return ProfileModel(
      gender: json['gender'] as String?,
      birthday: json['birthday'] as String?,
      city: json['city'] as String?,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'gender': gender,
      'birthday': birthday,
      'city': city,
    };
  }
}

class StatsModel {
  final int? followers;
  final int? following;

  StatsModel({this.followers, this.following});

  factory StatsModel.fromJson(Map<String, dynamic> json) {
    return StatsModel(
      followers: json['followers'] as int?,
      following: json['following'] as int?,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'followers': followers,
      'following': following,
    };
  }
}

自动识别

  • ✅ 嵌套对象(profile、stats)
  • ✅ 类型推断(int、String)
  • ✅ 可选字段(全部 nullable)
  • ✅ fromJson / toJson 方法

对比

方式用时容易出错吗
手写10 分钟✅ 容易拼错字段
Flu CLI1 秒❌ 机器生成,不会错

省时:9 分 59 秒 🐟🐟🐟


🎯 功能三

:其他文件类型

Widget(可复用组件)

// custom_button_widget.dart
import 'package:flutter/material.dart';

class CustomButtonWidget extends StatelessWidget {
  const CustomButtonWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      // TODO: Implement widget
    );
  }
}

Service(服务层)

// user_service.dart
class UserService {
  // TODO: Add service methods
  
  Future<void> fetchUserInfo() async {
    // Implementation
  }
}

Module(一整个功能模块)

输入 login,自动生成:

lib/features/login/
├── pages/
│   └── login_page.dart
├── viewmodels/
│   └── login_viewmodel.dart
├── widgets/
│   └── (empty)
└── models/
    └── (empty)

一次生成整个模块结构 🐟🐟


🐟 摸鱼小结

操作频率(3年)传统Flu CLI省时
生成 Page500 次3 分钟3 秒24.5 小时
生成 Model300 次10 分钟1 秒49.9 小时
生成 Widget200 次2 分钟2 秒6.6 小时
总计1000 次--81 小时

81 小时 = 10 个工作日 = 2 周。告别 Ctrl+C/V,去摸鱼吧!🐟🐟🐟


🛠️ 如何配置?

第一步:初始化配置

右键项目根目录 → Flu: 初始化项目 → 自动生成 .flu-cli.json

第二步:改配置(可选)

{
  "generators": {
    "page": {
      "path": "lib/pages",              // 生成路径
      "fileSuffix": "_page",             // 文件后缀
      "withViewModel": true,             // 自动生成 VM
      "withBasePage": true,              // 继承基类
      "basePageClass": "BasePage",
      "basePageImport": "package:my_app/core/base/base_page.dart"
    },
    "viewModel": {
      "path": "lib/viewmodels",
      "fileSuffix": "_viewmodel",
      "withBaseViewModel": true,
      "baseViewModelClass": "BaseViewModel",
      "baseViewModelImport": "package:my_app/core/base/base_viewmodel.dart"
    },
    "model": {
      "path": "lib/models",
      "fileSuffix": "_model"
    },
    "widget": {
      "path": "lib/widgets",
      "fileSuffix": "_widget"
    }
  }
}

配置一次,全员共享(提交到仓库即可)


❓ FAQ

问题答案
生成的代码能改吗?当然,就是普通 Dart 文件,随便改
支持 GetX/Riverpod 吗?支持,配置 ViewModel 基类即可(见下方示例)
能自定义模板吗?暂不支持,但可配置基类、路径、后缀
生成失败怎么办?查看 VSCode 输出面板的 Flu CLI 频道
JSON 转 Model 支持数组吗?支持,自动生成 List<XxxModel>

GetX 配置示例

{
  "generators": {
    "viewModel": {
      "baseViewModelClass": "GetxController",
      "baseViewModelImport": "package:get/get.dart"
    }
  }
}

📚 系列文章


💬 交流群:加微信 Huoye-TT 备注 "flu-cli"

开源地址Gitee | 求 Star!

📖 完整文档huozhiye.cn/flu-cli/


如果这篇文章帮你省了时间,点个赞呗 👍