Files
APAW/.kilo/rules/flutter.md
¨NW¨ af5f401a53 feat: add Flutter development support with agent, rules and skills
- Add flutter-developer agent (.kilo/agents/flutter-developer.md)
  - Role definition for cross-platform mobile development
  - Clean architecture templates (Domain/Presentation/Data)
  - State management patterns (Riverpod, Bloc, Provider)
  - Widget patterns, navigation, platform channels
  - Build & release commands
  - Performance and security checklists

- Add Flutter development rules (.kilo/rules/flutter.md)
  - Code style guidelines (const, final, trailing commas)
  - Widget architecture best practices
  - State management requirements
  - Error handling, API & network patterns
  - Navigation, testing, performance
  - Security and localization
  - Prohibitions list

- Add Flutter skills:
  - flutter-state: Riverpod, Bloc, Provider patterns
  - flutter-widgets: Widget composition, responsive design
  - flutter-navigation: go_router, deep links, guards

- Update AGENTS.md: add @flutter-developer to Core Development
- Update kilo.jsonc: configure flutter-developer and go-developer agents
2026-04-05 17:04:13 +01:00

11 KiB

Flutter Development Rules

Essential rules for Flutter mobile app development.

Code Style

  • Use final and const wherever possible
  • Follow Dart naming conventions
  • Use trailing commas for better auto-formatting
  • Keep widgets small and focused
  • Use meaningful variable names
// ✅ Good
class UserList extends StatelessWidget {
  const UserList({
    super.key,
    required this.users,
    this.onUserTap,
  });

  final List<User> users;
  final VoidCallback(User)? onUserTap;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        final user = users[index];
        return UserTile(
          user: user,
          onTap: onUserTap,
        );
      },
    );
  }
}

// ❌ Bad
class UserList extends StatelessWidget {
  UserList(this.users, {this.onUserTap}); // Missing const
  final List<User> users;
  final Function(User)? onUserTap; // Use VoidCallback instead
  @override
  Widget build(BuildContext context) {
    return ListView(children: users.map((u) => UserTile(u)).toList()); // No const
  }
}

Widget Architecture

  • Prefer stateless widgets when possible
  • Split large widgets into smaller ones
  • Use composition over inheritance
  • Pass data through constructors
  • Keep build methods pure
// ✅ Good: Split into small widgets
class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key, required this.user});

  final User user;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ProfileAppBar(user: user),
      body: ProfileBody(user: user),
    );
  }
}

// ❌ Bad: Everything in one widget
class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Column(
        children: [
          // 100+ lines of nested widgets
        ],
      ),
    );
  }
}

State Management

  • Use Riverpod, Bloc, or Provider (project choice)
  • Keep state close to where it's used
  • Separate business logic from UI
  • Use immutable state classes
// ✅ Good: Riverpod state management
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
  return UserNotifier();
});

class UserNotifier extends StateNotifier<UserState> {
  UserNotifier() : super(const UserState.initial());

  Future<void> loadUser(String id) async {
    state = const UserState.loading();
    try {
      final user = await _userRepository.getUser(id);
      state = UserState.loaded(user);
    } catch (e) {
      state = UserState.error(e.toString());
    }
  }
}

// ✅ Good: Immutable state with freezed
@freezed
class UserState with _$UserState {
  const factory UserState.initial() = _Initial;
  const factory UserState.loading() = _Loading;
  const factory UserState.loaded(User user) = _Loaded;
  const factory UserState.error(String message) = _Error;
}

Error Handling

  • Use Result/Either types for async operations
  • Never silently catch errors
  • Show user-friendly error messages
  • Log errors to monitoring service
// ✅ Good
Future<void> loadData() async {
  state = const AsyncValue.loading();
  state = await AsyncValue.guard(() async {
    final result = await _repository.fetchData();
    if (result.isError) {
      throw ServerException(result.message);
    }
    return result.data;
  });
}

// ❌ Bad
Future<void> loadData() async {
  try {
    final data = await _repository.fetchData();
    state = data;
  } catch (e) {
    // Silently swallowing error
  }
}

API & Network

  • Use dio for HTTP requests
  • Implement request interceptors
  • Handle connectivity changes
  • Cache responses when appropriate
// ✅ Good
class ApiClient {
  final Dio _dio;

  ApiClient(this._dio) {
    _dio.interceptors.addAll([
      AuthInterceptor(),
      LoggingInterceptor(),
      RetryInterceptor(),
    ]);
  }

  Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
    try {
      return await _dio.get(path, queryParameters: queryParameters);
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }
}

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    options.headers['Authorization'] = 'Bearer ${_getToken()}';
    handler.next(options);
  }
}

Navigation

  • Use go_router for declarative routing
  • Define routes as constants
  • Pass data through route parameters
  • Handle deep links
// ✅ Good: go_router setup
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/user/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return UserDetailScreen(userId: id);
      },
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
  errorBuilder: (context, state) => const ErrorScreen(),
);

Testing

  • Write unit tests for business logic
  • Write widget tests for UI components
  • Use mocks for dependencies
  • Test edge cases and error states
