đ¯ Your First Feature
āĻāϞā§āύ FLX CLI āĻĻāĻŋāϝāĻŧā§ āĻāĻāĻāĻŋ complete "User Profile" feature āϤā§āϰāĻŋ āĻāϰāĻŋ! āĻāĻ tutorial-āĻ āĻāĻĒāύāĻŋ āĻļāĻŋāĻāĻŦā§āύ āĻāĻŋāĻāĻžāĻŦā§ āĻāĻāĻāĻŋ real-world feature build āĻāϰāϤ⧠āĻšāϝāĻŧāĨ¤
đ¨ What We'll Buildâ
āĻāĻāĻāĻŋ User Profile feature āϝā§āĻāĻžāύ⧠āĻĨāĻžāĻāĻŦā§:
- â User profile āĻĻā§āĻāĻžāϰ page
- â Profile edit āĻāϰāĻžāϰ functionality
- â Image upload option
- â Clean Architecture implementation
- â Error handling āĻāĻŦāĻ loading states
đ Step-by-Step Tutorialâ
Step 1: Feature Generationâ
# User profile feature āϤā§āϰāĻŋ āĻāϰā§āύ
flx gen feature user_profile
# Output āĻĻā§āĻā§āύ:
# â
Generated lib/features/user_profile/
# â
Created domain layer (entity, repository, usecase)
# â
Created data layer (model, repository_impl, datasource)
# â
Created presentation layer (page, controller, binding)
Step 2: Generated Structure āĻĻā§āĻā§āύâ
lib/features/user_profile/
âââ data/
â âââ models/
â â âââ user_profile_model.dart
â âââ repositories/
â â âââ user_profile_repository_impl.dart
â âââ datasources/
â âââ user_profile_local_data_source.dart
â âââ user_profile_remote_data_source.dart
âââ domain/
â âââ entities/
â â âââ user_profile_entity.dart
â âââ repositories/
â â âââ user_profile_repository.dart
â âââ usecases/
â âââ get_user_profile_usecase.dart
âââ presentation/
âââ pages/
â âââ user_profile_page.dart
âââ controllers/
â âââ user_profile_controller.dart
âââ bindings/
âââ user_profile_binding.dart
Step 3: Entity Customize āĻāϰā§āύâ
lib/features/user_profile/domain/entities/user_profile_entity.dart āĻ āϝāĻžāύ:
import 'package:equatable/equatable.dart';
class UserProfileEntity extends Equatable {
final String id;
final String name;
final String email;
final String? phone;
final String? profileImageUrl;
final String? bio;
final DateTime createdAt;
final DateTime updatedAt;
const UserProfileEntity({
required this.id,
required this.name,
required this.email,
this.phone,
this.profileImageUrl,
this.bio,
required this.createdAt,
required this.updatedAt,
});
List<Object?> get props => [
id,
name,
email,
phone,
profileImageUrl,
bio,
createdAt,
updatedAt,
];
}
Step 4: Model Update āĻāϰā§āύâ
lib/features/user_profile/data/models/user_profile_model.dart update āĻāϰā§āύ:
import '../../domain/entities/user_profile_entity.dart';
class UserProfileModel extends UserProfileEntity {
const UserProfileModel({
required super.id,
required super.name,
required super.email,
super.phone,
super.profileImageUrl,
super.bio,
required super.createdAt,
required super.updatedAt,
});
factory UserProfileModel.fromJson(Map<String, dynamic> json) {
return UserProfileModel(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
phone: json['phone'] as String?,
profileImageUrl: json['profile_image_url'] as String?,
bio: json['bio'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'phone': phone,
'profile_image_url': profileImageUrl,
'bio': bio,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
UserProfileEntity toEntity() {
return UserProfileEntity(
id: id,
name: name,
email: email,
phone: phone,
profileImageUrl: profileImageUrl,
bio: bio,
createdAt: createdAt,
updatedAt: updatedAt,
);
}
factory UserProfileModel.fromEntity(UserProfileEntity entity) {
return UserProfileModel(
id: entity.id,
name: entity.name,
email: entity.email,
phone: entity.phone,
profileImageUrl: entity.profileImageUrl,
bio: entity.bio,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
);
}
}
Step 5: Additional Use Cases āϤā§āϰāĻŋ āĻāϰā§āύâ
# Profile update use case
flx gen usecase update_user_profile --feature=user_profile
# Profile image upload use case
flx gen usecase upload_profile_image --feature=user_profile
Step 6: Controller Enhance āĻāϰā§āύâ
lib/features/user_profile/presentation/controllers/user_profile_controller.dart update āĻāϰā§āύ:
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import '../../domain/entities/user_profile_entity.dart';
import '../../domain/usecases/get_user_profile_usecase.dart';
import '../../domain/usecases/update_user_profile_usecase.dart';
import '../../domain/usecases/upload_profile_image_usecase.dart';
class UserProfileController extends GetxController {
final GetUserProfileUsecase getUserProfileUsecase;
final UpdateUserProfileUsecase updateUserProfileUsecase;
final UploadProfileImageUsecase uploadProfileImageUsecase;
UserProfileController({
required this.getUserProfileUsecase,
required this.updateUserProfileUsecase,
required this.uploadProfileImageUsecase,
});
// Observable variables
var isLoading = false.obs;
var isUpdating = false.obs;
var userProfile = Rxn<UserProfileEntity>();
var errorMessage = ''.obs;
void onInit() {
super.onInit();
loadUserProfile();
}
Future<void> loadUserProfile() async {
try {
isLoading.value = true;
errorMessage.value = '';
final result = await getUserProfileUsecase(const NoParams());
result.fold(
(failure) {
errorMessage.value = failure.message;
Get.snackbar(
'Error',
failure.message,
snackPosition: SnackPosition.BOTTOM,
);
},
(profile) {
userProfile.value = profile;
},
);
} finally {
isLoading.value = false;
}
}
Future<void> updateProfile({
required String name,
String? phone,
String? bio,
}) async {
try {
isUpdating.value = true;
errorMessage.value = '';
final params = UpdateUserProfileParams(
name: name,
phone: phone,
bio: bio,
);
final result = await updateUserProfileUsecase(params);
result.fold(
(failure) {
errorMessage.value = failure.message;
Get.snackbar(
'Error',
failure.message,
snackPosition: SnackPosition.BOTTOM,
);
},
(updatedProfile) {
userProfile.value = updatedProfile;
Get.snackbar(
'Success',
'Profile updated successfully!',
snackPosition: SnackPosition.BOTTOM,
);
Get.back(); // Go back to profile view
},
);
} finally {
isUpdating.value = false;
}
}
Future<void> pickAndUploadImage() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
isUpdating.value = true;
final params = UploadProfileImageParams(imagePath: image.path);
final result = await uploadProfileImageUsecase(params);
result.fold(
(failure) {
errorMessage.value = failure.message;
Get.snackbar('Error', failure.message);
},
(imageUrl) {
// Update profile with new image URL
final updatedProfile = UserProfileEntity(
id: userProfile.value!.id,
name: userProfile.value!.name,
email: userProfile.value!.email,
phone: userProfile.value!.phone,
profileImageUrl: imageUrl,
bio: userProfile.value!.bio,
createdAt: userProfile.value!.createdAt,
updatedAt: DateTime.now(),
);
userProfile.value = updatedProfile;
Get.snackbar('Success', 'Profile image updated!');
},
);
}
} finally {
isUpdating.value = false;
}
}
void goToEditProfile() {
Get.toNamed('/edit-profile');
}
void refreshProfile() {
loadUserProfile();
}
}
Step 7: UI Page Enhance āĻāϰā§āύâ
lib/features/user_profile/presentation/pages/user_profile_page.dart update āĻāϰā§āύ:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/user_profile_controller.dart';
class UserProfilePage extends GetView<UserProfileController> {
const UserProfilePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: controller.goToEditProfile,
),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.userProfile.value == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Failed to load profile'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: controller.refreshProfile,
child: const Text('Retry'),
),
],
),
);
}
final profile = controller.userProfile.value!;
return RefreshIndicator(
onRefresh: controller.loadUserProfile,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildProfileHeader(profile),
const SizedBox(height: 24),
_buildProfileInfo(profile),
const SizedBox(height: 24),
_buildActionButtons(),
],
),
),
);
}),
);
}
Widget _buildProfileHeader(profile) {
return Column(
children: [
Stack(
children: [
CircleAvatar(
radius: 60,
backgroundImage: profile.profileImageUrl != null
? NetworkImage(profile.profileImageUrl!)
: null,
child: profile.profileImageUrl == null
? const Icon(Icons.person, size: 60)
: null,
),
Positioned(
bottom: 0,
right: 0,
child: Obx(() => FloatingActionButton.small(
onPressed: controller.isUpdating.value
? null
: controller.pickAndUploadImage,
child: controller.isUpdating.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.camera_alt, size: 20),
)),
),
],
),
const SizedBox(height: 16),
Text(
profile.name,
style: Get.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
profile.email,
style: Get.textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
),
],
);
}
Widget _buildProfileInfo(profile) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Profile Information',
style: Get.textTheme.titleLarge,
),
const SizedBox(height: 16),
_buildInfoRow('Phone', profile.phone ?? 'Not provided'),
_buildInfoRow('Bio', profile.bio ?? 'No bio available'),
_buildInfoRow(
'Member since',
'${profile.createdAt.day}/${profile.createdAt.month}/${profile.createdAt.year}',
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 16),
),
),
],
),
);
}
Widget _buildActionButtons() {
return Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: controller.goToEditProfile,
icon: const Icon(Icons.edit),
label: const Text('Edit Profile'),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: controller.refreshProfile,
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
),
],
);
}
}
Step 8: Routing Setup āĻāϰā§āύâ
lib/app/routes/app_pages.dart āĻ route āϝā§āĻ āĻāϰā§āύ:
import 'package:get/get.dart';
import '../features/user_profile/presentation/pages/user_profile_page.dart';
import '../features/user_profile/presentation/bindings/user_profile_binding.dart';
class AppPages {
static const initial = '/home';
static final routes = [
GetPage(
name: '/profile',
page: () => const UserProfilePage(),
binding: UserProfileBinding(),
),
// āĻ
āύā§āϝāĻžāύā§āϝ routes...
];
}
Step 9: Test āĻāϰā§āύâ
# Flutter app run āĻāϰā§āύ
flutter run
# āĻ
āĻĨāĻŦāĻž specific device āĻ
flutter run -d chrome # Web
flutter run -d android # Android
đ Congratulations!â
āĻāĻĒāύāĻŋ āϏāĻĢāϞāĻāĻžāĻŦā§ āϤā§āϰāĻŋ āĻāϰā§āĻā§āύ:
- â Complete Clean Architecture feature
- â Domain layer with entity, repository, use cases
- â Data layer with model, repository implementation
- â Presentation layer with page, controller, binding
- â Error handling āĻāĻŦāĻ loading states
- â Image upload functionality
- â Responsive UI design
đ What's Next?â
āĻāϰāĻĒāϰ āĻāĻĒāύāĻŋ explore āĻāϰāϤ⧠āĻĒāĻžāϰā§āύ:
- đ§Ē Testing Strategies
- đ¨ Advanced UI Components
- đ§ More Commands
- đ Project Scaling
Development time: ~30 minutes âąī¸
FLX CLI āĻĻāĻŋāϝāĻŧā§ feature development āĻāϤ āϏāĻšāĻ! đ