Flutter Dev Skill

v1.0.0

Flutter 跨平台移动开发技能。覆盖 Flutter 入门、Material 3 迁移、布局约束、动画 (Hero/Staggered)、自适应响应式布局、平台适配、大型屏幕支持、State 管理方案对比、 持久化、网络请求、性能优化。当用户提到 Flutter、Dart、移动开发、跨平台、Material...

0· 75·0 current·0 all-time

Install

OpenClaw Prompt Flow

Install with OpenClaw

Best for remote or guided setup. Copy the exact prompt, then paste it into OpenClaw for yhongm/flutter-dev-skill.

Previewing Install & Setup.
Prompt PreviewInstall & Setup
Install the skill "Flutter Dev Skill" (yhongm/flutter-dev-skill) from ClawHub.
Skill page: https://clawhub.ai/yhongm/flutter-dev-skill
Keep the work scoped to this skill only.
After install, inspect the skill metadata and help me finish setup.
Use only the metadata you can verify from ClawHub; do not invent missing requirements.
Ask before making any broader environment changes.

Command Line

CLI Commands

Use the direct CLI path if you want to install manually and keep every step visible.

OpenClaw CLI

Bare skill slug

openclaw skills install flutter-dev-skill

ClawHub CLI

Package manager switcher

npx clawhub@latest install flutter-dev-skill
Security Scan
VirusTotalVirusTotal
Benign
View report →
OpenClawOpenClaw
Benign
high confidence
Purpose & Capability
Name/description match the provided content: the bundle contains a large set of Flutter/Dart reference docs, examples, and migration guidance, which is appropriate for a "Flutter Dev" skill. There are no unrelated requirements (no cloud creds, no system binaries).
Instruction Scope
SKILL.md and the included reference files are documentation and code examples only. They do not instruct the agent to read system files, access environment variables, or transmit data to external endpoints beyond citing public Flutter docs. Trigger keywords are Flutter/Dart related and appropriate.
Install Mechanism
No install spec and no code files that would be downloaded or executed. Instruction-only skills are lowest risk because nothing is written to disk by an installer.
Credentials
The skill requests no environment variables, credentials, or config paths. There are no secret-bearing env names or disproportionate credential requests.
Persistence & Privilege
always is false and the skill is user-invocable; it does not request persistent system-wide privileges or modify other skills. Autonomous invocation is allowed by platform default but not combined with other red flags here.
Assessment
This skill is a documentation-only Flutter/Dart reference and appears coherent and low-risk: it asks for nothing and installs nothing. Before installing, you may still want to: (1) confirm you trust the publisher (source is listed as unknown), (2) review the trigger list so the agent only activates on appropriate queries, and (3) if you enable autonomous invocation, be aware the agent may offer Flutter guidance proactively — but there are no credentials or installers here to expose or execute.

Like a lobster shell, security has layers — review code before you run it.

latestvk970crm5528fw7ajqj215y41gx85fwvc
75downloads
0stars
1versions
Updated 4d ago
v1.0.0
MIT-0

Widget 体系与 State 管理

Widget 类型

类型说明示例
StatelessWidget不可变,props 决定 UIText, Icon, Container
StatefulWidget可变,通过 State 管理变化Checkbox, TextField, Scaffold

State 管理基础

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;

  void _increment() {
    setState(() { _counter++; }); // 触发重建
  }
}

BuildContext

每个 Widget 的 build() 方法接收 BuildContext。Context 包含:

  • 当前 widget 在 widget 树中的位置
  • 访问 Theme.of(context)MediaQuery.of(context)
  • 访问祖先 widget 提供的 InheritedWidget

布局约束

核心规则

"Constraints go down. Sizes go up. Parent sets position."

父 widget 向下传递约束;子 widget 向上报告尺寸;父 widget 决定子 widget 的位置。

BoxConstraints

