| Atributo | Valor | |----------|-------| | **ID** | `flutter-mvvm-pattern` | | **Nivel** | 🟡 Intermedio | | **Versión** | 1.0.0 | | **Keywords** | `mvvm`, `model-view-viewmodel`, `provider`, `changenotifier` |
Scanned 9/8/2026
Install to Claude Code
npx -y skills add chimeranext/flutter-boilerplate-monorepo-template --skill mvvm --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Mvvm?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/chimeranext-mvvm)More formats (shields.io, HTML) on the badges page.
# 🎨 Skill: MVVM Pattern
## 📋 Metadata
| Atributo | Valor |
|----------|-------|
| **ID** | `flutter-mvvm-pattern` |
| **Nivel** | 🟡 Intermedio |
| **Versión** | 1.0.0 |
| **Keywords** | `mvvm`, `model-view-viewmodel`, `provider`, `changenotifier` |
## 🔑 Keywords para Invocación
Usa cualquiera de estos keywords en tus prompts para invocar este skill:
- `mvvm`
- `model-view-viewmodel`
- `provider`
- `changenotifier`
- `@skill:mvvm`
### Ejemplos de Prompts
```
Crea una app de lista de tareas usando mvvm
```
```
Implementa model-view-viewmodel para un módulo de productos
```
```
@skill:mvvm - Genera una app de gestión de usuarios con provider
```
## 📖 Descripción
El patrón MVVM (Model-View-ViewModel) proporciona una separación clara entre la lógica de negocio y la interfaz de usuario, facilitando el testing, mantenimiento y escalabilidad del código.
**⚠️ IMPORTANTE:** Todos los comandos de este skill deben ejecutarse desde la **raíz del proyecto** (donde existe el directorio `mobile/`). El skill incluye verificaciones para asegurar que se está en el directorio correcto antes de ejecutar cualquier comando.
### ✅ Cuándo Usar Este Skill
- Proyectos medianos con lógica de negocio moderada
- Necesitas separación clara entre UI y lógica
- Quieres testear la lógica de presentación fácilmente
- El equipo está familiarizado con reactive programming
- Necesitas gestión de estado reactiva
### ❌ Cuándo NO Usar Este Skill
- Proyectos muy pequeños (usa setState)
- Aplicaciones enterprise muy complejas (considera Clean Architecture)
- Necesitas máxima separación de capas (usa Clean Architecture)
## 🏗️ Estructura del Proyecto
```
lib/
├── core/
│ ├── constants/
│ │ ├── app_constants.dart
│ │ ├── api_constants.dart
│ │ └── string_constants.dart
│ ├── utils/
│ │ ├── validators.dart
│ │ ├── formatters.dart
│ │ └── helpers.dart
│ └── extensions/
│ ├── string_extensions.dart
│ ├── date_extensions.dart
│ └── context_extensions.dart
│
├── models/
│ ├── entities/
│ │ ├── user.dart
│ │ ├── product.dart
│ │ └── order.dart
│ └── dto/
│ ├── user_dto.dart
│ └── api_response.dart
│
├── views/
│ ├── screens/
│ │ ├── home_screen.dart
│ │ ├── detail_screen.dart
│ │ └── settings_screen.dart
│ ├── widgets/
│ │ ├── custom_button.dart
│ │ ├── custom_card.dart
│ │ └── loading_indicator.dart
│ └── dialogs/
│ ├── confirmation_dialog.dart
│ └── error_dialog.dart
│
├── viewmodels/
│ ├── home_viewmodel.dart
│ ├── detail_viewmodel.dart
│ └── providers/
│ └── app_provider.dart
│
├── services/
│ ├── api/
│ │ ├── api_service.dart
│ │ └── http_client.dart
│ ├── storage/
│ │ ├── local_storage.dart
│ │ └── secure_storage.dart
│ └── navigation/
│ └── navigation_service.dart
│
└── main.dart
```
## 🧩 Componentes Principales
### 1. Model (Modelo)
Representa los datos y la lógica de negocio.
```dart
// models/entities/user.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
String? avatar,
DateTime? createdAt,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
```
**Características:**
- Inmutabilidad usando `freezed`
- Serialización JSON automática
- CopyWith para actualizaciones
- Equality por valor
### 2. View (Vista)
Widgets de Flutter que representan la UI.
```dart
// views/screens/user_list_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class UserListScreen extends StatelessWidget {
const UserListScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Usuarios'),
),
body: Consumer<UserViewModel>(
builder: (context, viewModel, child) {
// Estado de carga
if (viewModel.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
// Estado de error
if (viewModel.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(viewModel.error!),
ElevatedButton(
onPressed: viewModel.fetchUsers,
child: const Text('Reintentar'),
),
],
),
);
}
// Estado exitoso
return ListView.builder(
itemCount: viewModel.users.length,
itemBuilder: (context, index) {
final user = viewModel.users[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: user.avatar != null
? NetworkImage(user.avatar!)
: null,
child: user.avatar == null
? Text(user.name[0])
: null,
),
title: Text(user.name),
subtitle: Text(user.email),
onTap: () => _navigateToDetail(context, user),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _navigateToCreate(context),
child: const Icon(Icons.add),
),
);
}
void _navigateToDetail(BuildContext context, User user) {
// Navegación al detalle
}
void _navigateToCreate(BuildContext context) {
// Navegación a creación
}
}
```
**Características:**
- Libre de lógica de negocio
- Observa cambios con `Consumer`
- Maneja múltiples estados (loading, error, success)
- Usa `Selector` para optimizar rebuilds
### 3. ViewModel
Maneja la lógica de presentación y gestiona el estado.
```dart
// viewmodels/user_viewmodel.dart
import 'package:flutter/foundation.dart';
class UserViewModel extends ChangeNotifier {
final UserService _userService;
UserViewModel(this._userService);
// Estado
List<User> _users = [];
List<User> get users => List.unmodifiable(_users);
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _error;
String? get error => _error;
User? _selectedUser;
User? get selectedUser => _selectedUser;
// Acciones
Future<void> fetchUsers() async {
_setLoading(true);
_clearError();
try {
_users = await _userService.getUsers();
notifyListeners();
} catch (e) {
_setError('Error al cargar usuarios: ${e.toString()}');
} finally {
_setLoading(false);
}
}
Future<void> createUser(User user) async {
_setLoading(true);
_clearError();
try {
final createdUser = await _userService.createUser(user);
_users.add(createdUser);
notifyListeners();
} catch (e) {
_setError('Error al crear usuario: ${e.toString()}');
} finally {
_setLoading(false);
}
}
Future<void> updateUser(User user) async {
_setLoading(true);
_clearError();
try {
final updatedUser = await _userService.updateUser(user);
final index = _users.indexWhere((u) => u.id == user.id);
if (index != -1) {
_users[index] = updatedUser;
notifyListeners();
}
} catch (e) {
_setError('Error al actualizar usuario: ${e.toString()}');
} finally {
_setLoading(false);
}
}
Future<void> deleteUser(String userId) async {
_setLoading(true);
_clearError();
try {
await _userService.deleteUser(userId);
_users.removeWhere((u) => u.id == userId);
notifyListeners();
} catch (e) {
_setError('Error al eliminar usuario: ${e.toString()}');
} finally {
_setLoading(false);
}
}
void selectUser(User user) {
_selectedUser = user;
notifyListeners();
}
void clearSelection() {
_selectedUser = null;
notifyListeners();
}
// Helpers privados
void _setLoading(bool value) {
_isLoading = value;
notifyListeners();
}
void _setError(String message) {
_error = message;
notifyListeners();
}
void _clearError() {
_error = null;
}
@override
void dispose() {
// Limpiar recursos si es necesario
super.dispose();
}
}
```
**Características:**
- Extiende `ChangeNotifier`
- Expone estado inmutable
- Maneja errores y loading
- Interactúa con servicios
- Limpia recursos en dispose
### 4. Service (Servicio)
```dart
// services/api/user_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
class UserService {
final http.Client _client;
final String _baseUrl;
UserService({
required http.Client client,
required String baseUrl,
}) : _client = client,
_baseUrl = baseUrl;
Future<List<User>> getUsers() async {
final response = await _client.get(
Uri.parse('$_baseUrl/users'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
Future<User> createUser(User user) async {
final response = await _client.post(
Uri.parse('$_baseUrl/users'),
headers: {'Content-Type': 'application/json'},
body: json.encode(user.toJson()),
);
if (response.statusCode == 201) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to create user');
}
}
Future<User> updateUser(User user) async {
final response = await _client.put(
Uri.parse('$_baseUrl/users/${user.id}'),
headers: {'Content-Type': 'application/json'},
body: json.encode(user.toJson()),
);
if (response.statusCode == 200) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to update user');
}
}
Future<void> deleteUser(String userId) async {
final response = await _client.delete(
Uri.parse('$_baseUrl/users/$userId'),
);
if (response.statusCode != 204) {
throw Exception('Failed to delete user');
}
}
}
```
## 📦 Dependencias Recomendadas
```yaml
name: my_mvvm_app
description: Flutter app with MVVM pattern
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# State Management
provider: ^6.1.1
# Immutability & Serialization
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# Utilities
equatable: ^2.0.5
# HTTP
http: ^1.1.0
# Local Storage
shared_preferences: ^2.2.2
dev_dependencies:
flutter_test:
sdk: flutter
# Code Generation
build_runner: ^2.4.6
freezed: ^2.4.5
json_serializable: ^6.7.1
# Testing
mockito: ^5.4.4
# Linting
flutter_lints: ^3.0.1
```
## 📊 Flujo de Datos
```
┌─────────────┐
│ User │
│ Interaction │
└──────┬──────┘
│
▼
┌─────────────┐
│ View │
│ (Widget) │
└──────┬──────┘
│ Consumer/Selector
▼
┌─────────────┐
│ ViewModel │ ◄─── notifyListeners()
└──────┬──────┘
│
▼
┌─────────────┐
│ Service │
│ (API/DB) │
└──────┬──────┘
│
▼
┌─────────────┐
│ Model │
│ (Data) │
└─────────────┘
```
## 🧪 Testing
### Test del ViewModel
```dart
// test/viewmodels/user_viewmodel_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([UserService])
void main() {
late UserViewModel viewModel;
late MockUserService mockService;
setUp(() {
mockService = MockUserService();
viewModel = UserViewModel(mockService);
});
tearDown(() {
viewModel.dispose();
});
group('UserViewModel', () {
test('initial state should be empty', () {
expect(viewModel.users, isEmpty);
expect(viewModel.isLoading, false);
expect(viewModel.error, null);
});
test('fetchUsers should update users list when successful', () async {
// Arrange
final users = [
User(id: '1', name: 'John', email: 'john@test.com'),
User(id: '2', name: 'Jane', email: 'jane@test.com'),
];
when(mockService.getUsers()).thenAnswer((_) async => users);
// Act
await viewModel.fetchUsers();
// Assert
expect(viewModel.users, users);
expect(viewModel.isLoading, false);
expect(viewModel.error, null);
verify(mockService.getUsers()).called(1);
});
test('fetchUsers should set error when service fails', () async {
// Arrange
when(mockService.getUsers()).thenThrow(Exception('Network error'));
// Act
await viewModel.fetchUsers();
// Assert
expect(viewModel.users, isEmpty);
expect(viewModel.isLoading, false);
expect(viewModel.error, isNotNull);
expect(viewModel.error, contains('Network error'));
});
test('createUser should add user to list', () async {
// Arrange
final newUser = User(id: '1', name: 'John', email: 'john@test.com');
when(mockService.createUser(any)).thenAnswer((_) async => newUser);
// Act
await viewModel.createUser(newUser);
// Assert
expect(viewModel.users, contains(newUser));
expect(viewModel.isLoading, false);
});
});
}
```
### Test de Widget
```dart
// test/widgets/user_list_screen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:mockito/mockito.dart';
void main() {
late MockUserViewModel mockViewModel;
setUp(() {
mockViewModel = MockUserViewModel();
});
Widget createWidgetUnderTest() {
return MaterialApp(
home: ChangeNotifierProvider<UserViewModel>.value(
value: mockViewModel,
child: const UserListScreen(),
),
);
}
testWidgets('should show loading indicator when loading', (tester) async {
// Arrange
when(mockViewModel.isLoading).thenReturn(true);
when(mockViewModel.users).thenReturn([]);
when(mockViewModel.error).thenReturn(null);
// Act
await tester.pumpWidget(createWidgetUnderTest());
// Assert
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('should show user list when loaded', (tester) async {
// Arrange
final users = [
User(id: '1', name: 'John', email: 'john@test.com'),
User(id: '2', name: 'Jane', email: 'jane@test.com'),
];
when(mockViewModel.isLoading).thenReturn(false);
when(mockViewModel.users).thenReturn(users);
when(mockViewModel.error).thenReturn(null);
// Act
await tester.pumpWidget(createWidgetUnderTest());
// Assert
expect(find.text('John'), findsOneWidget);
expect(find.text('Jane'), findsOneWidget);
});
testWidgets('should show error message when error occurs', (tester) async {
// Arrange
when(mockViewModel.isLoading).thenReturn(false);
when(mockViewModel.users).thenReturn([]);
when(mockViewModel.error).thenReturn('Network error');
// Act
await tester.pumpWidget(createWidgetUnderTest());
// Assert
expect(find.text('Network error'), findsOneWidget);
expect(find.text('Reintentar'), findsOneWidget);
});
}
```
## ✅ Mejores Prácticas
### 1. Single Responsibility
Cada ViewModel debe manejar una única funcionalidad o pantalla.
### 2. Dependency Injection
Inyecta servicios a través del constructor del ViewModel.
```dart
// ❌ Malo
class UserViewModel extends ChangeNotifier {
final service = UserService(); // Acoplamiento fuerte
}
// ✅ Bueno
class UserViewModel extends ChangeNotifier {
final UserService _service;
UserViewModel(this._service); // Inyección de dependencia
}
```
### 3. Error Handling Consistente
Maneja errores de forma uniforme en todos los ViewModels.
### 4. Estado Loading
Siempre indica estados de carga para mejor UX.
### 5. Dispose Resources
Limpia recursos en el método `dispose()`.
```dart
@override
void dispose() {
_streamSubscription?.cancel();
_controller.dispose();
super.dispose();
}
```
### 6. Immutability
Usa objetos inmutables para los modelos.
### 7. Optimización con Selector
Usa `Selector` en lugar de `Consumer` cuando solo necesites parte del estado.
```dart
Selector<UserViewModel, bool>(
selector: (context, viewModel) => viewModel.isLoading,
builder: (context, isLoading, child) {
return isLoading
? CircularProgressIndicator()
: child!;
},
child: UserList(),
)
```
## 📚 Recursos Adicionales
- [Provider Documentation](https://pub.dev/packages/provider)
- [Freezed Package](https://pub.dev/packages/freezed)
## 🔄 Migración
### Desde setState
1. Extrae la lógica de negocio de tus Widgets a ViewModels
2. Reemplaza `setState()` con `notifyListeners()`
3. Envuelve tus Widgets con `Consumer` o `Selector`
### A Clean Architecture
Si tu proyecto crece, considera migrar a Clean Architecture para mayor escalabilidad.
---
**Última actualización:** Diciembre 2025
**Versión:** 1.0.0
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!