CtrlK
BlogDocsLog inGet started
Tessl Logo

g14wxz/flutter-micro-ui

Atomic Design micro-UI architecture

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

index.mddocs/

Flutter Micro-UI (Atomic Design)

Enforces a strict Atomic Design classification for micro-UI components in Flutter DDD projects with hybrid state management (hooks_riverpod + flutter_bloc).

Decision Tree

Use this tree to classify every UI component before writing code:

Is this component ONLY displaying data passed via constructor?
├── YES → Atom (StatelessWidget)
│         No controllers, no ref, no mutable state.
│         Examples: AppLabel, StatusBadge, PriceTag, IconTile
│
└── NO → Does it need local controllers OR Riverpod access?
         ├── YES → Molecule (HookConsumerWidget)
         │         Uses useTextEditingController, useFocusNode,
         │         ref.watch, ref.read, useState, etc.
         │         Examples: SearchBar, EmailField, QuantitySelector
         │
         └── NO → Re-evaluate. If truly no state, use Atom.

Atom Pattern (StatelessWidget)

Atoms are the smallest UI building blocks. They:

  • Accept ALL data via final constructor parameters
  • Use const constructors when possible
  • Contain ZERO business logic
  • Have no side effects
  • Cannot access Riverpod, BLoC, or any state management
class PriceTag extends StatelessWidget {
  const PriceTag({super.key, required this.amount, required this.currency});

  final double amount;
  final String currency;

  @override
  Widget build(BuildContext context) {
    return Text('$currency ${amount.toStringAsFixed(2)}');
  }
}

Molecule Pattern (HookConsumerWidget)

Molecules combine atoms with local state and/or Riverpod access. They:

  • Use HookConsumerWidget from hooks_riverpod
  • Manage ephemeral UI state via hooks (useTextEditingController, useFocusNode, useState)
  • Read shared state via ref.watch / ref.read
  • Delegate business logic to providers — NEVER contain it
  • Use a Validator class for input validation
class EmailField extends HookConsumerWidget {
  const EmailField({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = useTextEditingController();
    final focusNode = useFocusNode();

    return TextFormField(
      controller: controller,
      focusNode: focusNode,
      validator: EmailFieldValidator.validate,
      decoration: const InputDecoration(labelText: 'Email'),
      onFieldSubmitted: (value) {
        ref.read(authFormProvider.notifier).updateEmail(value);
      },
    );
  }
}

Validator Pattern

Every molecule with user input MUST have a companion Validator:

/// Validator for EmailField molecule.
class EmailFieldValidator {
  EmailFieldValidator._();

  static String? validate(String? value) {
    if (value == null || value.isEmpty) return 'Email is required';
    if (!RegExp(r'^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
      return 'Enter a valid email address';
    }
    return null;
  }
}

Validators are pure functions with no dependencies. They:

  • Live in {molecule_name}_validator.dart alongside the molecule
  • Expose static String? validate(String? value) per field
  • Wire into TextFormField.validator directly
  • Are tested independently from the widget

Testing Pattern

Every HookConsumerWidget molecule MUST have a widget test:

void main() {
  group('EmailField', () {
    testWidgets('renders with empty initial state', (tester) async {
      await tester.pumpWidget(
        ProviderScope(
          overrides: [
            authFormProvider.overrideWith(() => MockAuthFormNotifier()),
          ],
          child: const MaterialApp(home: Scaffold(body: Form(child: EmailField()))),
        ),
      );
      expect(find.byType(TextFormField), findsOneWidget);
    });

    testWidgets('shows error for invalid email', (tester) async {
      await tester.pumpWidget(/* ... ProviderScope wrapper ... */);
      final formKey = GlobalKey<FormState>();
      // Enter invalid email, trigger validation, assert error text
    });

    testWidgets('calls provider on valid submission', (tester) async {
      // Enter valid email, submit, verify ref.read was called
    });
  });
}

File Structure

lib/
├── shared/
│   └── widgets/
│       ├── atoms/           ← StatelessWidget components
│       │   ├── price_tag.dart
│       │   └── status_badge.dart
│       └── molecules/       ← HookConsumerWidget components
│           ├── email_field.dart
│           ├── email_field_validator.dart
│           ├── search_bar.dart
│           └── search_bar_validator.dart
├── features/{feature}/
│   └── presentation/
│       └── widgets/
│           ├── atoms/
│           └── molecules/
test/
├── shared/
│   └── widgets/
│       └── molecules/
│           └── email_field_test.dart
└── features/{feature}/
    └── presentation/
        └── widgets/
            └── molecules/

Compatibility

  • Requires: hooks_riverpod, flutter_hooks, mocktail (dev)
  • Compatible with: flutter-hybrid-state-management, flutter-feature-first-architecture, flutter-tdd-skeleton
  • Incompatible with: Any tile that mandates ConsumerWidget without hooks, or StatefulWidget for local state

docs

index.md

tile.json