类型说明场景
Tightmax == min,固定尺寸SizedBox(width: 100)
Loosemin == 0,尺寸可变Container() 默认
Unboundedmax == double.infinityScrollable、Flex 延伸方向

常用布局 Widget

Widget用途
Container通用盒子
SizedBox固定尺寸盒子
ConstrainedBox施加额外约束
Padding内边距
Row / Column线性布局(Flex)
Expanded填充剩余空间
Flexible类似 Expanded,可控制策略
Stack绝对定位布局
PositionedStack 中定位
LayoutBuilder布局阶段获取约束

常见布局错误

// ❌ Expanded 在 Stack 中无效 → 用 Positioned 或 Align
// ❌ 在 unbounded 约束中 Expanded 无效 → 用 Slivers

// ✅ ConstrainedBox 在 Loose 约束内
ConstrainedBox(
  constraints: BoxConstraints(minWidth: 100, maxWidth: 200),
  child: Text('Hello'),
)

详细参考:layout-constraints.md


Material 3 迁移

核心开关

MaterialApp(
  theme: ThemeData(useMaterial3: true),
);

关键变化

M2M3
backgroundsurface
primarySwatchColorScheme.fromSeed()
ButtonThemeFilledButtonTheme
MaterialStateWidgetState
FlatButtonFilledButton

种子色

ColorScheme.fromSeed(seedColor: Colors.blue)

详细参考:material-3.md


动画系统

Hero 动画

// 页面 A 和 B
Hero(
  tag: 'photo',
  child: Image.asset('photo.jpg'),
)

Staggered 动画

// 一个控制器驱动多个 Interval 动画
Animation<double> get _opacity => Tween(begin: 0.0, end: 1.0).animate(
  CurvedAnimation(parent: _controller, curve: Interval(0.0, 0.5)),
);
Animation<double> get _scale => Tween(begin: 0.0, end: 1.0).animate(
  CurvedAnimation(parent: _controller, curve: Interval(0.5, 1.0)),
);

详细参考:hero-animations.md | staggered-animations.md


State 管理方案对比

Provider(入门级)

// ChangeNotifier
class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() { _count++; notifyListeners(); }
}

// 注册
runApp(ChangeNotifierProvider(
  create: (_) => CounterModel(),
  child: MyApp(),
));

// 重建 UI
Consumer<CounterModel>(
  builder: (context, model, child) => Text('${model.count}'),
);

// 读取(不重建)
final count = context.read<CounterModel>().count;

Riverpod(生产推荐)

// 纯 Dart,无 context 依赖
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}

// 使用
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.watch(counterProvider);
  return Text('$count');
}

// ref.watch vs ref.read
// watch: 重建 UI(监听变化)
// read: 不重建,仅读取当前值(事件处理中常用)

// 强制刷新 Provider
ref.invalidate(counterProvider);  // 重置状态,重新执行 Provider 回调
// 或者基于现有状态刷新
ref.invalidate(myProvider);  // 下次访问时重新创建

// Family Provider(带参数)
final userProvider = Provider.family<User, String>((ref, id) {
  return User(id: id, name: 'User $id');
});
// 使用
final user = ref.watch(userProvider('abc'));

Provider 作用域

// ✅ 顶层 Provider:全局单例
final prefsProvider = Provider<SharedPreferences>((ref) {
  return SharedPreferences.getInstance() as SharedPreferences;
});

// ✅ 作用域 Provider:仅在子树内有效
ProviderScope(
  overrides: [counterProvider.overrideWith(() => MockCounter())],
  child: MyApp(),
);

// ✅ Child Provider:可读取父 Provider
final userProvider = Provider((ref) {
  final prefs = ref.watch(prefsProvider); // 依赖父 Provider
  return UserRepo(prefs: prefs);
});

BLoC(大型项目)

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

BlocBuilder<CounterBloc, int>(
  builder: (context, count) => Text('$count'),
)

方案选型

