CtrlK
BlogDocsLog inGet started
Tessl Logo

g14wxz/flutter-micro-ui

Atomic Design micro-UI architecture

90

Quality

90%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

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
Workspace
g14wxz
Visibility
Public
Created
Last updated
Publish Source
CLI
Badge
g14wxz/flutter-micro-ui badge