Saltar al contenido principal

Capa de datos

Un recorrido por la capa de datos de una aplicación que implementa la arquitectura MVVM.

La capa de datos de una aplicación, conocida como el modelo en la terminología MVVM, es la fuente de verdad de todos los datos de la aplicación. Como fuente de verdad, es el único lugar donde se deben actualizar los datos de la aplicación.

Es responsable de consumir datos de varias APIs externas, exponer esos datos a la interfaz de usuario, manejar los eventos de la interfaz de usuario que requieren la actualización de datos y enviar solicitudes de actualización a esas APIs externas según sea necesario.

La capa de datos en esta guía tiene dos componentes principales: repositorios y servicios.

Un diagrama que destaca los componentes de la capa de datos de una aplicación.

  • Los repositorios son la fuente de verdad para los datos de la aplicación y contienen lógica relacionada con esos datos, como la actualización de los datos en respuesta a nuevos eventos de usuario o el sondeo de datos desde los servicios. Los repositorios son responsables de sincronizar los datos cuando se admiten capacidades sin conexión, gestionar la lógica de reintentos y almacenar datos en caché.
  • Los servicios son clases de Dart stateless que interactúan con APIs, como servidores HTTP y plugins de plataforma. Cualquier dato que tu aplicación necesite y que no se cree dentro del propio código de la aplicación debe obtenerse desde las clases de servicio.

Definir un servicio

#

Una clase de servicio es el componente menos ambiguo de todos los componentes de la arquitectura. Es stateless y sus funciones no tienen efectos secundarios. Su único trabajo es envolver una API externa. Generalmente hay una clase de servicio por fuente de datos, como un servidor HTTP de cliente o un plugin de plataforma.

Un diagrama que muestra las entradas y salidas de los objetos de servicio.

En la aplicación Compass, por ejemplo, existe un servicio APIClient que maneja las llamadas CRUD al servidor orientado al cliente.

api_client.dart
dart
class ApiClient {
  // Some code omitted for demo purposes.

  Future<Result<List<ContinentApiModel>>> getContinents() async { /* ... */ }

  Future<Result<List<DestinationApiModel>>> getDestinations() async { /* ... */ }

  Future<Result<List<ActivityApiModel>>> getActivityByDestination(String ref) async { /* ... */ }

  Future<Result<List<BookingApiModel>>> getBookings() async { /* ... */ }

  Future<Result<BookingApiModel>> getBooking(int id) async { /* ... */ }

  Future<Result<BookingApiModel>> postBooking(BookingApiModel booking) async { /* ... */ }

  Future<Result<void>> deleteBooking(int id) async { /* ... */ }

  Future<Result<UserApiModel>> getUser() async { /* ... */ }
}

El servicio en sí es una clase, donde cada método envuelve un endpoint de API diferente y expone objetos de respuesta asíncronos. Continuando con el ejemplo anterior de eliminar una reserva guardada, el método deleteBooking devuelve un Future<Result<void>>.

Definir un repositorio

#

La única responsabilidad de un repositorio es gestionar los datos de la aplicación. Un repositorio es la fuente de verdad para un único tipo de datos de la aplicación, y debe ser el único lugar donde se modifique ese tipo de datos. El repositorio es responsable de sondear nuevos datos de fuentes externas, manejar la lógica de reintentos, gestionar los datos en caché y transformar los datos brutos en modelos de dominio.

Un diagrama que destaca el componente de repositorio de una aplicación.

Deberías tener un repositorio independiente para cada tipo de datos diferente en tu aplicación. Por ejemplo, la aplicación Compass tiene repositorios llamados UserRepository, BookingRepository, AuthRepository, DestinationRepository y más.

El siguiente ejemplo es el BookingRepository de la aplicación Compass y muestra la estructura básica de un repositorio.

booking_repository_remote.dart
dart
class BookingRepositoryRemote implements BookingRepository {
  BookingRepositoryRemote({
    required ApiClient apiClient,
  }) : _apiClient = apiClient;

  final ApiClient _apiClient;
  List<Destination>? _cachedDestinations;

  Future<Result<void>> createBooking(Booking booking) async {...}
  Future<Result<Booking>> getBooking(int id) async {...}
  Future<Result<List<BookingSummary>>> getBookingsList() async {...}
  Future<Result<void>> delete(int id) async {...}
}

El BookingRepository recibe el servicio ApiClient como entrada, el cual utiliza para obtener y actualizar los datos brutos del servidor. Es importante que el servicio sea un miembro privado, para que la capa de UI no pueda eludir el repositorio y llamar a un servicio directamente.

Con el servicio ApiClient, el repositorio puede realizar sondeos para buscar actualizaciones de las reservas guardadas de un usuario que puedan ocurrir en el servidor, y realizar solicitudes POST para eliminar reservas guardadas.

Los datos brutos que un repositorio transforma en modelos de aplicación pueden provenir de múltiples fuentes y múltiples servicios, y por lo tanto los repositorios y servicios tienen una relación de muchos a muchos. Un servicio puede ser utilizado por cualquier número de repositorios, y un repositorio puede utilizar más de un servicio.

Un diagrama que destaca los componentes de la capa de datos de una aplicación.

Modelos de dominio

#

El BookingRepository produce objetos Booking y BookingSummary, que son modelos de dominio. Todos los repositorios producen los correspondientes modelos de dominio. Estos modelos de datos difieren de los modelos de la API en que solo contienen los datos necesarios para el resto de la aplicación. Los modelos de la API contienen datos brutos que a menudo necesitan ser filtrados, combinados o eliminados para ser útiles para los view models de la aplicación. El repositorio refina los datos brutos y los expone como modelos de dominio.

En la aplicación de ejemplo, los modelos de dominio se exponen a través de valores de retorno en métodos como BookingRepository.getBooking. El método getBooking es responsable de obtener los datos brutos del servicio ApiClient y transformarlos en un objeto Booking. Hace esto combinando datos de múltiples endpoints de servicios.

booking_repository_remote.dart
dart
// This method was edited for brevity.
Future<Result<Booking>> getBooking(int id) async {
  try {
    // Get the booking by ID from server.
    final resultBooking = await _apiClient.getBooking(id);
    if (resultBooking is Error<BookingApiModel>) {
      return Result.error(resultBooking.error);
    }
    final booking = resultBooking.asOk.value;

    final destination = _apiClient.getDestination(booking.destinationRef);
    final activities = _apiClient.getActivitiesForBooking(
            booking.activitiesRef);

    return Result.ok(
      Booking(
        startDate: booking.startDate,
        endDate: booking.endDate,
        destination: destination,
        activity: activities,
      ),
    );
  } on Exception catch (e) {
    return Result.error(e);
  }
}

Completar el ciclo de eventos

#

A lo largo de esta página, has visto cómo un usuario puede eliminar una reserva guardada, comenzando con un evento: un usuario desliza un widget Dismissible. El view model maneja ese evento delegando la mutación de datos real al BookingRepository. El siguiente fragmento muestra el método BookingRepository.deleteBooking.

booking_repository_remote.dart
dart
Future<Result<void>> delete(int id) async {
  try {
    return _apiClient.deleteBooking(id);
  } on Exception catch (e) {
    return Result.error(e);
  }
}

El repositorio envía una solicitud POST al cliente de la API con el método _apiClient.deleteBooking y devuelve un Result. El HomeViewModel consume el Result y los datos que contiene, para luego llamar a notifyListeners, completando el ciclo.

Comentarios

#

Dado que esta sección del sitio web está evolucionando, ¡agradecemos tus comentarios!