CtrlK
BlogDocsLog inGet started
Tessl Logo

flutter-project-starter

Scaffold a production-ready Flutter 3.27+ app with Dart 3.6+, Material 3, BLoC or Riverpod for state management, go_router for navigation, platform channels, freezed for models, and Hive/Isar for local storage.

65

Quality

57%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./mobile/flutter-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Flutter Project Starter

Scaffold a production-ready Flutter 3.27+ app with Dart 3.6+, Material 3, BLoC or Riverpod for state management, go_router for navigation, platform channels, freezed for models, and Hive/Isar for local storage.

Prerequisites

  • Flutter SDK >= 3.27
  • Dart SDK >= 3.6 (bundled with Flutter)
  • Xcode 16+ (macOS, for iOS)
  • Android Studio with SDK 35+
  • CocoaPods (macOS, for iOS dependencies)

Scaffold Command

flutter create <project_name> --org com.example --platforms ios,android
cd <project_name>

# Core dependencies
flutter pub add go_router
flutter pub add flutter_bloc bloc
flutter pub add freezed_annotation json_annotation
flutter pub add dio                         # HTTP client
flutter pub add hive_flutter                # Local storage
flutter pub add flutter_secure_storage      # Secure token storage
flutter pub add get_it injectable           # Dependency injection

# Code generation
flutter pub add --dev freezed build_runner json_serializable
flutter pub add --dev injectable_generator

# Testing
flutter pub add --dev bloc_test mocktail

# Run code generation
dart run build_runner build --delete-conflicting-outputs

Alternative — Riverpod instead of BLoC

flutter pub add flutter_riverpod riverpod_annotation
flutter pub add --dev riverpod_generator custom_lint riverpod_lint

Project Structure

lib/
  main.dart                        # App entry — DI setup, MaterialApp.router
  app/
    app.dart                       # MaterialApp widget with theme and router
    router.dart                    # GoRouter configuration
    theme.dart                     # Material 3 theme data
  core/
    di/
      injection.dart               # GetIt + Injectable setup
    network/
      api_client.dart              # Dio instance with interceptors
      api_interceptors.dart        # Auth token, logging interceptors
    storage/
      local_storage.dart           # Hive wrapper
      secure_storage.dart          # flutter_secure_storage wrapper
    error/
      failures.dart                # Failure classes for error handling
      exceptions.dart              # Custom exceptions
  features/
    auth/
      data/
        datasources/
          auth_remote_datasource.dart
        repositories/
          auth_repository_impl.dart
        models/
          auth_response_model.dart
          auth_response_model.freezed.dart
          auth_response_model.g.dart
      domain/
        entities/
          user.dart
        repositories/
          auth_repository.dart     # Abstract interface
        usecases/
          login_usecase.dart
      presentation/
        bloc/
          auth_bloc.dart
          auth_event.dart
          auth_state.dart
        pages/
          login_page.dart
        widgets/
          login_form.dart
    users/
      data/
        datasources/
        repositories/
        models/
      domain/
        entities/
        repositories/
        usecases/
      presentation/
        bloc/
        pages/
          user_list_page.dart
          user_detail_page.dart
        widgets/
          user_card.dart
test/
  features/
    auth/
      presentation/
        bloc/
          auth_bloc_test.dart
    users/
      presentation/
          user_list_page_test.dart

Key Conventions

  • Feature-first folder structure — each feature is self-contained with data, domain, and presentation layers
  • Clean Architecture: domain layer has no dependencies, data layer implements domain interfaces, presentation depends on domain
  • BLoC pattern: Events in, States out — UI dispatches events, BLoC emits states
  • Freezed for immutable models and union types — eliminates boilerplate for copyWith, ==, toString, JSON serialization
  • GoRouter for declarative, type-safe routing with deep linking support
  • GetIt + Injectable for dependency injection — register in one place, inject everywhere
  • Material 3 with ColorScheme.fromSeed() for consistent theming
  • Dart 3.6 features: patterns, sealed classes, records, class modifiers

Essential Patterns

App Entry — lib/main.dart

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';

import 'app/app.dart';
import 'core/di/injection.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Hive for local storage
  await Hive.initFlutter();

  // Setup dependency injection
  await configureDependencies();

  runApp(const MyApp());
}

