[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6D1ZSCRY6RHH3PH6QBYNH2K":3,"skill-same:01M6D1ZSCRY6RHH3PH6QBYNH2K":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":54,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6D1ZSCRY6RHH3PH6QBYNH2K","mobile-flutter",null,"Flutter apps: project structure by feature, state management with Riverpod or Bloc, navigation with go_router, theming from tokens with Material 3, composing widgets and const constructors, lists, async work, platform channels, choosing packages, testing (unit, widget, golden, integration), performance, and flavors. Read when the project uses Flutter.","enowdev\u002Fenowxcli","enowdev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179420349?v=4","01M6D1MVX2KC0X3Z9NESE7P22K","Enow Dev.","01M6D1MW1FMZGBDKWAJY4GQX81",67,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fmobile-flutter\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"mobile flutter","flutter","mobile","async","feature","platform","widgets","material 3","tokens","project","structure","testing","router","read","work","state","packages","project structure","performance","go","2026-11-10 10:12:30",false,"---\nname: mobile-flutter\ndescription: \"Flutter apps: project structure by feature, state management with Riverpod or Bloc, navigation with go_router, theming from tokens with Material 3, composing widgets and const constructors, lists, async work, platform channels, choosing packages, testing (unit, widget, golden, integration), performance, and flavors. Read when the project uses Flutter.\"\n---\n\n# Flutter apps\n\nThe naive Flutter app: one 900-line screen with `setState` and HTTP calls\nin its methods, a `FutureBuilder` whose future is created in `build` and\nfires on every rebuild, `Navigator.push` everywhere and no deep links,\n`Colors.blue` scattered through widgets, Material switches on an iPhone,\nforty packages chosen by name, and no tests. This skill is how a Flutter\nteam structures, builds, tests and ships an app in 2026. Conventions in\n`mobile-ux`, offline data in `mobile-data`, stores in `mobile-release`.\n\n## 1. Structure\n\nFeature first, with a small shared core; follow the project if it differs.\n\n```\nlib\u002Fmain.dart               runApp(const ProviderScope(child: App()))\nlib\u002Fapp\u002F                    router, theme, the App widget\nlib\u002Fcore\u002F                   HTTP client, errors, storage, logging\nlib\u002Ffeatures\u002Forders\u002Fdata\u002F          DTOs, API calls, repository, Drift tables\nlib\u002Ffeatures\u002Forders\u002Fdomain\u002F        entities and interfaces, only when they earn it\nlib\u002Ffeatures\u002Forders\u002Fapplication\u002F   notifiers or blocs\nlib\u002Ffeatures\u002Forders\u002Fpresentation\u002F  screens and widgets\ntest\u002F mirrors lib\u002F; integration_test\u002F holds device journeys\n```\n\nCode generation with build_runner (`dart run build_runner watch -d`):\nfreezed for immutable models and unions, json_serializable,\nriverpod_generator. Freezed 3 requires the `abstract` or `sealed` keyword:\n\n```dart\n@freezed\nabstract class Order with _$Order {\n  const factory Order({required String id, required int totalCents, required DateTime createdAt}) = _Order;\n  factory Order.fromJson(Map\u003CString, dynamic> json) => _$OrderFromJson(json);\n}\n```\n\n## 2. State\n\nRiverpod 3 with code generation by default; Bloc when the project uses it.\n\n```dart\n@riverpod\nclass Orders extends _$Orders {\n  @override\n  Future\u003CList\u003COrder>> build() => ref.watch(orderRepositoryProvider).fetchOrders();\n\n  Future\u003Cvoid> cancel(String id) async {\n    await ref.read(orderRepositoryProvider).cancel(id);\n    if (!ref.mounted) return;\n    ref.invalidateSelf();\n  }\n}\n\nclass OrdersScreen extends ConsumerWidget {\n  const OrdersScreen({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    return switch (ref.watch(ordersProvider)) {\n      AsyncData(:final value) when value.isEmpty => const EmptyOrders(),\n      AsyncData(:final value) => OrderList(orders: value),\n      AsyncError(:final error) => ErrorView(error, onRetry: () => ref.invalidate(ordersProvider)),\n      _ => const OrdersSkeleton(),\n    };\n  }\n}\n```\n\n- `ref.watch` in `build`, `ref.read` in callbacks, `ref.listen` for side\n  effects (a snackbar, navigation). A refreshing provider still matches\n  `AsyncData`, so data stays on screen during a refresh.\n- Riverpod 3 retries failing providers automatically; after an `await`, a\n  notifier checks `ref.mounted` before touching state.\n- Bloc: sealed events and states, bloc_concurrency transformers\n  (`droppable()` for submits, `restartable()` for search), `BlocSelector`.\n- `setState` only for state local to one widget; no HTTP in widgets.\n\n## 3. Navigation with go_router\n\n```dart\nGoRouter(\n  initialLocation: '\u002Forders',\n  refreshListenable: auth, \u002F\u002F a Listenable that fires on sign-in and sign-out\n  redirect: (context, state) {\n    final atSignIn = state.matchedLocation == '\u002Fsign-in';\n    if (!auth.signedIn) return atSignIn ? null : '\u002Fsign-in';\n    return atSignIn ? '\u002Forders' : null;\n  },\n  routes: [\n    GoRoute(path: '\u002Fsign-in', builder: (context, state) => const SignInScreen()),\n    StatefulShellRoute.indexedStack(\n      builder: (context, state, shell) => AppShell(shell: shell),\n      branches: [\n        StatefulShellBranch(routes: [\n          GoRoute(path: '\u002Forders', builder: (context, state) => const OrdersScreen(), routes: [\n            GoRoute(path: ':id', builder: (context, state) => OrderScreen(id: state.pathParameters['id']!)),\n          ]),\n        ]),\n        StatefulShellBranch(routes: [GoRoute(path: '\u002Fsettings', builder: (context, state) => const SettingsScreen())]),\n      ],\n    ),\n  ],\n);\n```\n\n- `StatefulShellRoute.indexedStack` keeps a stack per tab. `context.go`\n  sets the location (the stack follows the URL); `context.push` adds a\n  screen on top. Typed routes with go_router_builder in larger apps.\n- Auth lives in `redirect` with `refreshListenable`, not in every screen.\n  Deep links arrive as locations once App Links and Universal Links are\n  configured natively.\n\n## 4. Theming\n\n```dart\nconst seed = Color(0xFF0B6E4F);\nMaterialApp.router(\n  routerConfig: router,\n  theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: seed), textTheme: text),\n  darkTheme: ThemeData(\n    colorScheme: ColorScheme.fromSeed(seedColor: seed, brightness: Brightness.dark), textTheme: text),\n);\n```\n\n- Material 3 is the default. Widgets read `Theme.of(context).colorScheme`\n  and `textTheme`, never literal colours or sizes; other tokens (success,\n  warning, brand surfaces) go in a `ThemeExtension`.\n- iOS feel where it matters: `Switch.adaptive`, `Slider.adaptive`,\n  `CircularProgressIndicator.adaptive`, `showAdaptiveDialog` with\n  `AlertDialog.adaptive`, `Icons.adaptive.share`. Branch on\n  `Theme.of(context).platform` (testable), not `Platform.isIOS`.\n\n## 5. Widgets\n\n- Small widget classes, not methods returning widgets: a class is its own\n  rebuild boundary and can be `const`. `const` wherever possible (the\n  `prefer_const_constructors` lint finds them).\n- `ValueKey(item.id)` on stateful items in lists that reorder or remove.\n- Narrow rebuilds: `ref.watch(provider.select((s) => s.count))`, a\n  `Consumer` around the part that changes, `MediaQuery.sizeOf(context)`\n  rather than `MediaQuery.of(context).size`.\n- Futures, streams and controllers are created in `initState` or a\n  provider and disposed, never in `build`. After an `await`,\n  `if (!context.mounted) return;` before using `context`.\n\n## 6. Lists\n\n- `ListView.builder`, with `itemExtent` or `prototypeItem` when rows share a\n  height (cheaper layout, accurate jumps). Slivers for complex scroll\n  views: `CustomScrollView`, `SliverAppBar`, `SliverList.builder`.\n- Request the next page when the builder reaches `items.length - 5`; a\n  footer for loading, error and the end. `RefreshIndicator.adaptive`.\n- Never a long `Column` in a `SingleChildScrollView`; `shrinkWrap: true` on\n  a long nested list builds every row.\n\n## 7. Async work\n\n- Timeouts and cancellation: Dio `BaseOptions(connectTimeout:\n  Duration(seconds: 10), receiveTimeout: Duration(seconds: 20))`, a\n  `CancelToken` cancelled in `ref.onDispose`, subscriptions cancelled in\n  `dispose`.\n- Heavy work off the UI isolate (16ms per frame at 60Hz, 8ms at 120Hz):\n  `await Isolate.run(() => parseOrders(body))` for large JSON, images or\n  crypto; `compute` in older code.\n- Uncaught errors reach crash reporting through `FlutterError.onError` and\n  `PlatformDispatcher.instance.onError` (`mobile-release`).\n\n## 8. Platform integration\n\nPigeon generates typed platform channels instead of string method names:\n\n```dart\n\u002F\u002F pigeons\u002Fdevice.dart, generated with: dart run pigeon --input pigeons\u002Fdevice.dart\n@ConfigurePigeon(PigeonOptions(dartOut: 'lib\u002Fcore\u002Fdevice.g.dart', swiftOut: 'ios\u002FRunner\u002FDevice.g.swift',\n    kotlinOut: 'android\u002Fapp\u002Fsrc\u002Fmain\u002Fkotlin\u002Fcom\u002Facme\u002Fapp\u002FDevice.g.kt'))\n@HostApi()\nabstract class DeviceApi {\n  @async\n  int freeBytes();\n}\n```\n\n- Plugins are federated: check the platforms a package supports first.\n- permission_handler: `final s = await Permission.camera.request();` then\n  proceed when `s.isGranted`, and offer `openAppSettings()` when\n  `s.isPermanentlyDenied`. On iOS each permission must also be enabled in\n  the `Podfile` (`PERMISSION_CAMERA=1` in `GCC_PREPROCESSOR_DEFINITIONS`);\n  without it the status reads as permanently denied.\n\n## 9. Choosing packages\n\n- Fewer packages. Before adding one: verified publisher, recent releases,\n  pub points, open issues, every platform you ship supported; the\n  flutter.dev and dart.dev publishers first.\n- Commit `pubspec.lock`; `flutter pub outdated`, then one major upgrade at\n  a time with tests between.\n\n## 10. Testing\n\n```dart\ntestWidgets('shows orders after a retry', (tester) async {\n  final repo = FakeOrderRepository(failFirst: true, orders: [sampleOrder]);\n  await tester.pumpWidget(ProviderScope(\n    overrides: [orderRepositoryProvider.overrideWith((ref) => repo)],\n    child: const MaterialApp(home: OrdersScreen()),\n  ));\n  await tester.pumpAndSettle();\n  await tester.tap(find.bySemanticsLabel('Retry'));\n  await tester.pumpAndSettle();\n  expect(find.text('Order 1042'), findsOneWidget);\n});\n```\n\n- Unit tests for notifiers, blocs and repositories, with fakes or mocktail\n  (`when(() => repo.fetchOrders()).thenAnswer((_) async => [sample])`);\n  widget tests find what a user finds, text and semantics labels first.\n- Goldens for key screens (`matchesGoldenFile('goldens\u002Forder_card.png')`,\n  refreshed with `flutter test --update-goldens`): real fonts loaded in\n  `test\u002Fflutter_test_config.dart`, generated on the same OS as CI.\n- Accessibility matchers: `meetsGuideline(androidTapTargetGuideline)`,\n  `iOSTapTargetGuideline`, `labeledTapTargetGuideline`,\n  `textContrastGuideline`.\n- `integration_test` for a few journeys on a device\n  (`IntegrationTestWidgetsFlutterBinding.ensureInitialized()`); Patrol when\n  a flow crosses system dialogs (`mobile-testing`).\n\n## 11. Performance\n\n- Profile mode on a real device (`flutter run --profile`), then DevTools:\n  frame chart (UI and raster time), rebuild counts, CPU and memory. Impeller\n  has mostly removed shader jank; slow frames are the app's own work.\n- Costly: `Opacity` on a changing subtree (use `FadeTransition` or\n  `AnimatedOpacity`), `ShaderMask`, `BackdropFilter`, anti-aliased clips on\n  large areas. `RepaintBoundary` around parts that repaint often.\n- Images decoded at the size shown: `Image.network(url, cacheWidth: 192)`,\n  or `memCacheWidth` with cached_network_image. Little work before\n  `runApp`; plugins initialised lazily.\n- Size: `flutter build appbundle --analyze-size`; release builds with\n  `--obfuscate --split-debug-info=build\u002Fsymbols`, symbols uploaded for crash\n  reports (`mobile-performance`).\n\n## 12. Flavors and environments\n\n```bash\nflutter run --flavor dev --dart-define-from-file=env\u002Fdev.json\nflutter build appbundle --flavor prod --dart-define-from-file=env\u002Fprod.json\nflutter build ipa --flavor prod --dart-define-from-file=env\u002Fprod.json\n```\n\n- Values read with `const String.fromEnvironment('API_URL')` are compiled\n  into the app: never secrets.\n- Android `productFlavors` with `applicationIdSuffix`; iOS a scheme per\n  flavor with matching configurations (`Debug-dev`, `Release-prod`).\n- `version: 1.4.0+42` in `pubspec.yaml` is the version name and build\n  number; CI overrides with `--build-name` and `--build-number`.\n\n## Check it\n\n- `dart format --output=none --set-exit-if-changed .`, `flutter analyze`,\n  `flutter test`; `dart run build_runner build -d` leaves no diff.\n- `flutter build apk --debug`, and on macOS `flutter build ios --no-codesign`.\n- The changed flow on a simulator or emulator; integration tests on a\n  device when the project has them.\n\n## Avoid\n\nBusiness logic and HTTP in widgets; futures created in `build`; `ref.read`\nin `build`; `setState` for shared state; literal colours; methods returning\nwidgets; missing `const`; lists built eagerly; heavy parsing on the UI\nisolate; performance judged in debug mode; secrets in `--dart-define`;\npackages added without checking platforms and upkeep; goldens generated on\na different OS from CI.\n","# Flutter apps\n\nThe naive Flutter app: one 900-line screen with `setState` and HTTP calls\nin its methods, a `FutureBuilder` whose future is created in `build` and\nfires on every rebuild, `Navigator.push` everywhere and no deep links,\n`Colors.blue` scattered through widgets, Material switches on an iPhone,\nforty packages chosen by name, and no tests. This skill is how a Flutter\nteam structures, builds, tests and ships an app in 2026. Conventions in\n`mobile-ux`, offline data in `mobile-data`, stores in `mobile-release`.\n\n## 1. Structure\n\nFeature first, with a small shared core; follow the project if it differs.\n\n```\nlib\u002Fmain.dart               runApp(const ProviderScope(child: App()))\nlib\u002Fapp\u002F                    router, theme, the App widget\nlib\u002Fcore\u002F                   HTTP client, errors, storage, logging\nlib\u002Ffeatures\u002Forders\u002Fdata\u002F          DTOs, API calls, repository, Drift tables\nlib\u002Ffeatures\u002Forders\u002Fdomain\u002F        entities and interfaces, only when they earn it\nlib\u002Ffeatures\u002Forders\u002Fapplication\u002F   notifiers or blocs\nlib\u002Ffeatures\u002Forders\u002Fpresentation\u002F  screens and widgets\ntest\u002F mirrors lib\u002F; integration_test\u002F holds device journeys\n```\n\nCode generation with build_runner (`dart run build_runner watch -d`):\nfreezed for immutable models and unions, json_serializable,\nriverpod_generator. Freezed 3 requires the `abstract` or `sealed` keyword:\n\n```dart\n@freezed\nabstract class Order with _$Order {\n  const factory Order({required String id, required int totalCents, required DateTime createdAt}) = _Order;\n  factory Order.fromJson(Map\u003CString, dynamic> json) => _$OrderFromJson(json);\n}\n```\n\n## 2. State\n\nRiverpod 3 with code generation by default; Bloc when the project uses it.\n\n```dart\n@riverpod\nclass Orders extends _$Orders {\n  @override\n  Future\u003CList\u003COrder>> build() => ref.watch(orderRepositoryProvider).fetchOrders();\n\n  Future\u003Cvoid> cancel(String id) async {\n    await ref.read(orderRepositoryProvider).cancel(id);\n    if (!ref.mounted) return;\n    ref.invalidateSelf();\n  }\n}\n\nclass OrdersScreen extends ConsumerWidget {\n  const OrdersScreen({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    return switch (ref.watch(ordersProvider)) {\n      AsyncData(:final value) when value.isEmpty => const EmptyOrders(),\n      AsyncData(:final value) => OrderList(orders: value),\n      AsyncError(:final error) => ErrorView(error, onRetry: () => ref.invalidate(ordersProvider)),\n      _ => const OrdersSkeleton(),\n    };\n  }\n}\n```\n\n- `ref.watch` in `build`, `ref.read` in callbacks, `ref.listen` for side\n  effects (a snackbar, navigation). A refreshing provider still matches\n  `AsyncData`, so data stays on screen during a refresh.\n- Riverpod 3 retries failing providers automatically; after an `await`, a\n  notifier checks `ref.mounted` before touching state.\n- Bloc: sealed events and states, bloc_concurrency transformers\n  (`droppable()` for submits, `restartable()` for search), `BlocSelector`.\n- `setState` only for state local to one widget; no HTTP in widgets.\n\n## 3. Navigation with go_router\n\n```dart\nGoRouter(\n  initialLocation: '\u002Forders',\n  refreshListenable: auth, \u002F\u002F a Listenable that fires on sign-in and sign-out\n  redirect: (context, state) {\n    final atSignIn = state.matchedLocation == '\u002Fsign-in';\n    if (!auth.signedIn) return atSignIn ? null : '\u002Fsign-in';\n    return atSignIn ? '\u002Forders' : null;\n  },\n  routes: [\n    GoRoute(path: '\u002Fsign-in', builder: (context, state) => const SignInScreen()),\n    StatefulShellRoute.indexedStack(\n      builder: (context, state, shell) => AppShell(shell: shell),\n      branches: [\n        StatefulShellBranch(routes: [\n          GoRoute(path: '\u002Forders', builder: (context, state) => const OrdersScreen(), routes: [\n            GoRoute(path: ':id', builder: (context, state) => OrderScreen(id: state.pathParameters['id']!)),\n          ]),\n        ]),\n        StatefulShellBranch(routes: [GoRoute(path: '\u002Fsettings', builder: (context, state) => const SettingsScreen())]),\n      ],\n    ),\n  ],\n);\n```\n\n- `StatefulShellRoute.indexedStack` keeps a stack per tab. `context.go`\n  sets the location (the stack follows the URL); `context.push` adds a\n  screen on top. Typed routes with go_router_builder in larger apps.\n- Auth lives in `redirect` with `refreshListenable`, not in every screen.\n  Deep links arrive as locations once App Links and Universal Links are\n  configured natively.\n\n## 4. Theming\n\n```dart\nconst seed = Color(0xFF0B6E4F);\nMaterialApp.router(\n  routerConfig: router,\n  theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: seed), textTheme: text),\n  darkTheme: ThemeData(\n    colorScheme: ColorScheme.fromSeed(seedColor: seed, brightness: Brightness.dark), textTheme: text),\n);\n```\n\n- Material 3 is the default. Widgets read `Theme.of(context).colorScheme`\n  and `textTheme`, never literal colours or sizes; other tokens (success,\n  warning, brand surfaces) go in a `ThemeExtension`.\n- iOS feel where it matters: `Switch.adaptive`, `Slider.adaptive`,\n  `CircularProgressIndicator.adaptive`, `showAdaptiveDialog` with\n  `AlertDialog.adaptive`, `Icons.adaptive.share`. Branch on\n  `Theme.of(context).platform` (testable), not `Platform.isIOS`.\n\n## 5. Widgets\n\n- Small widget classes, not methods returning widgets: a class is its own\n  rebuild boundary and can be `const`. `const` wherever possible (the\n  `prefer_const_constructors` lint finds them).\n- `ValueKey(item.id)` on stateful items in lists that reorder or remove.\n- Narrow rebuilds: `ref.watch(provider.select((s) => s.count))`, a\n  `Consumer` around the part that changes, `MediaQuery.sizeOf(context)`\n  rather than `MediaQuery.of(context).size`.\n- Futures, streams and controllers are created in `initState` or a\n  provider and disposed, never in `build`. After an `await`,\n  `if (!context.mounted) return;` before using `context`.\n\n## 6. Lists\n\n- `ListView.builder`, with `itemExtent` or `prototypeItem` when rows share a\n  height (cheaper layout, accurate jumps). Slivers for complex scroll\n  views: `CustomScrollView`, `SliverAppBar`, `SliverList.builder`.\n- Request the next page when the builder reaches `items.length - 5`; a\n  footer for loading, error and the end. `RefreshIndicator.adaptive`.\n- Never a long `Column` in a `SingleChildScrollView`; `shrinkWrap: true` on\n  a long nested list builds every row.\n\n## 7. Async work\n\n- Timeouts and cancellation: Dio `BaseOptions(connectTimeout:\n  Duration(seconds: 10), receiveTimeout: Duration(seconds: 20))`, a\n  `CancelToken` cancelled in `ref.onDispose`, subscriptions cancelled in\n  `dispose`.\n- Heavy work off the UI isolate (16ms per frame at 60Hz, 8ms at 120Hz):\n  `await Isolate.run(() => parseOrders(body))` for large JSON, images or\n  crypto; `compute` in older code.\n- Uncaught errors reach crash reporting through `FlutterError.onError` and\n  `PlatformDispatcher.instance.onError` (`mobile-release`).\n\n## 8. Platform integration\n\nPigeon generates typed platform channels instead of string method names:\n\n```dart\n\u002F\u002F pigeons\u002Fdevice.dart, generated with: dart run pigeon --input pigeons\u002Fdevice.dart\n@ConfigurePigeon(PigeonOptions(dartOut: 'lib\u002Fcore\u002Fdevice.g.dart', swiftOut: 'ios\u002FRunner\u002FDevice.g.swift',\n    kotlinOut: 'android\u002Fapp\u002Fsrc\u002Fmain\u002Fkotlin\u002Fcom\u002Facme\u002Fapp\u002FDevice.g.kt'))\n@HostApi()\nabstract class DeviceApi {\n  @async\n  int freeBytes();\n}\n```\n\n- Plugins are federated: check the platforms a package supports first.\n- permission_handler: `final s = await Permission.camera.request();` then\n  proceed when `s.isGranted`, and offer `openAppSettings()` when\n  `s.isPermanentlyDenied`. On iOS each permission must also be enabled in\n  the `Podfile` (`PERMISSION_CAMERA=1` in `GCC_PREPROCESSOR_DEFINITIONS`);\n  without it the status reads as permanently denied.\n\n## 9. Choosing packages\n\n- Fewer packages. Before adding one: verified publisher, recent releases,\n  pub points, open issues, every platform you ship supported; the\n  flutter.dev and dart.dev publishers first.\n- Commit `pubspec.lock`; `flutter pub outdated`, then one major upgrade at\n  a time with tests between.\n\n## 10. Testing\n\n```dart\ntestWidgets('shows orders after a retry', (tester) async {\n  final repo = FakeOrderRepository(failFirst: true, orders: [sampleOrder]);\n  await tester.pumpWidget(ProviderScope(\n    overrides: [orderRepositoryProvider.overrideWith((ref) => repo)],\n    child: const MaterialApp(home: OrdersScreen()),\n  ));\n  await tester.pumpAndSettle();\n  await tester.tap(find.bySemanticsLabel('Retry'));\n  await tester.pumpAndSettle();\n  expect(find.text('Order 1042'), findsOneWidget);\n});\n```\n\n- Unit tests for notifiers, blocs and repositories, with fakes or mocktail\n  (`when(() => repo.fetchOrders()).thenAnswer((_) async => [sample])`);\n  widget tests find what a user finds, text and semantics labels first.\n- Goldens for key screens (`matchesGoldenFile('goldens\u002Forder_card.png')`,\n  refreshed with `flutter test --update-goldens`): real fonts loaded in\n  `test\u002Fflutter_test_config.dart`, generated on the same OS as CI.\n- Accessibility matchers: `meetsGuideline(androidTapTargetGuideline)`,\n  `iOSTapTargetGuideline`, `labeledTapTargetGuideline`,\n  `textContrastGuideline`.\n- `integration_test` for a few journeys on a device\n  (`IntegrationTestWidgetsFlutterBinding.ensureInitialized()`); Patrol when\n  a flow crosses system dialogs (`mobile-testing`).\n\n## 11. Performance\n\n- Profile mode on a real device (`flutter run --profile`), then DevTools:\n  frame chart (UI and raster time), rebuild counts, CPU and memory. Impeller\n  has mostly removed shader jank; slow frames are the app's own work.\n- Costly: `Opacity` on a changing subtree (use `FadeTransition` or\n  `AnimatedOpacity`), `ShaderMask`, `BackdropFilter`, anti-aliased clips on\n  large areas. `RepaintBoundary` around parts that repaint often.\n- Images decoded at the size shown: `Image.network(url, cacheWidth: 192)`,\n  or `memCacheWidth` with cached_network_image. Little work before\n  `runApp`; plugins initialised lazily.\n- Size: `flutter build appbundle --analyze-size`; release builds with\n  `--obfuscate --split-debug-info=build\u002Fsymbols`, symbols uploaded for crash\n  reports (`mobile-performance`).\n\n## 12. Flavors and environments\n\n```bash\nflutter run --flavor dev --dart-define-from-file=env\u002Fdev.json\nflutter build appbundle --flavor prod --dart-define-from-file=env\u002Fprod.json\nflutter build ipa --flavor prod --dart-define-from-file=env\u002Fprod.json\n```\n\n- Values read with `const String.fromEnvironment('API_URL')` are compiled\n  into the app: never secrets.\n- Android `productFlavors` with `applicationIdSuffix`; iOS a scheme per\n  flavor with matching configurations (`Debug-dev`, `Release-prod`).\n- `version: 1.4.0+42` in `pubspec.yaml` is the version name and build\n  number; CI overrides with `--build-name` and `--build-number`.\n\n## Check it\n\n- `dart format --output=none --set-exit-if-changed .`, `flutter analyze`,\n  `flutter test`; `dart run build_runner build -d` leaves no diff.\n- `flutter build apk --debug`, and on macOS `flutter build ios --no-codesign`.\n- The changed flow on a simulator or emulator; integration tests on a\n  device when the project has them.\n\n## Avoid\n\nBusiness logic and HTTP in widgets; futures created in `build`; `ref.read`\nin `build`; `setState` for shared state; literal colours; methods returning\nwidgets; missing `const`; lists built eagerly; heavy parsing on the UI\nisolate; performance judged in debug mode; secrets in `--dart-define`;\npackages added without checking platforms and upkeep; goldens generated on\na different OS from CI.\n",{"name":5,"description":7},"main","38293fe1b096e6bf3646c7344c38786ed688f464","crates\u002Fenowx-core\u002Fskills\u002Fmobile-flutter",21,"https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli","https:\u002F\u002Fenowx.ai","https:\u002F\u002Fgithub.com\u002Fenowdev","Pretending to be a Developer","2026-10-06 02:05:35",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add enowdev\u002Fenowxcli --skill mobile-flutter",{"tool":64,"command":65},"bunx","bunx skills add enowdev\u002Fenowxcli --skill mobile-flutter",{"tool":67,"command":68},"pnpm","pnpm dlx skills add enowdev\u002Fenowxcli --skill mobile-flutter","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fmobile-flutter\u002FSKILL.md",true,[],[73,86,111,127,153,174],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":82,"publishedAt":44},"01M6D2HQYSQD9N7MTNDT4A7CBM","demo-skill","Project-scope demo skill for tests.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fworkspace\u002F.agents\u002Fskills\u002Fdemo-skill\u002FSKILL.md",[79],{"categoryNo":80,"categoryName":81,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[83,84,85,33,37],"demo skill","demo","tests",{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":100,"publishedAt":44},"01M6D2HM0GCPM0TM2HPG9HHR2R","writing","Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fwriting\u002FSKILL.md",[92,95],{"categoryNo":93,"categoryName":94,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":96,"categoryName":97,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[88,101,102,103,104,105,37,106,107,108,109,110],"text","emails","empty","drop","document","page","voice","notifications","documentation","translate",{"skillNo":112,"name":113,"nameZh":6,"description":114,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":115,"categories":116,"keywords":120,"publishedAt":44},"01M6D2HG07YS49NM196Q0R3AZ8","ui","Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui\u002FSKILL.md",[117],{"categoryNo":118,"categoryName":119,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000032","设计师",[113,121,122,123,124,106,125,37,126],"icons","components","SET","responsive","layout","before building",{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":139,"publishedAt":44},"01M6D2HB59HZHWG1RYZDV4SJ91","ui-themes","Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-themes\u002FSKILL.md",[133,136],{"categoryNo":134,"categoryName":135,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":137,"categoryName":138,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000030","开发工程师",[113,140,141,142,32,143,144,145,146,37,126,147,106,148,149,150,121,151,152],"ui themes","switching","photos","drawing","focus","maps","Art","flash","code","Charts","content","screenshots","logo",{"skillNo":154,"name":155,"nameZh":6,"description":156,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":157,"categories":158,"keywords":162,"publishedAt":44},"01M6D2H5R5KRAE524C8QX0T7Q0","ui-structure","Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-structure\u002FSKILL.md",[159,160,161],{"categoryNo":93,"categoryName":94,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":137,"categoryName":138,"taxonomyType":98,"taxonomyTypeDesc":99},{"categoryNo":96,"categoryName":97,"taxonomyType":98,"taxonomyTypeDesc":99},[34,113,163,122,164,165,148,166,167,168,125,32,169,170,171,37,126,106,172,173],"ui structure","pricing","dashboard","Blueprint","article","anatomy","styling","primitives","list","why","how",{"skillNo":175,"name":176,"nameZh":6,"description":177,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":178,"categories":179,"keywords":185,"publishedAt":44},"01M6D2H0ZZBJG77MBGPZHA6MWD","ui-stack-vue","Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-vue\u002FSKILL.md",[180,181,184],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":182,"categoryName":183,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":118,"categoryName":119,"taxonomyType":98,"taxonomyTypeDesc":99},[113,186,187,188,189,190,191,192,125,33,193,194,37,195,196,197,198,199,200,35],"vue","stack","ui stack vue","forms","pages","watch","runtime","transitions","fetch","pinia","nuxt","template","server","config","directory"]