方案复杂度适用场景
setState简单 widget
Provider中小型应用
Riverpod中高生产推荐
BLoC团队协作/大型项目

持久化存储

SharedPreferences(轻量配置)

final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'yhong');
await prefs.setInt('counter', 42);
final name = prefs.getString('username') ?? '';
await prefs.remove('counter');

SQLite(结构化数据)

final db = await openDatabase('myapp.db', version: 1,
  onCreate: (db, version) async {
    await db.execute(
      'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT)');
  });

await db.insert('tasks', {'title': 'Finish report'});
final maps = await db.query('tasks', where: 'id = ?', whereArgs: [1]);

// 批量事务
await db.transaction((txn) async {
  for (final task in tasks) await txn.insert('tasks', task);
});

Hive(高性能 KV)

Hive.registerAdapter(TaskAdapter());
final box = await Hive.openBox<Task>('tasks');
box.put('task1', Task(title: 'Hello'));
final task = box.get('task1');

网络请求

Dio(推荐)

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: Duration(seconds: 10),
  headers: {'Content-Type': 'application/json'},
));

// 全局拦截器:Token 自动附加
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    final token = getToken(); // 从安全存储读取
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  },
  onError: (error, handler) {
    // Token 过期自动刷新重试
    if (error.response?.statusCode == 401) {
      refreshToken().then((newToken) {
        // 重试原请求
        final opts = error.requestOptions;
        opts.headers['Authorization'] = 'Bearer $newToken';
        dio.fetch(opts).then(
          (r) => handler.resolve(r),
          (e) => handler.reject(e),
        );
      }).catchError((e) => handler.reject(error));
    } else {
      handler.next(error);
    }
  },
));

// GET / POST
final resp = await dio.get('/users', queryParameters: {'page': 1});
final resp = await dio.post('/users', data: {'name': 'yhong'});

统一响应体处理

// 常见后端响应格式:{ code, data, message }
class ApiResp<T> {
  final int code;
  final T? data;
  final String? message;

  bool get ok => code == 0;
}

// Dio 响应拦截器统一解析
dio.interceptors.add(InterceptorsWrapper(
  onResponse: (resp, handler) {
    final body = resp.data;
    if (body is Map && body.containsKey('code')) {
      if (body['code'] != 0) {
        // 业务错误,转为异常
        handler.reject(
          DioException(
            requestOptions: resp.requestOptions,
            error: body['message'] ?? 'Unknown error',
            type: DioExceptionType.badResponse,
          ),
        );
        return;
      }
      // 替换为 data 部分
      resp.data = body['data'];
    }
    handler.next(resp);
  },
));

// 使用时直接取 data
final List users = await dio.get('/users').then((r) => r.data);

错误处理

try {
  final resp = await dio.get('/users');
} on DioException catch (e) {
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
      // 网络超时
    case DioExceptionType.badResponse:
      final statusCode = e.response?.statusCode;
      if (statusCode == 401) { /* 未授权 */ }
      if (statusCode == 403) { /* 禁止 */ }
      if (statusCode == 404) { /* 资源不存在 */ }
      if (statusCode != null && statusCode >= 500) { /* 服务器错误 */ }
    case DioExceptionType.cancel:
      // 请求被取消
    default:
      // 网络不可达等
  }
}

JSON 解析

@JsonSerializable()
class User {
  final String name;
  final int age;
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

性能优化

重建控制

// ✅ Selector 精确重建(替代 Consumer)
Selector<Model, String>(
  selector: (_, m) => m.title,
  builder: (_, title, __) => Text(title),
);

// ✅ const 构造
const Text('Hello');
const Padding(padding: EdgeInsets.all(16));

// ❌ build 中创建新对象
Widget build(BuildContext context) {
  return SomeWidget(items: List.generate(100, (i) => Item(i))); // 每次重建
}

// ✅ initState 中初始化
final items = List.generate(100, (i) => Item(i));

长列表优化

// ✅ ListView.builder 懒加载
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
);