App Widget — lib/app/app.dart

import 'package:flutter/material.dart';

import 'router.dart';
import 'theme.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'My App',
      theme: AppTheme.light,
      darkTheme: AppTheme.dark,
      themeMode: ThemeMode.system,
      routerConfig: appRouter,
      debugShowCheckedModeBanner: false,
    );
  }
}

Theme — lib/app/theme.dart

import 'package:flutter/material.dart';

class AppTheme {
  static final light = ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1A73E8),
      brightness: Brightness.light,
    ),
    inputDecorationTheme: const InputDecorationTheme(
      border: OutlineInputBorder(),
      filled: true,
    ),
  );

  static final dark = ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1A73E8),
      brightness: Brightness.dark,
    ),
    inputDecorationTheme: const InputDecorationTheme(
      border: OutlineInputBorder(),
      filled: true,
    ),
  );
}

Router — lib/app/router.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

import '../features/auth/presentation/pages/login_page.dart';
import '../features/users/presentation/pages/user_list_page.dart';
import '../features/users/presentation/pages/user_detail_page.dart';

final appRouter = GoRouter(
  initialLocation: '/users',
  redirect: (context, state) {
    // Check auth state, redirect to /login if needed
    // final isAuthenticated = getIt<AuthBloc>().state is AuthAuthenticated;
    // if (!isAuthenticated && !state.matchedLocation.startsWith('/login')) {
    //   return '/login';
    // }
    return null;
  },
  routes: [
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginPage(),
    ),
    GoRoute(
      path: '/users',
      builder: (context, state) => const UserListPage(),
      routes: [
        GoRoute(
          path: ':id',
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return UserDetailPage(userId: id);
          },
        ),
      ],
    ),
  ],
);

Freezed Model — lib/features/users/data/models/user_model.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user_model.freezed.dart';
part 'user_model.g.dart';

@freezed
class UserModel with _$UserModel {
  const factory UserModel({
    required String id,
    required String email,
    required String name,
    required DateTime createdAt,
  }) = _UserModel;

  factory UserModel.fromJson(Map<String, dynamic> json) =>
      _$UserModelFromJson(json);
}

@freezed
class CreateUserRequest with _$CreateUserRequest {
  const factory CreateUserRequest({
    required String email,
    required String name,
    required String password,
  }) = _CreateUserRequest;

  factory CreateUserRequest.fromJson(Map<String, dynamic> json) =>
      _$CreateUserRequestFromJson(json);
}

BLoC — lib/features/users/presentation/bloc/user_list_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

import '../../data/models/user_model.dart';
import '../../domain/repositories/user_repository.dart';

part 'user_list_event.dart';
part 'user_list_state.dart';
part 'user_list_bloc.freezed.dart';

class UserListBloc extends Bloc<UserListEvent, UserListState> {
  final UserRepository _repository;

  UserListBloc(this._repository) : super(const UserListState.initial()) {
    on<UserListEvent>((event, emit) async {
      await event.map(
        fetch: (_) async {
          emit(const UserListState.loading());
          final result = await _repository.getAll();
          result.fold(
            (failure) => emit(UserListState.error(failure.message)),
            (users) => emit(UserListState.loaded(users)),
          );
        },
        refresh: (_) async {
          final result = await _repository.getAll();
          result.fold(
            (failure) => emit(UserListState.error(failure.message)),
            (users) => emit(UserListState.loaded(users)),
          );
        },
      );
    });
  }
}

BLoC Events and States

// user_list_event.dart
part of 'user_list_bloc.dart';

@freezed
class UserListEvent with _$UserListEvent {
  const factory UserListEvent.fetch() = _Fetch;
  const factory UserListEvent.refresh() = _Refresh;
}

// user_list_state.dart
part of 'user_list_bloc.dart';

@freezed
class UserListState with _$UserListState {
  const factory UserListState.initial() = _Initial;
  const factory UserListState.loading() = _Loading;
  const factory UserListState.loaded(List<UserModel> users) = _Loaded;
  const factory UserListState.error(String message) = _Error;
}

Page with BLoC — lib/features/users/presentation/pages/user_list_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

