Communitygithub.com

padrões-dart-flutter

Padrões de produção para Dart e Flutter, cobrindo null safety, estado imutável, composição assíncrona, arquitetura de widgets, gerenciamento de estado popular (BLoC, Riverpod, Provider), navegação GoRouter, rede Dio, geração de código Freezed e arquitetura limpa. Use ao escrever ou revisar código Dart e Flutter — estado, widgets, navegação, rede ou arquitetura.

O que é padrões-dart-flutter?

Este skill fornece padrões de código Dart e Flutter prontos para produção, organizados por preocupações como null safety, estado imutável, composição assíncrona, arquitetura de widgets e gerenciamento de estado (BLoC/Cubit, Riverpod, Provider). Ele também cobre navegação com GoRouter, rede com Dio, tratamento de erros, geração de código Freezed e boas práticas de teste. O objetivo é ajudar desenvolvedores a escrever código idiomático, seguro e de fácil manutenção, evitando armadilhas comuns como o operador bang (`!`), uso excessivo de `late`, métodos que retornam widgets em vez de classes separadas, e falta de verificação de `mounted` após `await`. O skill recomenda o uso de classes seladas para estados, `freezed` para imutabilidade, `Future.wait` para concorrência, `refreshListenable` para redirecionamento reativo no GoRouter, e interceptor de token com proteção contra retry infinito no Dio. Ele é agnóstico em relação a bibliotecas específicas sempre que possível, mas fornece exemplos explícitos para os pacotes mais comuns do ecossistema Flutter.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/dart-flutter-patterns

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões Dart/Flutter

Quando Usar

Use este skill quando:

  • Iniciar um novo recurso Flutter e precisar de padrões idiomáticos para gerenciamento de estado, navegação ou acesso a dados
  • Revisar ou escrever código Dart e precisar de orientação sobre null safety, tipos selados ou composição assíncrona
  • Configurar um novo projeto Flutter e escolher entre BLoC, Riverpod ou Provider
  • Implementar clientes HTTP seguros, integração WebView ou armazenamento local
  • Escrever testes para widgets Flutter, Cubits ou providers Riverpod
  • Configurar GoRouter com guards de autenticação

Como Funciona

Este skill fornece padrões de código Dart/Flutter prontos para copiar e colar, organizados por preocupação:

  1. Null safety — evite !, prefira ?./??/pattern matching
  2. Estado imutável — classes seladas, freezed, copyWith
  3. Composição assíncrona — Future.wait concorrente, BuildContext seguro após await
  4. Arquitetura de widgets — extrair para classes (não métodos), propagação const, reconstruções com escopo
  5. Gerenciamento de estado — eventos BLoC/Cubit, notificadores Riverpod e providers derivados
  6. Navegação — GoRouter com guards de autenticação reativos via refreshListenable
  7. Rede — Dio com interceptores, renovação de token com proteção de retry único
  8. Tratamento de erros — captura global, ErrorWidget.builder, integração com crashlytics
  9. Testes — unitário (teste BLoC), widget (sobrescrita ProviderScope), fakes em vez de mocks

Exemplos

// Estado selado — previne estados impossíveis
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 com redirecionamento reativo
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: [...],
);

// Provider derivado Riverpod com firstWhereOrNull seguro
@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;
  });
}

Padrões práticos e prontos para produção para aplicações Dart e Flutter. Agnóstico em relação a bibliotecas sempre que possível, com cobertura explícita dos pacotes mais comuns do ecossistema.


1. Fundamentos de Null Safety

Prefira Padrões ao Operador Bang

// RUIM — falha em tempo de execução se for nulo
final name = user!.name;

// BOM — forneça fallback
final name = user?.name ?? 'Unknown';

// BOM — pattern matching do Dart 3 (preferido para casos complexos)
final display = switch (user) {
  User(:final name, :final email) => '$name <$email>',
  null => 'Guest',
};

// BOM — guard com retorno antecipado
String getUserName(User? user) {
  if (user == null) return 'Unknown';
  return user.name; // promovido para não nulo após a verificação
}

Evite Uso Excessivo de late

// RUIM — adia erro nulo para o tempo de execução
late String userId;

// BOM — anulável com inicialização explícita
String? userId;

// OK — use late apenas quando a inicialização for garantida antes do primeiro acesso
// (ex.: em initState() antes de qualquer interação com widget)
late final AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
}

2. Estado Imutável

Classes Seladas para Hierarquias de Estado

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;
}

// Switch exaustivo — o compilador garante todos os ramos
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 para Imutabilidade sem Boilerplate

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);
}

// Uso
final user = User(id: '1', name: 'Alice', email: '[email protected]');
final updated = user.copyWith(name: 'Alice Smith'); // atualização imutável
final json = user.toJson();
final fromJson = User.fromJson(json);

3. Composição Assíncrona

Concorrência Estruturada com Future.wait

Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
  // Execute concorrentemente — não aguarde sequencialmente
  final (userList, orderList) = await (
    users.getAll(),
    orders.getRecent(),
  ).wait; // Desestruturação de registro Dart 3 + extensão Future.wait

  return DashboardData(users: userList, orders: orderList);
}

