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 করুন!