Source profileQuality 86/100

affaan-m/ECC/docs/zh-CN/skills/dart-flutter-patterns/SKILL.md

dart-flutter-patterns

Review dart-flutter-patterns's use cases, installation, workflow, and original source instructions.

Source repository stars
234,327
Declared platforms
0
Static risk flags
1
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/dart-flutter-patterns"
    Safe inspection promptEditorial

    Inspect the Agent Skill "dart-flutter-patterns" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/zh-CN/skills/dart-flutter-patterns/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      使用场景

      开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导 搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择 实现安全的 HTTP 客户端、WebView 集成或本地存储 为 Flutter 组件、Cubit 或 Riverpod 提供者编写测试 使用认证守卫配置 GoRouter

      开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择
    2. 02

      工作原理

      此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式:

      空安全 — 避免 !,优先使用 ?./??/模式匹配不可变状态 — 密封类、freezed、copyWith异步组合 — 并发 Future.wait、BuildContext 后安全使用 await
    3. 03

      示例

      适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。

      适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。
    4. 04

      1. 空安全基础

      Review the “1. 空安全基础” section in the pinned source before continuing.

      Review and apply the “1. 空安全基础” source section.
    5. 05

      优先使用模式而非感叹号操作符

      Review the “优先使用模式而非感叹号操作符” section in the pinned source before continuing.

      Review and apply the “优先使用模式而非感叹号操作符” source section.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 467

    The documentation includes network, browsing, or remote request actions.

    return handler.resolve(await dio.fetch(error.requestOptions));

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score86/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    docs/zh-CN/skills/dart-flutter-patterns/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    Dart/Flutter 模式

    使用场景

    在以下情况使用此技能:

    • 开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式
    • 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导
    • 搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择
    • 实现安全的 HTTP 客户端、WebView 集成或本地存储
    • 为 Flutter 组件、Cubit 或 Riverpod 提供者编写测试
    • 使用认证守卫配置 GoRouter

    工作原理

    此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式:

    1. 空安全 — 避免 !,优先使用 ?./??/模式匹配
    2. 不可变状态 — 密封类、freezedcopyWith
    3. 异步组合 — 并发 Future.waitBuildContext 后安全使用 await
    4. 组件架构 — 提取为类(而非方法)、const 传播、作用域重建
    5. 状态管理 — BLoC/Cubit 事件、Riverpod 通知器和派生提供者
    6. 导航 — 通过 refreshListenable 实现带响应式认证守卫的 GoRouter
    7. 网络请求 — 带拦截器的 Dio、带一次性重试守卫的令牌刷新
    8. 错误处理 — 全局捕获、ErrorWidget.builder、Crashlytics 集成
    9. 测试 — 单元测试(BLoC 测试)、组件测试(ProviderScope 覆盖)、使用假对象而非模拟对象

    示例

    // Sealed state — prevents impossible states
    sealed class AsyncState<T> {}
    final class Loading<T> extends AsyncState<T> {}
    final class Success<T> extends AsyncState<T> { final T data; const Success(this.data); }
    final class Failure<T> extends AsyncState<T> { final Object error; const Failure(this.error); }
    
    // GoRouter with reactive auth redirect
    final router = GoRouter(
      refreshListenable: GoRouterRefreshStream(authCubit.stream),
      redirect: (context, state) {
        final authed = context.read<AuthCubit>().state is AuthAuthenticated;
        if (!authed && !state.matchedLocation.startsWith('/login')) return '/login';
        return null;
      },
      routes: [...],
    );
    
    // Riverpod derived provider with safe firstWhereOrNull
    @riverpod
    double cartTotal(Ref ref) {
      final cart = ref.watch(cartNotifierProvider);
      final products = ref.watch(productsProvider).valueOrNull ?? [];
      return cart.fold(0.0, (total, item) {
        final product = products.firstWhereOrNull((p) => p.id == item.productId);
        return total + (product?.price ?? 0) * item.quantity;
      });
    }
    

    适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。


    1. 空安全基础

    优先使用模式而非感叹号操作符

    // BAD — crashes at runtime if null
    final name = user!.name;
    
    // GOOD — provide fallback
    final name = user?.name ?? 'Unknown';
    
    // GOOD — Dart 3 pattern matching (preferred for complex cases)
    final display = switch (user) {
      User(:final name, :final email) => '$name <$email>',
      null => 'Guest',
    };
    
    // GOOD — guard early return
    String getUserName(User? user) {
      if (user == null) return 'Unknown';
      return user.name; // promoted to non-null after check
    }
    

    避免过度使用 late

    // BAD — defers null error to runtime
    late String userId;
    
    // GOOD — nullable with explicit initialization
    String? userId;
    
    // OK — use late only when initialization is guaranteed before first access
    // (e.g., in initState() before any widget interaction)
    late final AnimationController _controller;
    
    @override
    void initState() {
      super.initState();
      _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
    }
    

    2. 不可变状态

    状态层次结构的密封类

    sealed class UserState {}
    
    final class UserInitial extends UserState {}
    
    final class UserLoading extends UserState {}
    
    final class UserLoaded extends UserState {
      const UserLoaded(this.user);
      final User user;
    }
    
    final class UserError extends UserState {
      const UserError(this.message);
      final String message;
    }
    
    // Exhaustive switch — compiler enforces all branches
    Widget buildFrom(UserState state) => switch (state) {
      UserInitial() => const SizedBox.shrink(),
      UserLoading() => const CircularProgressIndicator(),
      UserLoaded(:final user) => UserCard(user: user),
      UserError(:final message) => ErrorText(message),
    };
    

    使用 Freezed 实现无模板代码的不可变性

    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'user.freezed.dart';
    part 'user.g.dart';
    
    @freezed
    class User with _$User {
      const factory User({
        required String id,
        required String name,
        required String email,
        @Default(false) bool isAdmin,
      }) = _User;
    
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    }
    
    // Usage
    final user = User(id: '1', name: 'Alice', email: 'alice@example.com');
    final updated = user.copyWith(name: 'Alice Smith'); // immutable update
    final json = user.toJson();
    final fromJson = User.fromJson(json);
    

    3. 异步组合

    使用 Future.wait 的结构化并发

    Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
      // Run concurrently — don't await sequentially
      final (userList, orderList) = await (
        users.getAll(),
        orders.getRecent(),
      ).wait; // Dart 3 record destructuring + Future.wait extension
    
      return DashboardData(users: userList, orders: orderList);
    }
    

    流模式

    // Repository exposes reactive streams for live data
    Stream<List<Item>> watchCartItems() => _db
        .watchTable('cart_items')
        .map((rows) => rows.map(Item.fromRow).toList());
    
    // In widget layer — declarative, no manual subscription
    StreamBuilder<List<Item>>(
      stream: cartRepository.watchCartItems(),
      builder: (context, snapshot) => switch (snapshot) {
        AsyncSnapshot(connectionState: ConnectionState.waiting) =>
            const CircularProgressIndicator(),
        AsyncSnapshot(:final error?) => ErrorWidget(error.toString()),
        AsyncSnapshot(:final data?) => CartList(items: data),
        _ => const SizedBox.shrink(),
      },
    )
    

    Await 后的 BuildContext

    // CRITICAL — always check mounted after any await in StatefulWidget
    Future<void> _handleSubmit() async {
      setState(() => _isLoading = true);
      try {
        await authService.login(_email, _password);
        if (!mounted) return; // ← guard before using context
        context.go('/home');
      } on AuthException catch (e) {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
      } finally {
        if (mounted) setState(() => _isLoading = false);
      }
    }
    

    4. 组件架构

    提取为类,而非方法

    // BAD — private method returning widget, prevents optimization
    Widget _buildHeader() {
      return Container(
        padding: const EdgeInsets.all(16),
        child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
      );
    }
    
    // GOOD — separate widget class, enables const, element reuse
    class _PageHeader extends StatelessWidget {
      const _PageHeader(this.title);
      final String title;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: const EdgeInsets.all(16),
          child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
        );
      }
    }
    

    const 传播

    // BAD — new instances every rebuild
    child: Padding(
      padding: EdgeInsets.all(16.0),       // not const
      child: Icon(Icons.home, size: 24.0), // not const
    )
    
    // GOOD — const stops rebuild propagation
    child: const Padding(
      padding: EdgeInsets.all(16.0),
      child: Icon(Icons.home, size: 24.0),
    )
    

    作用域重建

    // BAD — entire page rebuilds on every counter change
    class CounterPage extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(counterProvider); // rebuilds everything
        return Scaffold(
          body: Column(children: [
            const ExpensiveHeader(), // unnecessarily rebuilt
            Text('$count'),
            const ExpensiveFooter(), // unnecessarily rebuilt
          ]),
        );
      }
    }
    
    // GOOD — isolate the rebuilding part
    class CounterPage extends StatelessWidget {
      const CounterPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Scaffold(
          body: Column(children: [
            ExpensiveHeader(),        // never rebuilt (const)
            _CounterDisplay(),        // only this rebuilds
            ExpensiveFooter(),        // never rebuilt (const)
          ]),
        );
      }
    }
    
    class _CounterDisplay extends ConsumerWidget {
      const _CounterDisplay();
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(counterProvider);
        return Text('$count');
      }
    }
    

    5. 状态管理:BLoC/Cubit

    // Cubit — synchronous or simple async state
    class AuthCubit extends Cubit<AuthState> {
      AuthCubit(this._authService) : super(const AuthState.initial());
      final AuthService _authService;
    
      Future<void> login(String email, String password) async {
        emit(const AuthState.loading());
        try {
          final user = await _authService.login(email, password);
          emit(AuthState.authenticated(user));
        } on AuthException catch (e) {
          emit(AuthState.error(e.message));
        }
      }
    
      void logout() {
        _authService.logout();
        emit(const AuthState.initial());
      }
    }
    
    // In widget
    BlocBuilder<AuthCubit, AuthState>(
      builder: (context, state) => switch (state) {
        AuthInitial() => const LoginForm(),
        AuthLoading() => const CircularProgressIndicator(),
        AuthAuthenticated(:final user) => HomePage(user: user),
        AuthError(:final message) => ErrorView(message: message),
      },
    )
    

    6. 状态管理:Riverpod

    // Auto-dispose async provider
    @riverpod
    Future<List<Product>> products(Ref ref) async {
      final repo = ref.watch(productRepositoryProvider);
      return repo.getAll();
    }
    
    // Notifier with complex mutations
    @riverpod
    class CartNotifier extends _$CartNotifier {
      @override
      List<CartItem> build() => [];
    
      void add(Product product) {
        final existing = state.where((i) => i.productId == product.id).firstOrNull;
        if (existing != null) {
          state = [
            for (final item in state)
              if (item.productId == product.id) item.copyWith(quantity: item.quantity + 1)
              else item,
          ];
        } else {
          state = [...state, CartItem(productId: product.id, quantity: 1)];
        }
      }
    
      void remove(String productId) =>
          state = state.where((i) => i.productId != productId).toList();
    
      void clear() => state = [];
    }
    
    // Derived provider (selector pattern)
    @riverpod
    int cartCount(Ref ref) => ref.watch(cartNotifierProvider).length;
    
    @riverpod
    double cartTotal(Ref ref) {
      final cart = ref.watch(cartNotifierProvider);
      final products = ref.watch(productsProvider).valueOrNull ?? [];
      return cart.fold(0.0, (total, item) {
        // firstWhereOrNull (from collection package) avoids StateError when product is missing
        final product = products.firstWhereOrNull((p) => p.id == item.productId);
        return total + (product?.price ?? 0) * item.quantity;
      });
    }
    

    7. 使用 GoRouter 的导航

    final router = GoRouter(
      initialLocation: '/',
      // refreshListenable re-evaluates redirect whenever auth state changes
      refreshListenable: GoRouterRefreshStream(authCubit.stream),
      redirect: (context, state) {
        final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
        final isGoingToLogin = state.matchedLocation == '/login';
        if (!isLoggedIn && !isGoingToLogin) return '/login';
        if (isLoggedIn && isGoingToLogin) return '/';
        return null;
      },
      routes: [
        GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
        ShellRoute(
          builder: (context, state, child) => AppShell(child: child),
          routes: [
            GoRoute(path: '/', builder: (_, __) => const HomePage()),
            GoRoute(
              path: '/products/:id',
              builder: (context, state) =>
                  ProductDetailPage(id: state.pathParameters['id']!),
            ),
          ],
        ),
      ],
    );
    

    8. 使用 Dio 的 HTTP 请求

    final dio = Dio(BaseOptions(
      baseUrl: const String.fromEnvironment('API_URL'),
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 30),
      headers: {'Content-Type': 'application/json'},
    ));
    
    // Add auth interceptor
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await secureStorage.read(key: 'auth_token');
        if (token != null) options.headers['Authorization'] = 'Bearer $token';
        handler.next(options);
      },
      onError: (error, handler) async {
        // Guard against infinite retry loops: only attempt refresh once per request
        final isRetry = error.requestOptions.extra['_isRetry'] == true;
        if (!isRetry && error.response?.statusCode == 401) {
          final refreshed = await attemptTokenRefresh();
          if (refreshed) {
            error.requestOptions.extra['_isRetry'] = true;
            return handler.resolve(await dio.fetch(error.requestOptions));
          }
        }
        handler.next(error);
      },
    ));
    
    // Repository using Dio
    class UserApiDataSource {
      const UserApiDataSource(this._dio);
      final Dio _dio;
    
      Future<User> getById(String id) async {
        final response = await _dio.get<Map<String, dynamic>>('/users/$id');
        return User.fromJson(response.data!);
      }
    }
    

    9. 错误处理架构

    // Global error capture — set up in main()
    void main() {
      FlutterError.onError = (details) {
        FlutterError.presentError(details);
        crashlytics.recordFlutterFatalError(details);
      };
    
      PlatformDispatcher.instance.onError = (error, stack) {
        crashlytics.recordError(error, stack, fatal: true);
        return true;
      };
    
      runApp(const App());
    }
    
    // Custom ErrorWidget for production
    class App extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        ErrorWidget.builder = (details) => ProductionErrorWidget(details);
        return MaterialApp.router(routerConfig: router);
      }
    }
    

    10. 测试快速参考

    // Unit test — use case
    test('GetUserUseCase returns null for missing user', () async {
      final repo = FakeUserRepository();
      final useCase = GetUserUseCase(repo);
      expect(await useCase('missing-id'), isNull);
    });
    
    // BLoC test
    blocTest<AuthCubit, AuthState>(
      'emits loading then error on failed login',
      build: () => AuthCubit(FakeAuthService(throwsOn: 'login')),
      act: (cubit) => cubit.login('user@test.com', 'wrong'),
      expect: () => [const AuthState.loading(), isA<AuthError>()],
    );
    
    // Widget test
    testWidgets('CartBadge shows item count', (tester) async {
      await tester.pumpWidget(
        ProviderScope(
          overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier(count: 3))],
          child: const MaterialApp(home: CartBadge()),
        ),
      );
      expect(find.text('3'), findsOneWidget);
    });
    

    参考

    Alternatives

    Compare before choosing