āĻ¸ā§āĻ•āĻŋāĻĒ āĻ•āϰ⧇ āĻŽā§‚āϞ āĻ•āĻ¨ā§āĻŸā§‡āĻ¨ā§āϟ āĻ āϝāĻžāύ

đŸŽ¯ 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 āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āύ:

  1. đŸ§Ē Testing Strategies
  2. 🎨 Advanced UI Components
  3. 🔧 More Commands
  4. 🏗 Project Scaling

Development time: ~30 minutes âąī¸

FLX CLI āĻĻāĻŋāϝāĻŧ⧇ feature development āĻāϤ āϏāĻšāϜ! 🚀