// ✅ Good: Unit test
void main() {
  group('UserNotifier', () {
    late UserNotifier notifier;
    late MockUserRepository mockRepository;

    setUp(() {
      mockRepository = MockUserRepository();
      notifier = UserNotifier(mockRepository);
    });

    test('loads user successfully', () async {
      // Arrange
      final user = User(id: '1', name: 'Test');
      when(mockRepository.getUser('1')).thenAnswer((_) async => user);

      // Act
      await notifier.loadUser('1');

      // Assert
      expect(notifier.state, equals(UserState.loaded(user)));
    });

    test('handles error gracefully', () async {
      // Arrange
      when(mockRepository.getUser('1')).thenThrow(NetworkException());

      // Act
      await notifier.loadUser('1');

      // Assert
      expect(notifier.state, isA<UserError>());
    });
  });
}

// ✅ Good: Widget test
void main() {
  testWidgets('UserTile displays user name', (tester) async {
    // Arrange
    final user = User(id: '1', name: 'John Doe');

    // Act
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: UserTile(user: user),
      ),
    ));

    // Assert
    expect(find.text('John Doe'), findsOneWidget);
  });
}

Performance

  • Use const constructors
  • Avoid rebuilds with Provider/InheritedWidget
  • Use ListView.builder for long lists
  • Lazy load images with cached_network_image
  • Profile with DevTools
// ✅ Good
class UserTile extends StatelessWidget {
  const UserTile({
    super.key,
    required this.user,
  }); // const constructor

  final User user;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CachedNetworkImage(
        imageUrl: user.avatarUrl,
        placeholder: (context, url) => const CircularProgressIndicator(),
        errorWidget: (context, url, error) => const Icon(Icons.error),
      ),
      title: Text(user.name),
    );
  }
}

Platform-Specific Code

  • Use separate files with .dart and .freezed.dart extensions
  • Use conditional imports for platform differences
  • Follow Material (Android) and Cupertino (iOS) guidelines
// ✅ Good: Platform-specific styling
Widget buildButton(BuildContext context) {
  return Platform.isIOS
      ? CupertinoButton.filled(
          onPressed: onPressed,
          child: Text(label),
        )
      : ElevatedButton(
          onPressed: onPressed,
          child: Text(label),
        );
}

Project Structure

lib/
├── main.dart
├── app.dart
├── core/
│   ├── constants/
│   ├── theme/
│   ├── utils/
│   └── errors/
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   ├── datasources/
│   │   │   ├── models/
│   │   │   └── repositories/
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   ├── repositories/
│   │   │   └── usecases/
│   │   └── presentation/
│   │       ├── pages/
│   │       ├── widgets/
│   │       └── providers/
│   └── user/
├── shared/
│   ├── widgets/
│   └── services/
└── injection_container.dart

Security

  • Never store sensitive data in plain text
  • Use flutter_secure_storage for tokens
  • Validate all user inputs
  • Use certificate pinning for APIs
  • Obfuscate release builds
// ✅ Good
final storage = FlutterSecureStorage();

Future<void> saveToken(String token) async {
  await storage.write(key: 'auth_token', value: token);
}

Future<void> buildRelease() async {
  await Process.run('flutter', [
    'build',
    'apk',
    '--release',
    '--obfuscate',
    '--split-debug-info=$debugInfoPath',
  ]);
}

// ❌ Bad
Future<void> saveToken(String token) async {
  await SharedPreferences.setString('auth_token', token); // Insecure!
}

Localization

  • Use intl package for translations
  • Generate localization files
  • Support RTL languages
  • Use message formatting for dynamic content
// ✅ Good
Widget build(BuildContext context) {
  return Text(AppLocalizations.of(context).hello(userName));
}

// Generated in l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

Dependencies

  • Keep dependencies up to date
  • Use exact versions in pubspec.yaml
  • Run flutter pub outdated regularly
  • Use flutter analyze before committing
# ✅ Good: Exact versions
dependencies:
  flutter:
    sdk: flutter
  riverpod: 2.4.9
  go_router: 13.1.0
  dio: 5.4.0

# ❌ Bad: Version ranges
dependencies:
  flutter:
    sdk: flutter
  riverpod: ^2.4.0  # Unpredictable
  dio: any  # Dangerous

Clean Architecture

  • Separate layers: presentation, domain, data
  • Use dependency injection
  • Keep business logic in use cases
  • Entities should be pure Dart classes
// Domain layer
abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> saveUser(User user);
}

class GetUser {
  final UserRepository repository;
  
  GetUser(this.repository);

  Future<User> call(String id) async {
    return repository.getUser(id);
  }
}

// Data layer
class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource remoteDataSource;
  final UserLocalDataSource localDataSource;

  UserRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
  });

  @override
  Future<User> getUser(String id) async {
    try {
      final remoteUser = await remoteDataSource.getUser(id);
      await localDataSource.cacheUser(remoteUser);
      return remoteUser;
    } catch (e) {
      return localDataSource.getUser(id);
    }
  }
}

Build & Release

  • Use flavors for different environments
  • Configure build variants
  • Sign releases properly
  • Upload symbols for crash reporting
# ✅ Good: Build commands
flutter build apk --flavor production --release
flutter build ios --flavor production --release
flutter build appbundle --flavor production --release

Prohibitions

  • DO NOT use setState in production code (use state management)
  • DO NOT put business logic in widgets
  • DO NOT use dynamic types
  • DO NOT ignore lint warnings
  • DO NOT skip testing for critical paths
  • DO NOT use hot reload as a development strategy
  • DO NOT embed secrets in code