flutter_clean_mvvm_toolkit 0.2.1
flutter_clean_mvvm_toolkit: ^0.2.1 copied to clipboard
A comprehensive Flutter toolkit for Clean Architecture with MVVM pattern. Provides ViewModels, Use Cases, Entities, Error Handling, and CRUD patterns for scalable Flutter applications.
Flutter Clean MVVM Toolkit ποΈ #
A comprehensive Flutter toolkit for implementing Clean Architecture with MVVM pattern. Provides foundational components and best practices for building scalable, maintainable, and testable Flutter applications.
β¨ Features #
ποΈ Clean Architecture Components #
- Entity: Base class with Equatable for value comparison
- UseCase: Abstract class for Future-based business logic
- StreamUseCase: Abstract class for reactive business logic
- Model: Base class for Data Transfer Objects (DTOs)
π MVVM ViewModels #
- EntityFormViewModel: Manages form fields, validation, and data transformation
- CrudViewModel: Handles CRUD operations with Use Cases
- DefaultFormViewModel: Base form state management with FormKey
- OperationResultMixin: Success/failure state handling
π₯ Error Handling System #
- ErrorItem: Structured error representation with severity levels
- ErrorCode: Categorized error types (network, validation, unauthorized, etc.)
- ErrorLevelEnum: Severity levels (systemInfo, warning, severe, danger)
β Validation System #
- FormValidators: UI validators returning
String?for Flutter widgets - Validators: Business logic validators returning
ErrorItem?for use cases - Common validations: email, phone, URL, password strength, numeric, alphabetic, date ranges, and more
π οΈ Utilities #
- DataUtils: Safe JSON parsing helpers
- FormType: Enum for form modes (create, edit, read)
- DefaultEntityForm: Base widget for entity forms
π¦ Installation #
Add this to your package's pubspec.yaml file:
dependencies:
flutter_clean_mvvm_toolkit: ^0.2.0
Then run:
flutter pub get
π Quick Start #
1. Define your Entity #
import 'package:flutter_clean_mvvm_toolkit/flutter_clean_mvvm_toolkit.dart';
class Patient extends Entity {
@override
final String? id;
final String name;
final int age;
final String email;
Patient({
this.id,
required this.name,
required this.age,
required this.email,
});
@override
List<Object?> get props => [id, name, age, email];
@override
Patient copyWith({String? id, String? name, int? age, String? email}) {
return Patient(
id: id ?? this.id,
name: name ?? this.name,
age: age ?? this.age,
email: email ?? this.email,
);
}
@override
String toString() => 'Patient(id: $id, name: $name, age: $age, email: $email)';
}
2. Create your Use Cases #
class CreatePatientUseCase extends UseCase<Patient, Patient> {
final PatientRepository repository;
CreatePatientUseCase(this.repository);
@override
Future<Either<ErrorItem, Patient>> call(Patient params) async {
// Validaciones de negocio
if (params.age < 18) {
return Left(ErrorItem.validation(
message: 'El paciente debe ser mayor de edad',
));
}
return await repository.create(params);
}
}
class GetPatientsUseCase extends UseCase<List<Patient>, NoParams> {
final PatientRepository repository;
GetPatientsUseCase(this.repository);
@override
Future<Either<ErrorItem, List<Patient>>> call(NoParams params) async {
return await repository.getAll();
}
}
3. Create Form ViewModel #
class PatientFormViewModel extends EntityFormViewModel<Patient> {
final nameController = TextEditingController();
final ageController = TextEditingController();
final emailController = TextEditingController();
PatientFormViewModel() {
createFormState(); // Inicializa el FormKey
}
@override
void loadDataFromEntity(Patient entity) {
nameController.text = entity.name;
ageController.text = entity.age.toString();
emailController.text = entity.email;
formType = FormType.edit;
}
@override
Patient buildEntityFromForm() {
return Patient(
name: nameController.text,
age: int.parse(ageController.text),
email: emailController.text,
);
}
@override
void clearFormData() {
nameController.clear();
ageController.clear();
emailController.clear();
formType = FormType.create;
}
@override
void dispose() {
nameController.dispose();
ageController.dispose();
emailController.dispose();
super.dispose();
}
}
4. Create CRUD ViewModel #
class PatientCrudViewModel extends CrudViewModel<Patient> {
final CreatePatientUseCase _createUseCase;
final GetPatientsUseCase _getPatientsUseCase;
final UpdatePatientUseCase _updateUseCase;
final DeletePatientUseCase _deleteUseCase;
List<Patient> _patients = [];
List<Patient> get patients => _patients;
PatientCrudViewModel(
this._createUseCase,
this._getPatientsUseCase,
this._updateUseCase,
this._deleteUseCase,
) {
getEntities();
}
@override
Future<OperationResult<Patient>> addEntity(Patient entity) async {
final result = await _createUseCase.call(entity);
return result.fold(
(error) => OperationResult.failure(error),
(patient) {
getEntities();
return OperationResult.success(patient);
},
);
}
@override
Future<OperationResult<Patient>> getEntity(String id) async {
final result = await _getPatientUseCase.call(id);
return result.fold(
(error) => OperationResult.failure(error),
(patient) => OperationResult.success(patient),
);
}
@override
Future<OperationResult<Patient>> updateEntity(Patient entity) async {
final result = await _updateUseCase.call(entity);
return result.fold(
(error) => OperationResult.failure(error),
(patient) {
getEntities();
return OperationResult.success(patient);
},
);
}
@override
Future<OperationResult<void>> deleteEntity(String id) async {
final result = await _deleteUseCase.call(id);
return result.fold(
(error) => OperationResult.failure(error),
(success) {
getEntities();
return OperationResult.success(null);
},
);
}
@override
Future<void> getEntities() async {
final result = await _getPatientsUseCase.call(NoParams());
result.fold(
(error) {
// Manejar error
},
(patients) {
_patients = patients;
notifyListeners();
},
);
}
}
5. Build your UI #
class PatientFormScreen extends StatelessWidget {
final PatientFormViewModel formViewModel;
final PatientCrudViewModel crudViewModel;
const PatientFormScreen({
required this.formViewModel,
required this.crudViewModel,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Nuevo Paciente')),
body: Form(
key: formViewModel.formState,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: formViewModel.nameController,
decoration: InputDecoration(labelText: 'Nombre'),
validator: (value) => FormValidators.validateNoEmpty(value, 'el nombre'),
),
TextFormField(
controller: formViewModel.ageController,
decoration: InputDecoration(labelText: 'Edad'),
keyboardType: TextInputType.number,
validator: (value) => FormValidators.validateIsNumeric(value, 'la edad'),
),
TextFormField(
controller: formViewModel.emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) => FormValidators.validateEmail(value),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
// El FormViewModel valida automΓ‘ticamente
final patient = formViewModel.mapDataToEntity();
if (patient != null) {
final result = formViewModel.formType == FormType.create
? await crudViewModel.addEntity(patient)
: await crudViewModel.updateEntity(patient);
if (result.isSuccess) {
formViewModel.clearFormData();
Navigator.pop(context);
}
}
},
child: Text(formViewModel.formType == FormType.create ? 'Crear' : 'Actualizar'),
),
],
),
),
),
);
}
}
π Core Concepts #
ViewModels Architecture #
This toolkit uses a decoupled ViewModel architecture:
EntityFormViewModel
- Responsibility: Manages form fields, validation, and data transformation
- Does NOT: Execute CRUD operations or communicate with Use Cases
- Methods:
loadDataFromEntity(entity): Populates form fields from entitymapDataToEntity(): Creates entity from form (validates first, returnsnullif invalid)buildEntityFromForm(): Abstract method to implement entity constructionclearFormData(): Clears all form fields
CrudViewModel
- Responsibility: Executes CRUD operations via Use Cases
- Does NOT: Know about form fields or controllers
- Methods:
addEntity(entity): Create operation βFuture<OperationResult<T>>getEntity(id): Read operation βFuture<OperationResult<T>>updateEntity(entity): Update operation βFuture<OperationResult<T>>deleteEntity(id): Delete operation βFuture<OperationResult<void>>getEntities(): List operation βFuture<void>
Widget Coordination
The Widget acts as the mediator between both ViewModels:
// Widget coordinates the communication
final patient = formViewModel.mapDataToEntity(); // Validates & builds entity
if (patient != null) {
final result = await crudViewModel.addEntity(patient); // Executes operation
if (result.isSuccess) formViewModel.clearFormData(); // Cleans up
}
Validation System #
FormValidators (UI Layer)
Returns String? for direct use in Flutter validators:
TextFormField(
validator: (value) => FormValidators.validateEmail(value),
)
Available validators:
validateNoEmpty(value, fieldName)validateEmail(value)validateMinLength(value, minLength, fieldName)validateIsNumeric(value, fieldName)validateMatch(value, other, message)validatePhone(value, fieldName)validateUrl(value, fieldName)validatePasswordStrength(value, fieldName)
Validators (Business Logic Layer)
Returns ErrorItem? for use in Use Cases:
final error = Validators.validateEmail(email);
if (error != null) {
return Left(error); // Return Either with error
}
Available validators:
- All from FormValidators, plus:
validateNotNull(value, fieldName)validateModelNotNull(model)validateMaxLength(value, maxLength, fieldName)validateRange(value, min, max, fieldName)validateStringLengthRange(value, minLength, maxLength, fieldName)validateAlphabetic(value, fieldName)validateNotFutureDate(date, fieldName)
Error Handling #
class GetPatientUseCase extends UseCase<Patient, String> {
@override
Future<Either<ErrorItem, Patient>> call(String id) async {
try {
final patient = await repository.getById(id);
return Right(patient);
} catch (e) {
return Left(ErrorItem.network(
message: 'No se pudo obtener el paciente',
));
}
}
}
Error types:
ErrorItem.validation(): Form/data validation errorsErrorItem.network(): Network connectivity errorsErrorItem.unauthorized(): Authentication errorsErrorItem.unknown(): Generic errors
π― Best Practices #
- Separate Concerns: Keep FormViewModel for UI and CrudViewModel for business logic
- Validate Early: Use FormValidators in UI, Validators in Use Cases
- Use Either: Always return
Either<ErrorItem, T>from Use Cases - Coordinate in Widget: Let the Widget mediate between ViewModels
- Dispose Properly: Always dispose controllers in FormViewModel
- Test Independently: Each ViewModel should be testable without the other
π Documentation #
For complete API documentation, visit pub.flutter-io.cn documentation.
π€ Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π¨βπ» Author #
Edwin Martinez
- GitHub: @Edwin-sh
π Acknowledgments #
Inspired by Clean Architecture principles and MVVM pattern best practices in Flutter development.