360 Secure SkillHub
360 安全技能中心

Flutter Dev Skill

提供Flutter跨平台开发全流程支持,涵盖Widget体系、布局约束、Material3迁移、状态管理方案对比、持久化、网络请求、性能优化等核心内容。适用于开发者解决布局错误、状态管理选型、性能瓶颈等各类Flutter开发问题,输入相关疑问,输出简洁解答+可运行代码示例+要点及避坑提醒。

yhongm
yhongm
热度 39.97
最近更新 2026-05-11 17:45:33
分类:开发依赖:纯提示词应用场景:代码生成 / 调试排错 / API 集成 / 数据处理 ...

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add flutter-dev-skill
Metadata
name
flutter-dev
description
Flutter 跨平台移动开发技能。覆盖 Flutter 入门、Material 3 迁移、布局约束、动画 (Hero/Staggered)、自适应响应式布局、平台适配、大型屏幕支持、State 管理方案对比、 持久化、网络请求、性能优化。当用户提到 Flutter、Dart、移动开发、跨平台、Material Design、 Flutter Widget、热重载、StatelessWidget、StatefulWidget、setState、Provider、Riverpod、 BLoC、布局约束、BoxConstraints、动画、Animation、Hero、Staggered、响应式、自适应时触发。
trigger
Flutter|flutter|Dart|移动开发|跨平台|Material Design|Flutter Widget|热重载|Hot Reload|StatelessWidget|StatefulWidget|setState|BuildContext|InheritedWidget|Provider|Riverpod|BLoC|布局约束|BoxConstraints|动画|Animation|Hero|Staggered|响应式|自适应|adaptive|MediaQuery|LayoutBuilder|平台适配|SharedPreferences|Hive|Dio|性能优化|重绘|rebuild
tags
["flutter","dart","mobile-development","cross-platform","ui-design"]
hermes
{"platform":"hermes","version":"1.1","last_updated":"2026-04-24","source":"https://flutter.cn/docs\nhttps://flutter.cn/docs/release/breaking-changes/material-3-migration\nhttps://flutter.cn/docs/development/ui/layout/constraints\nhttps://flutter.cn/docs/development/ui/animations/hero-animations\nhttps://flutter.cn/docs/development/ui/animations/staggered-animations\n"}

Widget 体系与 State 管理

Widget 类型

类型 说明 示例
StatelessWidget 不可变,props 决定 UI Text, 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

类型 说明 场景
Tight max == min,固定尺寸 SizedBox(width: 100)
Loose min == 0,尺寸可变 Container() 默认
Unbounded max == double.infinity Scrollable、Flex 延伸方向

常用布局 Widget

Widget 用途
Container 通用盒子
SizedBox 固定尺寸盒子
ConstrainedBox 施加额外约束
Padding 内边距
Row / Column 线性布局(Flex)
Expanded 填充剩余空间
Flexible 类似 Expanded,可控制策略
Stack 绝对定位布局
Positioned Stack 中定位
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),
);

关键变化

M2 M3
background surface
primarySwatch ColorScheme.fromSeed()
ButtonTheme FilledButtonTheme
MaterialState WidgetState
FlatButton FilledButton

种子色

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() 中直接使用 context ✅ didChangeDependencies()
❌ 大型对象放 State ✅ const 构造函数

布局

错误 正确
❌ Expanded 在 Stack 中 ✅ Positioned/Align
❌ 硬编码尺寸 ✅ MediaQuery/LayoutBuilder

M3 迁移

错误 正确
❌ ButtonTheme ✅ FilledButtonTheme
❌ background/onBackground ✅ surface/onSurface
❌ MaterialState ✅ WidgetState

快速参考

热重载 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();
}

组件速查

按钮 导航 容器
FilledButton AppBar Card
FilledButton.tonal NavigationBar Dialog
OutlinedButton NavigationRail SnackBar
TextButton NavigationDrawer BottomSheet

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


来源

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

参考文档

文件 行数 覆盖内容
material-3.md 767 Material 3 迁移完整指南
layout-constraints.md 368 布局约束与约束传递机制
overview-getting-started.md 376 Flutter 入门与平台能力
buttons-input.md 424 按钮与输入组件
adaptive-responsive.md 298 自适应响应式布局
platform-idioms.md 358 平台适配与设备类型
large-screens.md 264 大屏幕与折叠屏支持
navigation-components.md 350 导航组件
display-components.md 300 展示组件
hero-animations.md 239 Hero 动画
staggered-animations.md 354 交错动画
testing.md 121 测试指南(单元/Widget/集成/Mock)
dependency-injection.md 81 依赖注入(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. 避坑提醒 — 常见错误 + 正确做法

禁用格式

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