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
57%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./mobile/flutter-project-starter/SKILL.mdScaffold 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-outputsflutter pub add flutter_riverpod riverpod_annotation
flutter pub add --dev riverpod_generator custom_lint riverpod_lintlib/
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.dartcopyWith, ==, 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: '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'),
],
);
}# 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_iconscontext.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.181fcbc
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.