mobile/flutter-project-starter/SKILL.md
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.
npx skillsauth add achreftlili/deep-dev-skills flutter-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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
flutter pub add flutter_riverpod riverpod_annotation
flutter pub add --dev riverpod_generator custom_lint riverpod_lint
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
copyWith, ==, toString, JSON serializationColorScheme.fromSeed() for consistent theminglib/main.dartimport '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());
}
lib/app/app.dartimport '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,
);
}
}
lib/app/theme.dartimport '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,
),
);
}
lib/app/router.dartimport '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);
},
),
],
),
],
);
lib/features/users/data/models/user_model.dartimport '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);
}
lib/features/users/presentation/bloc/user_list_bloc.dartimport '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)),
);
},
);
});
}
}
// 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;
}
lib/features/users/presentation/pages/user_list_page.dartimport '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'),
),
],
),
),
},
),
),
);
}
}
lib/core/network/api_client.dartimport '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);
},
));
}
}
lib/features/users/data/repositories/user_repository_impl.dartimport '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'));
}
}
}
lib/core/error/failures.dartsealed 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);
}
lib/features/users/presentation/widgets/user_card.dartimport '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,
),
);
}
}
lib/core/platform/native_bridge.dartimport '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;
}
}
test/features/users/presentation/bloc/user_list_bloc_test.dartimport '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: '[email protected]', 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'),
],
);
}
# 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
context.go() for navigation, context.push() for stack navigation.flutter_secure_storage for tokens and secrets (Keychain/Keystore).dart run build_runner build after changing model files. Use watch mode during development.MethodChannel for one-off native calls. Use EventChannel for streams from native (sensors, Bluetooth). Platform-specific code in android/ and ios/ directories.flutter_test for widget tests, bloc_test for BLoC testing, mocktail for mocking. Integration tests in integration_test/ directory using IntegrationTestWidgetsFlutterBinding.codemagic.yaml or GitHub Actions with subosito/flutter-action. Fastlane for iOS/Android store deployment.firebase_core + firebase_auth + cloud_firestore via FlutterFire CLI (flutterfire configure).connectivity_plus for network detection. Isar supports full-text search and complex queries offline.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.