ejemplo de web usando md

MVVM (Model View View-Model)

Reglas MVVM:

1) Las 'views' nunca deben usar servicios directamente, solo los 'view-models'. 2) En teoría una 'view' no debería contener lógica (aunque esto no siempre es posible), debe delegar al 'view-model' tanto como sea posible: esa es su función. 3) Los 'view-model' nunca debrían usar otros 'view-models'. Mueve esa función compartida a un 'service' y consúmela desde allí. 4) Los 'view-models' nunca deberían tener acceso al 'BuilContext'. Delega eso a las 'views'. 5) Las dependencias deberían siempre ser inyectadas a través del constructor.

View

Es todo lo que se ve o también conocida como UI (interfaz de usuario en inglés).
Puede ser una página de la aplicación o un widget de UI (Stateless o Stateful).

ViewModel

Cada página debe tener exactamente un ViewModel. El ViewModel cumple con múltiples propósitos:

Service

Un servicio es similar a un ViewModel, pero la principal diferencia es que no está vinculado a una sola vista, ya que está diseñado para ser utilizado en toda la aplicación.
Eventualmente, la aplicación necesitará interactuar con él, por lo que debe ser pasado al ViewModel. Esto se logra inyectándolo a través del constructor del ViewModel. Así que, cada vez que tengas múltiples vistas dependiendo del mismo estado, ese estado debería manejarse mediante un servicio del que dependan esos ViewModels.
Todas las propiedades deben ser ValueNotifiers, y el ViewModel expone getters para esas propiedades, de modo que las vistas puedan reaccionar a los cambios.
En el ejemplo anterior, teníamos los todos dentro de un único ViewModel. Eso significa que estaban vinculados a una sola página. ¿Qué sucede si queremos acceder a los todos en varias páginas? En ese caso, usaríamos un servicio. Así es como se vería:

class TodoService {
  final ValueNotifier<List<Todo>> todosNotifier = ValueNotifier([]);

  void add(Todo todo) {
    todosNotifier.value = [...todosNotifier.value, todo];
  }
}

class TodoPageViewModel {
  TodoPageViewModel({required TodoService todoService}) : _todoService = todoService;

  final TodoService _todoService;

  ValueNotifier<List<Todo>> get todosNotifier => _todoService.todosNotifier;

  void add(Todo todo) {
    _todoService.add(todo);
  }
}

Ahora el TodoService maneja el ValueNotifier en lugar del ViewModel, y este último simplemente lo expone a la vista.
Esto es útil por varias razones:

Repositories

La última parte esencial de la arquitectura son los Repositories. Son una capa de abstracción para cualquier fuente de datos. Esto incluye bases de datos, solicitudes HTTP, sistemas de archivos, cachés y servicios externos.

La idea clave es que los Repositories proporcionan una interfaz consistente para que la aplicación interactúe con diversas fuentes de datos, independientemente de su naturaleza. Esta abstracción permite a la aplicación:

1) Obtener datos de múltiples fuentes y usar los mejores datos. 2) Cambiar entre diferentes fuentes de datos sin modificar la lógica de la aplicación. 3) Combinar datos de varias fuentes en una sola interfaz coherente.

Por ejemplo, un repositorio podría obtener datos de usuario de una base de datos local, recuperar su actividad reciente de una API REST y verificar su estado de suscripción en un servicio de terceros, todo mientras presenta una interfaz unificada al resto de la aplicación.

Testing
Tener esto como una capa separada ayuda a mantener los datos de tu aplicación organizados y facilita significativamente las pruebas de tu aplicación.

Con la capa de repositorio, probar tu aplicación se vuelve mucho más sencillo. En lugar de simular cada servicio externo, simplemente simulas tu repositorio. Esto simplifica el proceso de pruebas y lo hace más conveniente para los desarrolladores.

Dependency injection

Para unir las capas anteriores, se necesita una forma de inyectar un Service en un ViewModel o un Repository en un Service.
Recordar la regla: Las dependencias siempre deben inyectarse a través del constructor.
En la práctica, esto es muy sencillo, pero necesitamos una forma de tener una única instancia e inyectar esa instancia en lugar de crear una nueva.

class MyService {}

class MyViewModel {
  MyViewModel({required MyService myService}) : _myService = myService;

  final MyService _myService
}

final myViewModel = MyViewModel(myService: WeNeedTheServiceInstanceHere);

Pasar dependencias
El ViewModel está vinculado a una sola vista; su principal función es contener la lógica de negocio. Para llevar a cabo esta lógica, necesita acceder a la instancia del servicio o repositorio.
Existen varias formas de crear una nueva instancia y usar esa única instancia en toda la aplicación.
Un enfoque es InheritedWidget. Usando un InheritedWidget, primero se crea una instancia del Service y se inserta en el árbol de widgets. Cualquier widget que esté más abajo en el árbol de widgets puede acceder a ella. Aunque InheritedWidget funciona, no es la única forma de hacerlo.
El otro enfoque es usar un 'Service Locator'.
Service Locator
Se puede usar un Service Locator, que es básicamente un mapa avanzado utilizado para crear y referenciar instancias de clases. Se podría crear un localizador de servicios por cuenta propia, pero en este ejemplo se usa el paquete GetIt, un paquete de localizador de servicios.

