A comprehensive development guide for building modern mobile applications using Flutter, Dart, Riverpod, Freezed, Flutter Hooks, and Supabase with best practices and performance optimization
This agents.md example showcases industry best practices for AI agent instruction. The agents.md example provides a comprehensive template that you can adapt for your specific project requirements.
Every element in this agents.md example has been carefully designed to optimize OpenAI Codex performance and ensure consistent AI agent behavior across your development workflow.
To use this agents.md example in your project, download the template and customize it according to your specific needs. This agents.md example serves as a solid foundation for your AI agent configuration.
Study the structure and conventions used in this agents.md example to understand how successful projects implement AI agent instruction for optimal results.
This comprehensive guide outlines best practices for developing modern mobile applications using Flutter, Dart, Riverpod for state management, Freezed for immutable data classes, Flutter Hooks for lifecycle management, and Supabase for backend services. The guide emphasizes functional and declarative programming patterns, performance optimization, and maintainable code architecture.
# Install Flutter dependencies
flutter pub add riverpod flutter_riverpod riverpod_annotation
flutter pub add freezed_annotation json_annotation
flutter pub add flutter_hooks hooks_riverpod
flutter pub add supabase_flutter
flutter pub add go_router
flutter pub add cached_network_image
flutter pub add dio
# Development dependencies
flutter pub add --dev build_runner
flutter pub add --dev riverpod_generator
flutter pub add --dev freezed
flutter pub add --dev json_serializable
flutter pub add --dev flutter_test
flutter pub add --dev mockito
# Generate code
flutter pub run build_runner build --delete-conflicting-outputs
flutter_app/
├── lib/
│ ├── main.dart # App entry point
│ ├── app.dart # App configuration
│ ├── core/ # Core utilities
│ │ ├── constants/
│ │ ├── extensions/
│ │ ├── utils/
│ │ └── theme/
│ ├── features/ # Feature modules
│ │ ├── auth/
│ │ │ ├── data/ # Data layer
│ │ │ ├── domain/ # Domain layer
│ │ │ └── presentation/ # UI layer
│ │ ├── home/
│ │ └── profile/
│ ├── shared/ # Shared components
│ │ ├── widgets/
│ │ ├── models/
│ │ └── providers/
│ └── services/ # External services
│ ├── supabase_service.dart
│ ├── storage_service.dart
│ └── api_service.dart
├── test/ # Test files
├── assets/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── icons/
├── pubspec.yaml
└── analysis_options.yaml
// Use descriptive variable names with auxiliary verbs
bool isLoading = false;
bool hasError = false;
bool canSubmit = true;
// Use const constructors for immutable widgets
class CustomButton extends StatelessWidget {
const CustomButton({
super.key,
required this.onPressed,
required this.text,
});
final VoidCallback onPressed;
final String text;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text(text),
);
}
}
// Use arrow syntax for simple functions
String get fullName => '$firstName $lastName';
bool get isValid => email.isNotEmpty && password.length >= 6;
// Use trailing commas for better formatting
Widget buildCard() {
return Card(
elevation: 4,
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title'),
const SizedBox(height: 8),
Text('Content'),
],
),
),
);
}
// user_profile_screen.dart - Proper file structure
// 1. Exported widget
class UserProfileScreen extends HookConsumerWidget {
const UserProfileScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: const _ProfileContent(),
);
}
}
// 2. Subwidgets (private)
class _ProfileContent extends ConsumerWidget {
const _ProfileContent();
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(currentUserProvider);
return userAsync.when(
data: (user) => _UserDetails(user: user),
loading: () => const _LoadingWidget(),
error: (error, stack) => _ErrorWidget(error: error),
);
}
}
class _UserDetails extends StatelessWidget {
const _UserDetails({required this.user});
final User user;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(user.name),
Text(user.email),
],
);
}
}
class _LoadingWidget extends StatelessWidget {
const _LoadingWidget();
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}
class _ErrorWidget extends StatelessWidget {
const _ErrorWidget({required this.error});
final Object error;
@override
Widget build(BuildContext context) {
return Center(
child: SelectableText.rich(
TextSpan(
text: 'Error: ${error.toString()}',
style: TextStyle(color: Colors.red),
),
),
);
}
}
// 3. Helpers and utilities
extension UserProfileHelpers on User {
String get displayName => name.isEmpty ? email : name;
bool get hasProfileImage => profileImageUrl?.isNotEmpty ?? false;
}
// 4. Static content and constants
class _Constants {
static const double profileImageSize = 120;
static const EdgeInsets contentPadding = EdgeInsets.all(16);
}
// 5. Types and models (if specific to this file)
enum ProfileTab { info, settings, security }
// providers/user_provider.dart - Modern Riverpod patterns
// Use @riverpod annotation for generating providers
@riverpod
class CurrentUser extends _$CurrentUser {
@override
Future<User?> build() async {
// Initialize with current user from Supabase
final supabase = ref.read(supabaseProvider);
final session = supabase.auth.currentSession;
if (session?.user == null) return null;
return await _fetchUserProfile(session!.user.id);
}
Future<void> updateProfile(UserUpdateRequest request) async {
state = const AsyncValue.loading();
try {
final updatedUser = await ref
.read(userRepositoryProvider)
.updateProfile(request);
state = AsyncValue.data(updatedUser);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
Future<void> signOut() async {
await ref.read(supabaseProvider).auth.signOut();
ref.invalidate(currentUserProvider);
}
Future<User> _fetchUserProfile(String userId) async {
return await ref.read(userRepositoryProvider).getUser(userId);
}
}
// Prefer AsyncNotifierProvider over StateProvider
@riverpod
class UserList extends _$UserList {
@override
Future<List<User>> build() async {
return await ref.read(userRepositoryProvider).getAllUsers();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() =>
ref.read(userRepositoryProvider).getAllUsers());
}
Future<void> addUser(User user) async {
final currentList = state.valueOrNull ?? [];
state = AsyncValue.data([...currentList, user]);
try {
await ref.read(userRepositoryProvider).createUser(user);
} catch (error) {
// Revert optimistic update
state = AsyncValue.data(currentList);
rethrow;
}
}
}
// Simple state provider for UI state
@riverpod
class SelectedTab extends _$SelectedTab {
@override
int build() => 0;
void selectTab(int index) => state = index;
}
// models/user.dart - Immutable data classes with Freezed
@freezed
class User with _$User {
const factory User({
required String id,
required String email,
required String name,
String? profileImageUrl,
@JsonKey(name: 'created_at') required DateTime createdAt,
@JsonKey(name: 'updated_at') required DateTime updatedAt,
@JsonKey(name: 'is_deleted', includeFromJson: true, includeToJson: false)
@Default(false) bool isDeleted,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
@freezed
class UserUpdateRequest with _$UserUpdateRequest {
const factory UserUpdateRequest({
String? name,
String? profileImageUrl,
}) = _UserUpdateRequest;
factory UserUpdateRequest.fromJson(Map<String, dynamic> json) =>
_$UserUpdateRequestFromJson(json);
}
// Union types for handling different states
@freezed
class AuthState with _$AuthState {
const factory AuthState.initial() = AuthInitial;
const factory AuthState.loading() = AuthLoading;
const factory AuthState.authenticated(User user) = AuthAuthenticated;
const factory AuthState.unauthenticated() = AuthUnauthenticated;
const factory AuthState.error(String message) = AuthError;
}
// Enums with database values
enum UserRole {
@JsonValue(0) user,
@JsonValue(1) admin,
@JsonValue(2) moderator,
}
// Error handling in UI components
class UserListScreen extends ConsumerWidget {
const UserListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(userListProvider);
return Scaffold(
appBar: AppBar(title: const Text('Users')),
body: usersAsync.when(
data: (users) => users.isEmpty
? const _EmptyState()
: _UserList(users: users),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => _ErrorDisplay(
error: error,
onRetry: () => ref.invalidate(userListProvider),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddUserDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
}
class _ErrorDisplay extends StatelessWidget {
const _ErrorDisplay({
required this.error,
required this.onRetry,
});
final Object error;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SelectableText.rich(
TextSpan(
text: 'Error: ${_getErrorMessage(error)}',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.red,
),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: onRetry,
child: const Text('Retry'),
),
],
),
);
}
String _getErrorMessage(Object error) {
if (error is SupabaseException) {
return error.message;
}
return error.toString();
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 16),
Text(
'No users found',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Add your first user to get started',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
}
// Using Flutter Hooks for lifecycle management
class SearchScreen extends HookConsumerWidget {
const SearchScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Use hooks for local state and lifecycle
final searchController = useTextEditingController();
final focusNode = useFocusNode();
final isSearching = useState(false);
final searchResults = useState<List<User>>([]);
// Debounced search
final debouncedSearchTerm = useDebounced(
searchController.text,
const Duration(milliseconds: 500),
);
// Effect for search
useEffect(() {
if (debouncedSearchTerm.isEmpty) {
searchResults.value = [];
return null;
}
isSearching.value = true;
// Perform search
ref.read(userRepositoryProvider)
.searchUsers(debouncedSearchTerm)
.then((results) {
searchResults.value = results;
isSearching.value = false;
}).catchError((error) {
isSearching.value = false;
// Handle error
});
return null;
}, [debouncedSearchTerm]);
// Auto-focus on mount
useEffect(() {
focusNode.requestFocus();
return null;
}, []);
return Scaffold(
appBar: AppBar(
title: TextField(
controller: searchController,
focusNode: focusNode,
decoration: const InputDecoration(
hintText: 'Search users...',
border: InputBorder.none,
),
textCapitalization: TextCapitalization.words,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
),
),
body: _SearchBody(
isSearching: isSearching.value,
results: searchResults.value,
searchTerm: debouncedSearchTerm,
),
);
}
}
// Custom hook for debouncing
String useDebounced(String value, Duration duration) {
final debouncedValue = useState(value);
useEffect(() {
final timer = Timer(duration, () {
debouncedValue.value = value;
});
return timer.cancel;
}, [value]);
return debouncedValue.value;
}
// services/supabase_service.dart - Supabase integration
@riverpod
SupabaseClient supabase(SupabaseRef ref) {
return Supabase.instance.client;
}
@riverpod
class AuthService extends _$AuthService {
@override
AuthState build() {
final supabase = ref.read(supabaseProvider);
final session = supabase.auth.currentSession;
if (session?.user != null) {
return AuthState.authenticated(
User.fromJson(session!.user.userMetadata ?? {}),
);
}
return const AuthState.unauthenticated();
}
Future<void> signInWithEmail(String email, String password) async {
state = const AuthState.loading();
try {
final response = await ref.read(supabaseProvider).auth.signInWithPassword(
email: email,
password: password,
);
if (response.user != null) {
final user = await _fetchUserProfile(response.user!.id);
state = AuthState.authenticated(user);
}
} on AuthException catch (error) {
state = AuthState.error(error.message);
} catch (error) {
state = AuthState.error('An unexpected error occurred');
}
}
Future<void> signUp(String email, String password, String name) async {
state = const AuthState.loading();
try {
final response = await ref.read(supabaseProvider).auth.signUp(
email: email,
password: password,
data: {'name': name},
);
if (response.user != null) {
// Create user profile
await _createUserProfile(response.user!, name);
final user = await _fetchUserProfile(response.user!.id);
state = AuthState.authenticated(user);
}
} on AuthException catch (error) {
state = AuthState.error(error.message);
} catch (error) {
state = AuthState.error('Failed to create account');
}
}
Future<void> signOut() async {
await ref.read(supabaseProvider).auth.signOut();
state = const AuthState.unauthenticated();
}
Future<User> _fetchUserProfile(String userId) async {
final response = await ref.read(supabaseProvider)
.from('users')
.select()
.eq('id', userId)
.single();
return User.fromJson(response);
}
Future<void> _createUserProfile(auth.User authUser, String name) async {
await ref.read(supabaseProvider).from('users').insert({
'id': authUser.id,
'email': authUser.email,
'name': name,
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
});
}
}
// Optimized list with proper image handling
class UserListView extends StatelessWidget {
const UserListView({
super.key,
required this.users,
});
final List<User> users;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
// Implement refresh logic
},
child: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return _UserListItem(user: user);
},
),
);
}
}
class _UserListItem extends StatelessWidget {
const _UserListItem({required this.user});
final User user;
@override
Widget build(BuildContext context) {
return ListTile(
leading: _UserAvatar(user: user),
title: Text(user.name),
subtitle: Text(user.email),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push('/user/${user.id}'),
);
}
}
class _UserAvatar extends StatelessWidget {
const _UserAvatar({required this.user});
final User user;
@override
Widget build(BuildContext context) {
if (user.profileImageUrl?.isNotEmpty == true) {
return CircleAvatar(
child: CachedNetworkImage(
imageUrl: user.profileImageUrl!,
imageBuilder: (context, imageProvider) => CircleAvatar(
backgroundImage: imageProvider,
),
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => _DefaultAvatar(user: user),
),
);
}
return _DefaultAvatar(user: user);
}
}
class _DefaultAvatar extends StatelessWidget {
const _DefaultAvatar({required this.user});
final User user;
@override
Widget build(BuildContext context) {
return CircleAvatar(
child: Text(
user.name.isNotEmpty ? user.name[0].toUpperCase() : '?',
style: Theme.of(context).textTheme.titleLarge,
),
);
}
}
// Responsive design utilities
class ResponsiveLayout extends StatelessWidget {
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
this.desktop,
});
final Widget mobile;
final Widget? tablet;
final Widget? desktop;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200) {
return desktop ?? tablet ?? mobile;
} else if (constraints.maxWidth >= 800) {
return tablet ?? mobile;
} else {
return mobile;
}
},
);
}
}
// Theme configuration
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
titleMedium: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
titleMedium: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
),
);
}
}
// router/app_router.dart - Navigation setup
@riverpod
GoRouter goRouter(GoRouterRef ref) {
final authState = ref.watch(authServiceProvider);
return GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isAuthenticated = authState.maybeWhen(
authenticated: (_) => true,
orElse: () => false,
);
final isAuthRoute = state.location.startsWith('/auth');
if (!isAuthenticated && !isAuthRoute) {
return '/auth/login';
}
if (isAuthenticated && isAuthRoute) {
return '/';
}
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/auth/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/auth/register',
builder: (context, state) => const RegisterScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: '/user/:id',
builder: (context, state) {
final userId = state.pathParameters['id']!;
return UserDetailScreen(userId: userId);
},
),
],
);
}
This comprehensive guide provides a solid foundation for building scalable, maintainable Flutter applications with modern state management, proper error handling, and performance optimization.