import '../../../../core/di/injection.dart';
import '../bloc/user_list_bloc.dart';
import '../widgets/user_card.dart';

class UserListPage extends StatelessWidget {
  const UserListPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => getIt<UserListBloc>()..add(const UserListEvent.fetch()),
      child: Scaffold(
        appBar: AppBar(title: const Text('Users')),
        body: BlocBuilder<UserListBloc, UserListState>(
          builder: (context, state) => switch (state) {
            _Initial() || _Loading() => const Center(
              child: CircularProgressIndicator(),
            ),
            _Loaded(:final users) => RefreshIndicator(
              onRefresh: () async {
                context.read<UserListBloc>().add(const UserListEvent.refresh());
              },
              child: ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: users.length,
                itemBuilder: (context, index) {
                  final user = users[index];
                  return UserCard(
                    user: user,
                    onTap: () => context.go('/users/${user.id}'),
                  );
                },
              ),
            ),
            _Error(:final message) => Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: $message'),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () => context.read<UserListBloc>().add(
                      const UserListEvent.fetch(),
                    ),
                    child: const Text('Retry'),
                  ),
                ],
              ),
            ),
          },
        ),
      ),
    );
  }
}

Dio API Client — lib/core/network/api_client.dart

import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class ApiClient {
  final Dio dio;
  final FlutterSecureStorage _secureStorage;

  ApiClient({required String baseUrl, required FlutterSecureStorage secureStorage})
      : _secureStorage = secureStorage,
        dio = Dio(BaseOptions(
          baseUrl: baseUrl,
          connectTimeout: const Duration(seconds: 10),
          receiveTimeout: const Duration(seconds: 10),
          headers: {'Content-Type': 'application/json'},
        )) {
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await _secureStorage.read(key: 'auth_token');
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        handler.next(options);
      },
      onError: (error, handler) async {
        if (error.response?.statusCode == 401) {
          await _secureStorage.delete(key: 'auth_token');
          // Navigate to login
        }
        handler.next(error);
      },
    ));
  }
}

Repository — lib/features/users/data/repositories/user_repository_impl.dart

import 'package:dartz/dartz.dart';

import '../../../../core/error/failures.dart';
import '../../../../core/network/api_client.dart';
import '../../domain/repositories/user_repository.dart';
import '../models/user_model.dart';

class UserRepositoryImpl implements UserRepository {
  final ApiClient _apiClient;

  UserRepositoryImpl(this._apiClient);

  @override
  Future<Either<Failure, List<UserModel>>> getAll() async {
    try {
      final response = await _apiClient.dio.get('/users');
      final users = (response.data['data'] as List)
          .map((json) => UserModel.fromJson(json))
          .toList();
      return Right(users);
    } on DioException catch (e) {
      return Left(ServerFailure(e.message ?? 'Server error'));
    }
  }

  @override
  Future<Either<Failure, UserModel>> getById(String id) async {
    try {
      final response = await _apiClient.dio.get('/users/$id');
      return Right(UserModel.fromJson(response.data['data']));
    } on DioException catch (e) {
      return Left(ServerFailure(e.message ?? 'Server error'));
    }
  }
}

Failure Classes — lib/core/error/failures.dart

sealed class Failure {
  final String message;
  const Failure(this.message);
}

class ServerFailure extends Failure {
  const ServerFailure(super.message);
}

class CacheFailure extends Failure {
  const CacheFailure(super.message);
}

class NetworkFailure extends Failure {
  const NetworkFailure(super.message);
}

Widget — lib/features/users/presentation/widgets/user_card.dart

import 'package:flutter/material.dart';

import '../../data/models/user_model.dart';

class UserCard extends StatelessWidget {
  final UserModel user;
  final VoidCallback onTap;

  const UserCard({super.key, required this.user, required this.onTap});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: theme.colorScheme.primaryContainer,
          child: Text(
            user.name[0].toUpperCase(),
            style: TextStyle(color: theme.colorScheme.onPrimaryContainer),
          ),
        ),
        title: Text(user.name),
        subtitle: Text(user.email),
        trailing: const Icon(Icons.chevron_right),
        onTap: onTap,
      ),
    );
  }
}

Platform Channel — lib/core/platform/native_bridge.dart

