Flutter extension for Gemini CLI

repository·main·Indexed 18 days ago

https://github.com/gemini-cli-extensions/flutter

A Flutter extension for Gemini CLI (v0.4.0+) that automates the development lifecycle. It provides commands for bootstrapping projects and packages (/create-app, /create-package), guided code modifications (/modify), automated pre-commit checks and Conventional Commit generation (/commit), and a structured debugging workflow (/debug-app). The extension integrates with the Dart Tooling Daemon (DTD) for connecting to running apps and manages the Dart MCP server.

Tokens
8.9K
Snippets
28
Records
40
Agent score
63%

What's inside gemini-cli-extensions-flutter

  1. Implement Flutter state management

    main

    The extension follows a preference for Flutter's built-in state management solutions unless third-party packages are explicitly requested.

    • Simple local state: Use ValueNotifier with ValueListenableBuilder for single values.
    • Asynchronous sequences: Use Streams and StreamBuilder.
    • Single asynchronous operations: Use Futures and FutureBuilder.
    • Complex or shared state: Use ChangeNotifier with ListenableBuilder.
    • Robust architecture: Use the Model-View-ViewModel (MVVM) pattern.
    • Dependency Injection: Prefer manual constructor injection to keep dependencies explicit.
    // Define a ValueNotifier to hold the state.
    final ValueNotifier<int> _counter = ValueNotifier<int>(0);
    
    // Use ValueListenableBuilder to listen and rebuild.
    ValueListenableBuilder<int>(
      valueListenable: _counter,
      builder: (context, value, child) {
        return Text('Count: $value');
      },
    );
  2. Follow Flutter and Dart best practices

    main

    When developing with this extension, adhere to these core principles:

    Dart Best Practices

    • Null Safety: Write sound null-safe code; avoid ! unless necessary.
    • Async/Await: Use Futures for single async operations and Streams for sequences.
    • Patterns: Use pattern matching and records to simplify code.
    • Documentation: Add documentation comments to all public APIs.

    Flutter Best Practices

    • Immutability: Treat widgets (especially StatelessWidget) as immutable.
    • Composition: Favor composing small, reusable widgets over deep inheritance or large build() methods.
    • Performance:
      • Use const constructors to reduce rebuilds.
      • Use ListView.builder or SliverList for long lists.
      • Use compute() to run expensive calculations in a separate isolate.
      • Avoid expensive operations (network, complex math) directly inside build() methods.
  3. Layer widgets with Stack and OverlayPortal

    main

    Using Stack

    • Positioned: Precisely place a child within a Stack by anchoring it to edges.
    • Align: Position a child using alignments like Alignment.center.

    Using OverlayPortal for advanced UI

    Use OverlayPortal to show UI elements (like dropdowns or tooltips) on top of all other content. It manages the OverlayEntry automatically via an OverlayPortalController.

    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'),
          ),
        );
      }
    }
  4. Manage Flutter dependencies with the `pub` tool

    main

    DO NOT manually modify dependencies in the pubspec.yaml file. Always use the pub tool to add or remove dependencies.

    • Adding Dependencies: Use the pub tool with add and remove subcommands.
    • Dev Dependencies: When adding a package as a dev dependency via the pub tool, prefix the package name with "dev:".
    • Exception: You may edit pubspec.yaml directly only to change metadata like description, version, or the package name.
  5. Implement typographic hierarchy in Flutter

    main

    Font Selection

    • Limit to one or two font families.
    • Prioritize legibility (Sans-serif is preferred for body text).
    • Use the google_fonts package for open-source options.

    Hierarchy and Readability

    • Scale: Define specific sizes for headlines, titles, body, and captions.
    • Line Height (Leading): Aim for 1.4x to 1.6x the font size.
    • Line Length: Aim for 45-75 characters for body text.
    • Weight/Color: Use font weight and opacity to establish importance.
    // In your ThemeData
    textTheme: const TextTheme(
      displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold),
      titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold),
      bodyLarge: TextStyle(fontSize: 16.0, height: 1.5),
      bodyMedium: TextStyle(fontSize: 14.0, height: 1.4),
      labelSmall: TextStyle(fontSize: 11.0, color: Colors.grey),
    ),
  6. Configure centralized Flutter theming

    main

    Use a centralized ThemeData object to maintain consistency. Implement support for both light and dark modes using the theme and darkTheme properties in MaterialApp. You can generate harmonious color palettes from a single seed color using ColorScheme.fromSeed.

    // 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(),
    );
  7. Connect to a running Flutter app via DTD

    main

    You can connect Gemini CLI to a running app using the Dart Tooling Daemon (DTD) URL.

    Method 1: VSCode

    1. Run the app in VSCode on a target device.
    2. Open the VSCode Command Runner (Cmd+Shift+P or Ctrl+Shift+P) and run Copy DTD Uri to Clipboard.
    3. In Gemini CLI, prompt: Connect to the Flutter app with this DTD URL: <PASTE_URL>.

    Method 2: Command Line Run the app with the --print-dtd flag to see the URL:

    flutter run --print-dtd
    $ flutter run --print-dtd
  8. Configure declarative navigation with go_router

    main

    For declarative navigation, deep linking, and web support, use the go_router package.

    1. Add the dependency: flutter pub add go_router
    2. Define your GoRouter instance with a route tree.
    3. Pass the router configuration to MaterialApp.router.
    // 1. Add the dependency
    // flutter pub add go_router
    
    // 2. Configure the router
    final GoRouter _router = GoRouter(
      routes: <RouteBase>[
        GoRoute(
          path: '/',
          builder: (context, state) => const HomeScreen(),
          routes: <RouteBase>[
            GoRoute(
              path: 'details/:id', // Route with a path parameter
              builder: (context, state) {
                final String id = state.pathParameters['id']!;
                return DetailScreen(id: id);
              },
            ),
          ],
        ),
      ],
    );
    
    // 3. Use it in your MaterialApp
    MaterialApp.router(
      routerConfig: _router,
    );
  9. Run Dart code generation with build_runner

    main

    If your project uses code generation (e.g., json_serializable), ensure build_runner is a dev_dependency in your pubspec.yaml. After modifying files that require generation, run the following command to update the generated files and resolve conflicts:

    dart run build_runner build --delete-conflicting-outputs
  10. Testing strategies for Flutter and Dart

    main

    Follow the Arrange-Act-Assert (Given-When-Then) pattern for all tests. Use the following packages for different testing scopes:

    • Unit Tests: Use package:test for domain logic, data layers, and state management.
    • Widget Tests: Use package:flutter_test for UI components.
    • Integration Tests: Use package:integration_test (from the Flutter SDK) for end-to-end user flows. Add it as a dev_dependency with sdk: flutter in pubspec.yaml.

    Best Practices:

    • Prefer fakes or stubs over mocks. If mocks are required, use mockito or mocktail.
    • Avoid using code generation for mocks (even if used for state management like freezed).
    • Use package:checks for more expressive assertions.
  11. Implement structured logging with dart:developer

    main

    Use the log function from dart:developer for structured logging. This integrates with Dart DevTools and allows you to include metadata like name, level, error, and stackTrace for better debugging, especially for error handling.

    import 'dart:developer' as developer;
    
    // For simple messages
    developer.log('User logged in successfully.');
    
    // For structured error logging
    try {
      // ... code that might fail
    } catch (e, s) {
      developer.log(
        'Failed to fetch data',
        name: 'myapp.network',
        level: 1000, // SEVERE
        error: e,
        stackTrace: s,
      );
    }
  12. Handle JSON serialization in Dart

    main

    Use the json_serializable and json_annotation packages for robust parsing and encoding of JSON data. To ensure Dart's camelCase fields are converted to snake_case in JSON, use the fieldRename: FieldRename.snake option within the @JsonSerializable annotation.

    // In your model file
    import 'package:json_annotation/json_annotation.dart';
    
    part 'user.g.dart';
    
    @JsonSerializable(fieldRename: FieldRename.snake)
    class User {
      final String firstName;
      final String lastName;
    
      User({required this.firstName, required this.lastName});
    
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
      Map<String, dynamic> toJson() => _$UserToJson(this);
    }