// ✅ cacheExtent
ListView.builder(cacheExtent: 200, itemBuilder: ...);

// ✅ RepaintBoundary 隔离重绘
RepaintBoundary(child: MyComplexWidget());

响应式布局实战

自适应 Scaffold

class AdaptiveScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.of(context).size.width;

    if (width < 600) {
      return Scaffold(
        body: body,
        bottomNavigationBar: NavigationBar(
          selectedIndex: currentIndex,
          onDestinationSelected: onIndexChanged,
          destinations: _destinations,
        ),
      );
    }

    return Scaffold(
      body: Row(
        children: [
          NavigationRail(
            selectedIndex: currentIndex,
            onDestinationSelected: onIndexChanged,
            labelType: width > 840
                ? NavigationRailLabelType.all
                : NavigationRailLabelType.selected,
            destinations: _destinations
                .map((d) => NavigationRailDestination(
                      icon: d.icon,
                      label: d.label,
                    ))
                .toList(),
          ),
          const VerticalDivider(thickness: 1, width: 1),
          Expanded(child: body),
        ],
      ),
    );
  }
}

响应式列数

LayoutBuilder(
  builder: (context, constraints) {
    final cols = constraints.maxWidth > 900 ? 3
               : constraints.maxWidth > 600 ? 2
               : 1;
    return GridView.count(
      crossAxisCount: cols,
      childAspectRatio: 1.5,
      children: items.map((item) => Card(child: item)).toList(),
    );
  },
);

详细参考:adaptive-responsive.md | large-screens.md


平台适配

平台检测

final idiom = MediaQuery.of(context).size.shortestSide >= 600 ? 'tablet' : 'phone';
final platform = Theme.of(context).platform;

键盘快捷键

Shortcuts(
  shortcuts: {
    LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS): SaveIntent(),
  },
  child: Actions(
    actions: { SaveIntent: CallbackAction(onInvoke: (_) => _save()) },
    child: focusNode,
  ),
)

鼠标 Hover

MouseRegion(
  onEnter: (_) => setState(() => _isHovered = true),
  onExit: (_) => setState(() => _isHovered = false),
  child: MyWidget(),
)

详细参考:platform-idioms.md


避坑指南

State 管理

错误正确
❌ 在 build() 中调用 setState()✅ 在回调中调用
initState() 中直接使用 contextdidChangeDependencies()
❌ 大型对象放 Stateconst 构造函数

布局

错误正确
ExpandedStackPositioned/Align
❌ 硬编码尺寸MediaQuery/LayoutBuilder

M3 迁移

错误正确
ButtonThemeFilledButtonTheme
background/onBackgroundsurface/onSurface
MaterialStateWidgetState

快速参考

热重载 vs 热重启

操作效果
R (Hot Reload)保持 State,仅重建 Widget 树
Shift+R (Hot Restart)State 丢失,重新执行 main()

常用 EdgeInsets

EdgeInsets.all(16.0)
EdgeInsets.symmetric(v: 8)
EdgeInsets.only(left: 16)
EdgeInsets.fromLTRB(4,8,4,8)

AnimationController 生命周期

_controller = AnimationController(
  duration: Duration(milliseconds: 300),
  vsync: this,
);

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

组件速查

按钮导航容器
FilledButtonAppBarCard
FilledButton.tonalNavigationBarDialog
OutlinedButtonNavigationRailSnackBar
TextButtonNavigationDrawerBottomSheet

详细参考:buttons-input.md | navigation-components.md | display-components.md


来源

文档版本:Flutter 3.x + Material 3 URL: https://flutter.cn/docs 抓取时间:2026-04-24

参考文档