Padrões de Stream

// Repositório expõe streams reativos para dados ao vivo
Stream<List<Item>> watchCartItems() => _db
    .watchTable('cart_items')
    .map((rows) => rows.map(Item.fromRow).toList());

// Na camada de widget — declarativo, sem assinatura manual
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(),
  },
)

BuildContext Após Await

// CRÍTICO — sempre verifique mounted após qualquer await em StatefulWidget
Future<void> _handleSubmit() async {
  setState(() => _isLoading = true);
  try {
    await authService.login(_email, _password);
    if (!mounted) return; // ← guard antes de usar 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. Arquitetura de Widgets

Extraia para Classes, Não Métodos

// RUIM — método privado retornando widget, impede otimização
Widget _buildHeader() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
  );
}

// BOM — classe de widget separada, permite const, reutilização de elemento
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),
    );
  }
}

Propagação const

// RUIM — novas instâncias a cada reconstrução
child: Padding(
  padding: EdgeInsets.all(16.0),       // não const
  child: Icon(Icons.home, size: 24.0), // não const
)

// BOM — const interrompe a propagação de reconstrução
child: const Padding(
  padding: EdgeInsets.all(16.0),
  child: Icon(Icons.home, size: 24.0),
)

Reconstruções com Escopo

// RUIM — página inteira é reconstruída a cada mudança de contador
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider); // reconstroi tudo
    return Scaffold(
      body: Column(children: [
        const ExpensiveHeader(), // reconstruído desnecessariamente
        Text('$count'),
        const ExpensiveFooter(), // reconstruído desnecessariamente
      ]),
    );
  }
}

// BOM — isole a parte que reconstroi
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Column(children: [
        ExpensiveHeader(),        // nunca reconstruído (const)
        _CounterDisplay(),        // apenas este reconstroi
        ExpensiveFooter(),        // nunca reconstruído (const)
      ]),
    );
  }
}

class _CounterDisplay extends ConsumerWidget {
  const _CounterDisplay();

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

5. Gerenciamento de Estado: BLoC/Cubit

// Cubit — estado síncrono ou assíncrono simples
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());
  }
}

// No 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. Gerenciamento de Estado: Riverpod

// Provider assíncrono com auto-descarte
@riverpod
Future<List<Product>> products(Ref ref) async {
  final repo = ref.watch(productRepositoryProvider);
  return repo.getAll();
}

// Notificador com mutações complexas
@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 = [];
}

// Provider derivado (padrão seletor)
@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 (do pacote collection) evita StateError quando o produto está ausente
    final product = products.firstWhereOrNull((p) => p.id == item.productId);
    return total + (product?.price ?? 0) * item.quantity;
  });
}

7. Navegação com GoRouter

final router = GoRouter(
  initialLocation: '/',
  // refreshListenable reavalia o redirecionamento sempre que o estado de autenticação muda
  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. HTTP com Dio

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'},
));

// Adiciona interceptor de autenticação
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 {
    // Proteção contra loops infinitos de retry: tenta renovar apenas uma vez por requisição
    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);
  },
));

// Repositório usando 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. Arquitetura de Tratamento de Erros

// Captura global de erros — configurada em 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());
}

// ErrorWidget personalizado para produção
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
      builder: (context, child) => ErrorWidget.builder = (details) =>
          const Scaffold(
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.error_outline, size: 48, color: Colors.red),
                  SizedBox(height: 16),
                  Text('Ocorreu um erro inesperado. Tente novamente.'),
                ],
              ),
            ),
          ),
    );
  }
}

10. Testes

Teste de BLoC

void main() {
  late MockAuthService authService;

  setUp(() {
    authService = MockAuthService();
  });

  blocTest<AuthCubit, AuthState>(
    'emite [loading, authenticated] quando o login é bem-sucedido',
    build: () => AuthCubit(authService),
    act: (cubit) => cubit.login('[email protected]', 'password'),
    setUp: () => when(() => authService.login(any(), any()))
        .thenAnswer((_) async => User(id: '1', name: 'Test')),
    expect: () => [
      const AuthState.loading(),
      AuthState.authenticated(User(id: '1', name: 'Test')),
    ],
  );
}

Teste de Widget Riverpod

void main() {
  testWidgets('exibe o total do carrinho', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          cartNotifierProvider.overrideWith(() => _TestCartNotifier()),
          productsProvider.overrideWith(() => Future.value([Product(id: '1', name: 'Item', price: 10)])),
        ],
        child: const MaterialApp(home: CartPage()),
      ),
    );

    expect(find.text('Total: R\$ 20,00'), findsOneWidget);
  });
}

Fakes vs. Mocks

// Prefira fakes em vez de mocks para lógica de negócios
class FakeAuthService implements AuthService {
  bool shouldFail = false;

  @override
  Future<User> login(String email, String password) async {
    if (shouldFail) throw AuthException('Falha');
    return User(id: 'fake', name: 'Fake');
  }
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

Habilidades Relacionadas