import 'package:flutter/services.dart';

class NativeBridge {
  static const _channel = MethodChannel('com.example.app/native');

  static Future<String> getBatteryLevel() async {
    final level = await _channel.invokeMethod<int>('getBatteryLevel');
    return '${level ?? -1}%';
  }

  static Future<bool> isBiometricAvailable() async {
    return await _channel.invokeMethod<bool>('isBiometricAvailable') ?? false;
  }
}

BLoC Test — test/features/users/presentation/bloc/user_list_bloc_test.dart

import 'package:bloc_test/bloc_test.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late UserListBloc bloc;
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
    bloc = UserListBloc(mockRepository);
  });

  tearDown(() => bloc.close());

  final testUsers = [
    const UserModel(id: '1', email: 'a@test.com', name: 'Alice', createdAt: DateTime(2024)),
  ];

  blocTest<UserListBloc, UserListState>(
    'emits [loading, loaded] when fetch is successful',
    build: () {
      when(() => mockRepository.getAll())
          .thenAnswer((_) async => Right(testUsers));
      return bloc;
    },
    act: (bloc) => bloc.add(const UserListEvent.fetch()),
    expect: () => [
      const UserListState.loading(),
      UserListState.loaded(testUsers),
    ],
  );

  blocTest<UserListBloc, UserListState>(
    'emits [loading, error] when fetch fails',
    build: () {
      when(() => mockRepository.getAll())
          .thenAnswer((_) async => const Left(ServerFailure('Network error')));
      return bloc;
    },
    act: (bloc) => bloc.add(const UserListEvent.fetch()),
    expect: () => [
      const UserListState.loading(),
      const UserListState.error('Network error'),
    ],
  );
}

Common Commands

# Run on connected device/emulator
flutter run

# Run on specific device
flutter run -d chrome          # Web
flutter run -d <device-id>     # Specific device

# Hot reload (in running session)
# Press 'r' in terminal or save file in IDE

# Build
flutter build apk              # Android APK
flutter build appbundle         # Android AAB (Play Store)
flutter build ios               # iOS
flutter build web               # Web

# Test
flutter test
flutter test test/features/users/

# Code generation (freezed, json_serializable, injectable)
dart run build_runner build --delete-conflicting-outputs
dart run build_runner watch     # Watch mode

# Analyze
flutter analyze

# Format
dart format .

# Clean
flutter clean && flutter pub get

# Check outdated packages
flutter pub outdated

# Generate launcher icons
flutter pub add --dev flutter_launcher_icons
flutter pub run flutter_launcher_icons

Integration Notes

  • State Management: BLoC for event-driven state with clear separation. Riverpod for a more functional, provider-based approach. Both support code generation. Do not mix — pick one per project.
  • Navigation: GoRouter for declarative routing with deep linking, path parameters, guards (redirects), and shell routes for persistent layouts (bottom nav). Use context.go() for navigation, context.push() for stack navigation.
  • Local Storage: Hive for fast key-value storage (settings, cached data). Isar for full NoSQL database with queries. Use flutter_secure_storage for tokens and secrets (Keychain/Keystore).
  • Code Generation: Freezed + json_serializable for immutable models with JSON support. Run dart run build_runner build after changing model files. Use watch mode during development.
  • HTTP: Dio for HTTP with interceptors, cancellation, form data, file uploads. Interceptors handle auth token injection and 401 responses globally.
  • Platform Channels: Use MethodChannel for one-off native calls. Use EventChannel for streams from native (sensors, Bluetooth). Platform-specific code in android/ and ios/ directories.
  • Testing: flutter_test for widget tests, bloc_test for BLoC testing, mocktail for mocking. Integration tests in integration_test/ directory using IntegrationTestWidgetsFlutterBinding.
  • CI/CD: Use codemagic.yaml or GitHub Actions with subosito/flutter-action. Fastlane for iOS/Android store deployment.
  • Firebase: For authentication, Firestore, push notifications, and analytics, add the Firebase SDK: firebase_core + firebase_auth + cloud_firestore via FlutterFire CLI (flutterfire configure).
  • Offline-First: For offline-first, use Hive or Isar for local persistence, with connectivity_plus for network detection. Isar supports full-text search and complex queries offline.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.