文件行数覆盖内容
material-3.md767Material 3 迁移完整指南
layout-constraints.md368布局约束与约束传递机制
overview-getting-started.md376Flutter 入门与平台能力
buttons-input.md424按钮与输入组件
adaptive-responsive.md298自适应响应式布局
platform-idioms.md358平台适配与设备类型
large-screens.md264大屏幕与折叠屏支持
navigation-components.md350导航组件
display-components.md300展示组件
hero-animations.md239Hero 动画
staggered-animations.md354交错动画
testing.md121测试指南(单元/Widget/集成/Mock)
dependency-injection.md81依赖注入(get_it/Riverpod/injectable)

| testing.md | 参考文档:测试指南 |

单元测试(flutter_test)

详见 testing.md(flutter_test / widget / integration / Mock)

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() { _count++; notifyListeners(); }
}

test('CounterModel increments correctly', () {
  final model = CounterModel();
  expect(model.count, 0);
  model.increment();
  expect(model.count, 1);
});

Widget 测试

testWidgets('CounterWidget displays count and increments', (tester) async {
  await tester.pumpWidget(MaterialApp(home: CounterWidget()));
  expect(find.text('0'), findsOneWidget);
  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();
  expect(find.text('1'), findsOneWidget);
});

Riverpod Provider 测试

testProvider('counterProvider increments', (override) async {
  await runProviderScope((ref) async {
    final counter = ref.watch(counterProvider);
    expect(counter, 0);
    ref.read(counterProvider.notifier).increment();
    expect(ref.watch(counterProvider), 1);
  }, overrides: []);
});

集成测试

setUpAll(() async { await FlutterDriver.connect(); });

test('app loads and shows home', () async {
  final driver = await FlutterDriver.connect();
  await driver.waitFor(find.byType('MyHomePage'));
  expect(find.text('Home'), findsOneWidget);
});

Mock + mockito

@GenerateMocks([UserRepository])
void main() {
  late MockUserRepository mockRepo;
  setUp(() { mockRepo = MockUserRepository(); });

  test('loads user data', () async {
    when(mockRepo.getUser('123'))
        .thenAnswer((_) async => User(id: '123', name: 'Alice'));
    final user = await mockRepo.getUser('123');
    expect(user.name, 'Alice');
    verify(mockRepo.getUser('123')).called(1);
  });
}

依赖注入

详见 dependency-injection.md(get_it / injectable / DI 陷阱)

import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;

void setupDependencies() {
  getIt.registerLazySingleton<Dio>(() => Dio());
  getIt.registerFactory<UserRepository>(
    () => UserRepository(dio: getIt<Dio>()),
  );
  getIt.registerSingleton<SharedPreferences>(prefs);
}

final repo = getIt<UserRepository>();

Riverpod + get_it

final dioProvider = Provider<Dio>((ref) => getIt<Dio>());
final userRepoProvider = Provider<UserRepository>(
  (ref) => UserRepository(dio: ref.watch(dioProvider)),
);

injectable

@injectable
class AuthRepository {
  final Dio dio;
  AuthRepository({required this.dio});
}
// configureDependencies(); // 自动生成

DI 陷阱

错误正确
❌ 在 build()GetIt.instance<Dio>()✅ 顶层 setupDependencies() 中注册
❌ 单例中引用非单例✅ 确保生命周期一致
❌ 直接 Dio() 硬编码✅ 通过 getIt<Dio>() 注入

输出格式规范

回复结构

  1. 直接回答 — 一段简洁的话给出核心答案
  2. 代码示例 — 提供完整的 Dart/Flutter 代码
  3. 实现要点 — 关键步骤和注意事项
  4. 避坑提醒 — 常见错误 + 正确做法

禁用格式

  • ❌ 不要显式分层(避免"第一层/第二层/框架分析"等字眼)
  • ❌ 不要长篇解释概念,要直接给出实现
  • ❌ 不要只给代码片段,要给完整可运行的示例
  • ✅ 输出应是一段干净的话 + 完整代码

Comments

Loading comments...