final locator = GetIt.instance;

// call this when you want to create your instances
void setupLocator() {
  locator.registerSingleton<MyService>(MyService());
  // ... other services, integrations etc
}

final myViewModel = MyViewModel(myService: locator<MyService>());

El objetivo principal es NO crear múltiples instancias de los Services. Estos servicios manejan lógica que debe ser consistente en toda la aplicación (imagina tener un PaymentService que maneje el estado de pagos. Ahora, imagina manejar múltiples de esos).
Algunos otros enfoques:

Entities

Cuando se trabaja con datos de un servidor o almacenados en un dispositivo, esos datos pueden tener una estructura diferente a la de los datos que se utilizan en la View. La responsabilidad de una Entity es reflejar los datos exactamente como están almacenados (de forma local o remota). Luego, utilizas la Entity para crear el Model.
Ejemplo de UserEntity y User (Model):

class UserEntity {
  UserEntity({required this.id, required this.createdAt, required this.firstName, required this.lastName});

  final int id;
  final DateTime createAt;
  final String firstName;
  final String lastName;
   // method to convert to a `User`
}
class User {
  User({required this.id, required this.firstName, required this.lastName});

  final int id;
  final String firstName;
  final String lastName;

  String get fullName => `$firstName $lastName`;
   // method to convert to a `UserEntity`
}

En general usar solamente entidades está bien la mayor parte del tiempo. Pero, cuando los datos almacenados se vuelven masivos y complicados, y se trabaja en un proyecto más grande con un equipo más grande, agregar una capa desde Entities a Models simplifica las cosas.

Entidades en paquetes Muchas veces, las entidades son proporcionadas por los proveedores de almacenamiento a través de sus paquetes o utilizando paquetes asociados para generar las entidades. Por ejemplo, en GraphQL, existe un paquete graphql_codegen para generar estas entidades.

Cualquiera que sea el enfoque que se use, se necesitarán clases de entidades para representar las respuestas de datos y mapear esas entidades a la interfaz de usuario. Nuevamente, esto puede estar directamente vinculado a la interfaz de usuario si los datos almacenados son simples o a través de un modelo.

También es una buena idea que las entidades se generen para reducir el riesgo de problemas de análisis. Hay generadores específicos de paquetes como los mencionados anteriormente o generadores generales como json_serializable o retrofit, que permiten crear las entidades uno mismo.

How to handle remote data

Almost every app nowadays uses a database. Some of the worst user experiences I have experienced have all been caused by not having the data locally first.

No network connection, and the app doesn’t work at all Slow network connection and the app becomes almost unusable For some apps, this is acceptable. For example, I don’t think anybody complains that their video call app isn’t working when they don’t have a connection. Obviously, there is no connection, and the app shouldn’t be working. However, a todo list, notes app, games, and fitness tracker should all work just the same when there is no connection to have a good experience.

So how do you handle this?

You want to have two data sources, a local and a remote. Then, the repository is used to orchestrate which data source to call.

When do you need this For example, if you are building a todo app, you would want to have a local and a remote database of the user’s todo list. When the app is offline, use the local database, and when it is back online, create a system to update the remote database with all the new todos so they can be synced with other devices.

Data Source This fits right into the MVVM architecture. You can create a LocalDatabaseAbstraction and a RemoteDatabaseAbstraction and then use them within the repository.

For example, if we want to create a todo, it would be nice if local and remote data sources had the same create(Todo todo) function.

Then, as you handle this in the repository, you don’t have to worry about how the actual todo is created (the implementation details are within the data source); you only have to worry about orchestrating it properly.

class TodosRepository {
  TodosRepository({required TodosLocalDataSource todosLocalDataSource, required TodosRemoteDataSource todosRemoteDataSource}) : _todosLocalDataSource = todosLocalDataSource, _todosRemoteDataSource = todosRemoteDataSource;

  final TodosLocalDataSource _todosLocalDataSource;
  final TodosRemoteDataSource _todosRemoteDataSource;

  final _streamController =
      StreamController<List<Todo>>.broadcast(sync: true);

  Stream<List<Todo>> watchAll() {
    return _streamController.stream;
  }

  Future<void> create(Todo todo) async {
    // You would need to handle any exceptions that can come up and gracefully revert state if needed
    await _todosRemoteDataSource.create(model);
    await _todosLocalDataSource.create(model);
    final todos = await _todosLocalDataSource.readAll();
    _streamController.add(todos);
  }
}

In short, we call both the remote and local source and provide a listenable so services or ViewModels can update once the data has been updated.