todo-app/docs/DOCS-flutter-rules-ui.md
Krzysztof kuhy Rudnicki 59ee407d80
Some checks failed
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Adopt the four markdown namespaces and gate them
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:54:35 +02:00

6.4 KiB

Flutter AI rules — UI: theming, layout and overlays

Material theming, WidgetStateProperty, layout, and overlays.

Verbatim from flutter/flutter docs/rules/rules.md, split across four files only to keep each under the repo's 250-line cap. Re-fetch periodically to stay current; do not hand-edit.

  • Responsiveness: Use LayoutBuilder or MediaQuery to create responsive UIs.
  • Text: Use Theme.of(context).textTheme for text styles.
  • Text Fields: Configure textCapitalization, keyboardType, and placeholder.

Material Theming Best Practices

Embrace ThemeData and Material 3

  • Use ColorScheme.fromSeed(): Use this to generate a complete, harmonious color palette for both light and dark modes from a single seed color.
  • Define Light and Dark Themes: Provide both theme and darkTheme to your MaterialApp to support system brightness settings seamlessly.
  • Centralize Component Styles: Customize specific component themes (e.g., elevatedButtonTheme, cardTheme, appBarTheme) within ThemeData to ensure consistency.
  • Dark/Light Mode and Theme Toggle: Implement support for both light and dark themes using theme and darkTheme properties of MaterialApp. The themeMode property can be dynamically controlled (e.g., via a ChangeNotifierProvider) to allow for toggling between ThemeMode.light, ThemeMode.dark, or ThemeMode.system.
// main.dart
MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.deepPurple,
      brightness: Brightness.light,
    ),
    textTheme: const TextTheme(
      displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold),
      bodyMedium: TextStyle(fontSize: 14.0, height: 1.4),
    ),
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.deepPurple,
      brightness: Brightness.dark,
    ),
  ),
  home: const MyHomePage(),
);

Implement Design Tokens with ThemeExtension

For custom styles that aren't part of the standard ThemeData, use ThemeExtension to define reusable design tokens.

  • Create a Custom Theme Extension: Define a class that extends ThemeExtension<T> and include your custom properties.
  • Implement copyWith and lerp: These methods are required for the extension to work correctly with theme transitions.
  • Register in ThemeData: Add your custom extension to the extensions list in your ThemeData.
  • Access Tokens in Widgets: Use Theme.of(context).extension<MyColors>()! to access your custom tokens.
// 1. Define the extension
@immutable
class MyColors extends ThemeExtension<MyColors> {
  const MyColors({required this.success, required this.danger});

  final Color? success;
  final Color? danger;

  @override
  ThemeExtension<MyColors> copyWith({Color? success, Color? danger}) {
    return MyColors(success: success ?? this.success, danger: danger ?? this.danger);
  }

  @override
  ThemeExtension<MyColors> lerp(ThemeExtension<MyColors>? other, double t) {
    if (other is! MyColors) return this;
    return MyColors(
      success: Color.lerp(success, other.success, t),
      danger: Color.lerp(danger, other.danger, t),
    );
  }
}

// 2. Register it in ThemeData
theme: ThemeData(
  extensions: const <ThemeExtension<dynamic>>[
    MyColors(success: Colors.green, danger: Colors.red),
  ],
),

// 3. Use it in a widget
Container(
  color: Theme.of(context).extension<MyColors>()!.success,
)

Styling with WidgetStateProperty

  • WidgetStateProperty.resolveWith: Provide a function that receives a Set<WidgetState> and returns the appropriate value for the current state.
  • WidgetStateProperty.all: A shorthand for when the value is the same for all states.
// Example: Creating a button style that changes color when pressed.
final ButtonStyle myButtonStyle = ButtonStyle(
  backgroundColor: WidgetStateProperty.resolveWith<Color>(
    (Set<WidgetState> states) {
      if (states.contains(WidgetState.pressed)) {
        return Colors.green; // Color when pressed
      }
      return Colors.red; // Default color
    },
  ),
);

Layout Best Practices

Building Flexible and Overflow-Safe Layouts

For Rows and Columns
  • Expanded: Use to make a child widget fill the remaining available space along the main axis.
  • Flexible: Use when you want a widget to shrink to fit, but not necessarily grow. Don't combine Flexible and Expanded in the same Row or Column.
  • Wrap: Use when you have a series of widgets that would overflow a Row or Column, and you want them to move to the next line.
For General Content
  • SingleChildScrollView: Use when your content is intrinsically larger than the viewport, but is a fixed size.
  • ListView / GridView: For long lists or grids of content, always use a builder constructor (.builder).
  • FittedBox: Use to scale or fit a single child widget within its parent.
  • LayoutBuilder: Use for complex, responsive layouts to make decisions based on the available space.

Layering Widgets with Stack

  • Positioned: Use to precisely place a child within a Stack by anchoring it to the edges.
  • Align: Use to position a child within a Stack using alignments like Alignment.center.

Advanced Layout with Overlays

  • OverlayPortal: Use this widget to show UI elements (like custom dropdowns or tooltips) "on top" of everything else. It manages the OverlayEntry for you.

    class MyDropdown extends StatefulWidget {
      const MyDropdown({super.key});
    
      @override
      State<MyDropdown> createState() => _MyDropdownState();
    }
    
    class _MyDropdownState extends State<MyDropdown> {
      final _controller = OverlayPortalController();
    
      @override
      Widget build(BuildContext context) {
        return OverlayPortal(
          controller: _controller,
          overlayChildBuilder: (BuildContext context) {
            return const Positioned(
              top: 50,
              left: 10,
              child: Card(
                child: Padding(
                  padding: EdgeInsets.all(8.0),
                  child: Text('I am an overlay!'),
                ),
              ),
            );
          },
          child: ElevatedButton(
            onPressed: _controller.toggle,
            child: const Text('Toggle Overlay'),
          ),
        );
      }
    }
    

Color Scheme Best Practices