স্কিপ করে মূল কন্টেন্ট এ যান

Development Workflow

Clean Architecture pattern ব্যবহার করে Flutter development-এর জন্য একটি systematic workflow যা code quality এবং maintainability ensure করে।

🎯 Feature Development Flow

1. 📋 Planning Phase

Requirements Analysis

Feature: User Authentication
- User login করতে পারবে
- User registration করতে পারবে
- Session management থাকবে
- Password reset functionality থাকবে

Architecture Planning

Entities:
- User (id, email, name, token)
- AuthStatus (isLoggedIn, user)

Use Cases:
- LoginUseCase
- RegisterUseCase
- LogoutUseCase
- GetCurrentUserUseCase

Data Sources:
- AuthRemoteDataSource (API calls)
- AuthLocalDataSource (Token storage)

2. 🏗 Domain Layer First

Step 1: Create Entities

flx gen feature auth

auth_entity.dart তৈরি করুন:


class AuthEntity with _$AuthEntity {
const factory AuthEntity({
required String id,
required String email,
required String name,
required String token,
}) = _AuthEntity;
}

Step 2: Define Repository Interface

auth_repository.dart তৈরি করুন:

abstract class AuthRepository {
Future<AuthEntity> login(String email, String password);
Future<AuthEntity> register(String name, String email, String password);
Future<void> logout();
Future<bool> isLoggedIn();
Future<AuthEntity?> getCurrentUser();
}

Step 3: Create Use Cases

login_usecase.dart:

class LoginUseCase {
final AuthRepository _repository;

LoginUseCase(this._repository);

Future<AuthEntity> call(String email, String password) async {
// Validation
if (email.isEmpty || !email.contains('@')) {
throw Exception('Valid email is required');
}

if (password.length < 6) {
throw Exception('Password must be at least 6 characters');
}

return await _repository.login(email, password);
}
}

3. 📊 Data Layer Implementation

Step 1: Create Models

auth_model.dart:


class AuthModel with _$AuthModel {
const factory AuthModel({
required String id,
required String email,
required String name,
required String token,
(name: 'created_at') String? createdAt,
}) = _AuthModel;

factory AuthModel.fromJson(Map<String, dynamic> json) =>
_$AuthModelFromJson(json);
}

extension AuthModelX on AuthModel {
AuthEntity toEntity() {
return AuthEntity(
id: id,
email: email,
name: name,
token: token,
);
}
}

Step 2: Implement Data Sources

auth_remote_data_source.dart:

abstract class AuthRemoteDataSource {
Future<AuthModel> login(String email, String password);
Future<AuthModel> register(String name, String email, String password);
Future<void> logout();
}

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final Dio _dio;

AuthRemoteDataSourceImpl(this._dio);


Future<AuthModel> login(String email, String password) async {
final response = await _dio.post('/auth/login', data: {
'email': email,
'password': password,
});

if (response.statusCode == 200) {
return AuthModel.fromJson(response.data);
} else {
throw Exception('Login failed');
}
}
}

auth_local_data_source.dart:

abstract class AuthLocalDataSource {
Future<void> saveToken(String token);
Future<String?> getToken();
Future<void> clearToken();
Future<void> saveUser(AuthModel user);
Future<AuthModel?> getUser();
}

class AuthLocalDataSourceImpl implements AuthLocalDataSource {
final SharedPreferences _prefs;

AuthLocalDataSourceImpl(this._prefs);


Future<void> saveToken(String token) async {
await _prefs.setString('auth_token', token);
}


Future<String?> getToken() async {
return _prefs.getString('auth_token');
}
}

Step 3: Implement Repository

auth_repository_impl.dart:

class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource _remoteDataSource;
final AuthLocalDataSource _localDataSource;

AuthRepositoryImpl(this._remoteDataSource, this._localDataSource);


Future<AuthEntity> login(String email, String password) async {
final authModel = await _remoteDataSource.login(email, password);

// Save token locally
await _localDataSource.saveToken(authModel.token);
await _localDataSource.saveUser(authModel);

return authModel.toEntity();
}


Future<bool> isLoggedIn() async {
final token = await _localDataSource.getToken();
return token != null && token.isNotEmpty;
}
}

4. 🖥 Presentation Layer

Step 1: Create BLoC/Controller

GetX Controller:

class AuthController extends GetxController {
final LoginUseCase _loginUseCase;
final LogoutUseCase _logoutUseCase;

AuthController(this._loginUseCase, this._logoutUseCase);

final _isLoading = false.obs;
final _user = Rx<AuthEntity?>(null);

bool get isLoading => _isLoading.value;
AuthEntity? get user => _user.value;
bool get isLoggedIn => _user.value != null;

Future<void> login(String email, String password) async {
try {
_isLoading.value = true;
final user = await _loginUseCase(email, password);
_user.value = user;
Get.offAllNamed('/dashboard');
} catch (e) {
Get.snackbar('Error', e.toString());
} finally {
_isLoading.value = false;
}
}
}

Step 2: Create UI Pages

login_page.dart:

