mobile-flutter
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.
- License 许可
- Apache-2.0
- 所属仓库
- enowdev/enowxcli
- 最近更新
- 2026-10-06
- Stars
- ★ 67
- Forks
- 21
01 前言 Front Matter
- name
- mobile-flutter
- description
- 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.
03 SKILL.md 正文
Flutter apps
The naive Flutter app: one 900-line screen with setState and HTTP calls
in its methods, a FutureBuilder whose future is created in build and
fires on every rebuild, Navigator.push everywhere and no deep links,
Colors.blue scattered through widgets, Material switches on an iPhone,
forty packages chosen by name, and no tests. This skill is how a Flutter
team structures, builds, tests and ships an app in 2026. Conventions in
mobile-ux, offline data in mobile-data, stores in mobile-release.
1. Structure
Feature first, with a small shared core; follow the project if it differs.
lib/main.dart runApp(const ProviderScope(child: App()))
lib/app/ router, theme, the App widget
lib/core/ HTTP client, errors, storage, logging
lib/features/orders/data/ DTOs, API calls, repository, Drift tables
lib/features/orders/domain/ entities and interfaces, only when they earn it
lib/features/orders/application/ notifiers or blocs
lib/features/orders/presentation/ screens and widgets
test/ mirrors lib/; integration_test/ holds device journeys
Code generation with build_runner (dart run build_runner watch -d):
freezed for immutable models and unions, json_serializable,
riverpod_generator. Freezed 3 requires the abstract or sealed keyword:
@freezed
abstract class Order with _$Order {
const factory Order({required String id, required int totalCents, required DateTime createdAt}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
}
2. State
Riverpod 3 with code generation by default; Bloc when the project uses it.
@riverpod
class Orders extends _$Orders {
@override
Future<List<Order>> build() => ref.watch(orderRepositoryProvider).fetchOrders();
Future<void> cancel(String id) async {
await ref.read(orderRepositoryProvider).cancel(id);
if (!ref.mounted) return;
ref.invalidateSelf();
}
}
class OrdersScreen extends ConsumerWidget {
const OrdersScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return switch (ref.watch(ordersProvider)) {
AsyncData(:final value) when value.isEmpty => const EmptyOrders(),
AsyncData(:final value) => OrderList(orders: value),
AsyncError(:final error) => ErrorView(error, onRetry: () => ref.invalidate(ordersProvider)),
_ => const OrdersSkeleton(),
};
}
}
ref.watchinbuild,ref.readin callbacks,ref.listenfor side effects (a snackbar, navigation). A refreshing provider still matchesAsyncData, so data stays on screen during a refresh.- Riverpod 3 retries failing providers automatically; after an
await, a notifier checksref.mountedbefore touching state. - Bloc: sealed events and states, bloc_concurrency transformers
(
droppable()for submits,restartable()for search),BlocSelector. setStateonly for state local to one widget; no HTTP in widgets.
3. Navigation with go_router
GoRouter(
initialLocation: '/orders',
refreshListenable: auth, // a Listenable that fires on sign-in and sign-out
redirect: (context, state) {
final atSignIn = state.matchedLocation == '/sign-in';
if (!auth.signedIn) return atSignIn ? null : '/sign-in';
return atSignIn ? '/orders' : null;
},
routes: [
GoRoute(path: '/sign-in', builder: (context, state) => const SignInScreen()),
StatefulShellRoute.indexedStack(
builder: (context, state, shell) => AppShell(shell: shell),
branches: [
StatefulShellBranch(routes: [
GoRoute(path: '/orders', builder: (context, state) => const OrdersScreen(), routes: [
GoRoute(path: ':id', builder: (context, state) => OrderScreen(id: state.pathParameters['id']!)),
]),
]),
StatefulShellBranch(routes: [GoRoute(path: '/settings', builder: (context, state) => const SettingsScreen())]),
],
),
],
);
StatefulShellRoute.indexedStackkeeps a stack per tab.context.gosets the location (the stack follows the URL);context.pushadds a screen on top. Typed routes with go_router_builder in larger apps.- Auth lives in
redirectwithrefreshListenable, not in every screen. Deep links arrive as locations once App Links and Universal Links are configured natively.
4. Theming
const seed = Color(0xFF0B6E4F);
MaterialApp.router(
routerConfig: router,
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: seed), textTheme: text),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: seed, brightness: Brightness.dark), textTheme: text),
);
- Material 3 is the default. Widgets read
Theme.of(context).colorSchemeandtextTheme, never literal colours or sizes; other tokens (success, warning, brand surfaces) go in aThemeExtension. - iOS feel where it matters:
Switch.adaptive,Slider.adaptive,CircularProgressIndicator.adaptive,showAdaptiveDialogwithAlertDialog.adaptive,Icons.adaptive.share. Branch onTheme.of(context).platform(testable), notPlatform.isIOS.
5. Widgets
- Small widget classes, not methods returning widgets: a class is its own
rebuild boundary and can be
const.constwherever possible (theprefer_const_constructorslint finds them). ValueKey(item.id)on stateful items in lists that reorder or remove.- Narrow rebuilds:
ref.watch(provider.select((s) => s.count)), aConsumeraround the part that changes,MediaQuery.sizeOf(context)rather thanMediaQuery.of(context).size. - Futures, streams and controllers are created in
initStateor a provider and disposed, never inbuild. After anawait,if (!context.mounted) return;before usingcontext.
6. Lists
ListView.builder, withitemExtentorprototypeItemwhen rows share a height (cheaper layout, accurate jumps). Slivers for complex scroll views:CustomScrollView,SliverAppBar,SliverList.builder.- Request the next page when the builder reaches
items.length - 5; a footer for loading, error and the end.RefreshIndicator.adaptive. - Never a long
Columnin aSingleChildScrollView;shrinkWrap: trueon a long nested list builds every row.
7. Async work
- Timeouts and cancellation: Dio
BaseOptions(connectTimeout: Duration(seconds: 10), receiveTimeout: Duration(seconds: 20)), aCancelTokencancelled inref.onDispose, subscriptions cancelled indispose. - Heavy work off the UI isolate (16ms per frame at 60Hz, 8ms at 120Hz):
await Isolate.run(() => parseOrders(body))for large JSON, images or crypto;computein older code. - Uncaught errors reach crash reporting through
FlutterError.onErrorandPlatformDispatcher.instance.onError(mobile-release).
8. Platform integration
Pigeon generates typed platform channels instead of string method names:
// pigeons/device.dart, generated with: dart run pigeon --input pigeons/device.dart
@ConfigurePigeon(PigeonOptions(dartOut: 'lib/core/device.g.dart', swiftOut: 'ios/Runner/Device.g.swift',
kotlinOut: 'android/app/src/main/kotlin/com/acme/app/Device.g.kt'))
@HostApi()
abstract class DeviceApi {
@async
int freeBytes();
}
- Plugins are federated: check the platforms a package supports first.
- permission_handler:
final s = await Permission.camera.request();then proceed whens.isGranted, and offeropenAppSettings()whens.isPermanentlyDenied. On iOS each permission must also be enabled in thePodfile(PERMISSION_CAMERA=1inGCC_PREPROCESSOR_DEFINITIONS); without it the status reads as permanently denied.
9. Choosing packages
- Fewer packages. Before adding one: verified publisher, recent releases, pub points, open issues, every platform you ship supported; the flutter.dev and dart.dev publishers first.
- Commit
pubspec.lock;flutter pub outdated, then one major upgrade at a time with tests between.
10. Testing
testWidgets('shows orders after a retry', (tester) async {
final repo = FakeOrderRepository(failFirst: true, orders: [sampleOrder]);
await tester.pumpWidget(ProviderScope(
overrides: [orderRepositoryProvider.overrideWith((ref) => repo)],
child: const MaterialApp(home: OrdersScreen()),
));
await tester.pumpAndSettle();
await tester.tap(find.bySemanticsLabel('Retry'));
await tester.pumpAndSettle();
expect(find.text('Order 1042'), findsOneWidget);
});
- Unit tests for notifiers, blocs and repositories, with fakes or mocktail
(
when(() => repo.fetchOrders()).thenAnswer((_) async => [sample])); widget tests find what a user finds, text and semantics labels first. - Goldens for key screens (
matchesGoldenFile('goldens/order_card.png'), refreshed withflutter test --update-goldens): real fonts loaded intest/flutter_test_config.dart, generated on the same OS as CI. - Accessibility matchers:
meetsGuideline(androidTapTargetGuideline),iOSTapTargetGuideline,labeledTapTargetGuideline,textContrastGuideline. integration_testfor a few journeys on a device (IntegrationTestWidgetsFlutterBinding.ensureInitialized()); Patrol when a flow crosses system dialogs (mobile-testing).
11. Performance
- Profile mode on a real device (
flutter run --profile), then DevTools: frame chart (UI and raster time), rebuild counts, CPU and memory. Impeller has mostly removed shader jank; slow frames are the app's own work. - Costly:
Opacityon a changing subtree (useFadeTransitionorAnimatedOpacity),ShaderMask,BackdropFilter, anti-aliased clips on large areas.RepaintBoundaryaround parts that repaint often. - Images decoded at the size shown:
Image.network(url, cacheWidth: 192), ormemCacheWidthwith cached_network_image. Little work beforerunApp; plugins initialised lazily. - Size:
flutter build appbundle --analyze-size; release builds with--obfuscate --split-debug-info=build/symbols, symbols uploaded for crash reports (mobile-performance).
12. Flavors and environments
flutter run --flavor dev --dart-define-from-file=env/dev.json
flutter build appbundle --flavor prod --dart-define-from-file=env/prod.json
flutter build ipa --flavor prod --dart-define-from-file=env/prod.json
- Values read with
const String.fromEnvironment('API_URL')are compiled into the app: never secrets. - Android
productFlavorswithapplicationIdSuffix; iOS a scheme per flavor with matching configurations (Debug-dev,Release-prod). version: 1.4.0+42inpubspec.yamlis the version name and build number; CI overrides with--build-nameand--build-number.
Check it
dart format --output=none --set-exit-if-changed .,flutter analyze,flutter test;dart run build_runner build -dleaves no diff.flutter build apk --debug, and on macOSflutter build ios --no-codesign.- The changed flow on a simulator or emulator; integration tests on a device when the project has them.
Avoid
Business logic and HTTP in widgets; futures created in build; ref.read
in build; setState for shared state; literal colours; methods returning
widgets; missing const; lists built eagerly; heavy parsing on the UI
isolate; performance judged in debug mode; secrets in --dart-define;
packages added without checking platforms and upkeep; goldens generated on
a different OS from CI.
04 同仓库更多 Skills
demo-skill
Project-scope demo skill for tests.
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.
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.
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.
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.
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.