class LoginPage extends GetView<AuthController> {
const LoginPage({Key? key}) : super(key: key);


Widget build(BuildContext context) {
final emailController = TextEditingController();
final passwordController = TextEditingController();

return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: emailController,
decoration: InputDecoration(labelText: 'Email'),
),
TextField(
controller: passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
SizedBox(height: 20),
Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : () {
controller.login(
emailController.text,
passwordController.text,
);
},
child: controller.isLoading
? CircularProgressIndicator()
: Text('Login'),
)),
],
),
),
);
}
}

Step 3: Setup Dependency Injection

auth_binding.dart:

class AuthBinding extends Bindings {

void dependencies() {
// Data Sources
Get.lazyPut<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(Get.find()),
);

Get.lazyPut<AuthLocalDataSource>(
() => AuthLocalDataSourceImpl(Get.find()),
);

// Repository
Get.lazyPut<AuthRepository>(
() => AuthRepositoryImpl(Get.find(), Get.find()),
);

// Use Cases
Get.lazyPut<LoginUseCase>(
() => LoginUseCase(Get.find()),
);

Get.lazyPut<LogoutUseCase>(
() => LogoutUseCase(Get.find()),
);

// Controller
Get.lazyPut<AuthController>(
() => AuthController(Get.find(), Get.find()),
);
}
}

🧪 Testing Strategy

1. Unit Tests (Domain Layer)

// test/domain/usecases/login_usecase_test.dart
void main() {
late LoginUseCase usecase;
late MockAuthRepository mockRepository;

setUp(() {
mockRepository = MockAuthRepository();
usecase = LoginUseCase(mockRepository);
});

group('LoginUseCase', () {
test('should return AuthEntity when login is successful', () async {
// arrange
when(mockRepository.login(any, any))
.thenAnswer((_) async => tAuthEntity);

// act
final result = await usecase('test@email.com', 'password');

// assert
expect(result, tAuthEntity);
verify(mockRepository.login('test@email.com', 'password'));
});

test('should throw exception when email is invalid', () async {
// act & assert
expect(
() => usecase('invalid-email', 'password'),
throwsException,
);
});
});
}

2. Integration Tests (Data Layer)

// test/data/repositories/auth_repository_impl_test.dart
void main() {
late AuthRepositoryImpl repository;
late MockAuthRemoteDataSource mockRemoteDataSource;
late MockAuthLocalDataSource mockLocalDataSource;

setUp(() {
mockRemoteDataSource = MockAuthRemoteDataSource();
mockLocalDataSource = MockAuthLocalDataSource();
repository = AuthRepositoryImpl(mockRemoteDataSource, mockLocalDataSource);
});

group('login', () {
test('should return AuthEntity when login is successful', () async {
// arrange
when(mockRemoteDataSource.login(any, any))
.thenAnswer((_) async => tAuthModel);

// act
final result = await repository.login('email', 'password');

// assert
expect(result, tAuthEntity);
verify(mockLocalDataSource.saveToken(tAuthModel.token));
});
});
}

3. Widget Tests (Presentation Layer)

// test/presentation/pages/login_page_test.dart
void main() {
testWidgets('should show login form', (WidgetTester tester) async {
// arrange
await tester.pumpWidget(MaterialApp(home: LoginPage()));

// assert
expect(find.text('Email'), findsOneWidget);
expect(find.text('Password'), findsOneWidget);
expect(find.text('Login'), findsOneWidget);
});
}

🔄 Git Workflow

1. Feature Branch Strategy

# Create feature branch
git checkout -b feature/auth-implementation

# Generate feature using FLX CLI
flx gen feature auth

# Commit generated structure
git add .
git commit -m "feat: generate auth feature structure"

# Implement domain layer
git add lib/features/auth/domain/
git commit -m "feat: implement auth domain layer"

# Implement data layer
git add lib/features/auth/data/
git commit -m "feat: implement auth data layer"

# Implement presentation layer
git add lib/features/auth/presentation/
git commit -m "feat: implement auth presentation layer"

# Create pull request
git push origin feature/auth-implementation

2. Commit Message Convention

feat: add new feature
fix: bug fix
docs: documentation changes
style: formatting changes
refactor: code refactoring
test: add tests
chore: maintenance tasks

📋 Code Review Checklist

✅ Domain Layer

  • Entities contain only business data
  • Use cases have single responsibility
  • Repository interfaces are abstract
  • No external dependencies

✅ Data Layer

  • Models have JSON serialization
  • Data sources handle external communication
  • Repository implementations handle errors
  • Model to Entity mapping is correct

✅ Presentation Layer

  • Controllers/BLoC handle state correctly
  • Pages are stateless when possible
  • Dependency injection is properly setup
  • Error handling is implemented

✅ General

  • Naming conventions are followed
  • Tests are written
  • Documentation is updated
  • No layer violations

💡 Pro Tip: FLX CLI-র generated structure follow করলে এই workflow অনেক সহজ হয়ে যায়। Generated files modify করে আপনার requirements অনুযায়ী customize করুন!