Flutter Arquitectura Limpia [8] – Fuente de Datos Local

flutterclean8
Compartir

Flutter Arquitectura Limpia [8] – Fuente de Datos Local

La siguiente dependencia del repositorio es la fuente de datos local utilizada para almacenar en caché los datos obtenidos de la API remota. Vamos a implementarlo usando shared_preferences

Debo aclarar que el contenido original es de Resocoder,  lo que he hecho  es una traducción al español del contenido. Al final de este artículo está el video en inglés para que vayan ilustrándose mejor y ya tengan una claridad de lo expuesto

Curso TDD Arquitectura Limpia

Esta publicación es solo una parte de una serie de tutoriales. ¡Todas las demás partes las puede ver aquí y aprenda a diseñar sus aplicaciones Flutter!

Hay muchas opciones para elegir cuando se trata de la persistencia de datos locales. Estamos usando shared_preferences porque no almacenamos muchos datos, solo un NumberTrivia que se convertirá a JSON. Como de costumbre, configuremos la prueba primero en una ubicación de prueba reflejada.

number_trivia_local_data_source_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:matcher/matcher.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../../../../fixtures/fixture_reader.dart';

class MockSharedPreferences extends Mock implements SharedPreferences {}

void main() {
  NumberTriviaLocalDataSourceImpl dataSource;
  MockSharedPreferences mockSharedPreferences;

  setUp(() {
    mockSharedPreferences = MockSharedPreferences();
    dataSource = NumberTriviaLocalDataSourceImpl(
      sharedPreferences: mockSharedPreferences,
    );
  });
}

Siguiendo lo que acabamos de prescribir en la prueba, crearemos la clase de implementación debajo de la abstracta.

number_trivia_local_data_source.dart

...
class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource {
  final SharedPreferences sharedPreferences;

  NumberTriviaLocalDataSourceImpl({@required this.sharedPreferences});

  @override
  Future<NumberTriviaModel> getLastNumberTrivia() {
    // TODO: implement getLastNumberTrivia
    return null;
  }

  @override
  Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache) {
    // TODO: implement cacheNumberTrivia
    return null;
  }
}

Centrémonos primero en el método getLastNumberTrivia. Llamarlo debería devolver el NumberTriviaModel almacenado en caché de las preferencias compartidas, por supuesto, dado que previamente se ha almacenado en caché. Sin embargo, ¿cómo se almacenará el modelo dentro de las preferencias? ¿Y cómo podemos probar todo esto?

 

Objetos y preferencias compartidas

El almacenamiento de objetos complejos como NumberTrivia dentro de las preferencias compartidas solo es posible en un formato string. Por lo tanto, el valor devuelto del objeto SharedPreferences simulado será un formato JSON, al igual que la respuesta API es un formato JSON. De las partes anteriores, ya conoce la mejor manera de trabajar con JSON en pruebas: ¡fixtures!

¿Podemos usar los fixtures que ya creamos en las partes anteriores? Bueno, más o menos. Usarlos ciertamente no rompería nada, pero eso no se aplica necesariamente a aplicaciones más complejas. ¿Por qué? Trivia.json y también trivia_double.json contienen algunos campos y valores que solo se encuentran en la respuesta API.

 

trivia.json

{
  "text": "Test Text",
  "number": 1,
  "found": true,
  "type": "trivia"
}

Cada vez que NumberTriviaModel se convierte a JSON (esto también sucederá en la fuente de datos local), el método toJson se ejecuta generando el siguiente mapa que contiene solo los campos de texto y número. Este mapa codificado con JSON se guardará en las preferencias.

 

number_trivia_model.dart

Map<String, dynamic> toJson() {
  return {
    'text': text,
    'number': number,
  };
}

Por lo tanto, creemos otro accesorio para imitar el NumberTriviaModel persistente. El nuevo archivo pasará a test/fixtures llamados trivia_cached.json.

 

trivia_cached.json

{
  "text": "Test Text",
  "number": 1
}

getLastNumberTrivia

Comenzando con la prueba, definimos la primera funcionalidad de este método. Debería devolver NumberTrivia de SharedPreferences cuando hay uno en el caché.

 

number_trivia_local_data_source_test.dart

group('getLastNumberTrivia', () {
  final tNumberTriviaModel =
      NumberTriviaModel.fromJson(json.decode(fixture('trivia_cached.json')));

  test(
    'should return NumberTrivia from SharedPreferences when there is one in the cache',
    () async {
      // arrange
      when(mockSharedPreferences.getString(any))
          .thenReturn(fixture('trivia_cached.json'));
      // act
      final result = await dataSource.getLastNumberTrivia();
      // assert
      verify(mockSharedPreferences.getString('CACHED_NUMBER_TRIVIA'));
      expect(result, equals(tNumberTriviaModel));
    },
  );
});

Haciendo lo prescrito por la prueba, implementamos el método para que pase. Dado que el tipo de retorno del método es Future y SharedPreferences es probablemente la única biblioteca de persistencia local síncrona que existe, usaremos una fábrica Future.value para devolver un Future ya completado.

number_trivia_local_data_source.dart

@override
Future<NumberTriviaModel> getLastNumberTrivia() {
  final jsonString = sharedPreferences.getString('CACHED_NUMBER_TRIVIA');
  // Future which is immediately completed
  return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString)));
}

La prueba pasa ahora y pasemos directamente a la fase de refactorización. Ciertamente no me gusta pasar strings mágicos, como ‘CACHED_NUMBER_TRIVIA‘, así que creemos una constante con el mismo nombre y usémosla durante toda la producción y el archivo de prueba.

number_trivia_local_data_source.dart

const CACHED_NUMBER_TRIVIA = 'CACHED_NUMBER_TRIVIA';

class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource {
...
}

No podemos confiar simplemente en que siempre habrá una versión en caché del último NumberTrivia. ¿Qué sucede si el usuario inicia la aplicación por primera vez sin tener conexión a Internet? En tal caso, el repositorio pasará inmediatamente a SharedPreferences y devolverá nulo.

¡Saludar a un primer usuario con un bloqueo de la aplicación ciertamente no sería una buena experiencia para el usuario! Para evitar que se produzcan bloqueos, lanzaremos una CacheException controlada. Si recuerda de la parte anterior, esta excepción está atrapada en el Repositorio que devuelve un Left (CacheFailure ()).

 

 

test.dart

test('should throw a CacheException when there is not a cached value', () {
  // arrange
  when(mockSharedPreferences.getString(any)).thenReturn(null);
  // act
  // Not calling the method here, just storing it inside a call variable
  final call = dataSource.getLastNumberTrivia;
  // assert
  // Calling the method happens from a higher-order function passed.
  // This is needed to test if calling a method throws an exception.
  expect(() => call(), throwsA(TypeMatcher<CacheException>()));
});

Implementar la prueba es tan simple como agregar una declaración if.

implementation.dart

@override
Future<NumberTriviaModel> getLastNumberTrivia() {
  final jsonString = sharedPreferences.getString('CACHED_NUMBER_TRIVIA');
  if (jsonString != null) {
    return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString)));
  } else {
    throw CacheException();
  }
}

cacheNumberTrivia

 

El método para colocar datos en preferencias solo debe llamar a SharedPreferences para almacenar en caché los datos. Realmente no podemos probar si los datos están presentes dentro de las preferencias (no en una prueba unitaria, al menos). La siguiente mejor opción es usar el poder de los mocks para verificar si la instancia en mock se ha llamado con los argumentos adecuados.

Después de todo, la cadena JSON generada por el método toJson del modelo y la cadena que se almacena dentro de las preferencias debe ser exactamente la misma.

 

test.dart

group('cacheNumberTrivia', () {
  final tNumberTriviaModel =
      NumberTriviaModel(number: 1, text: 'test trivia');

  test('should call SharedPreferences to cache the data', () {
    // act
    dataSource.cacheNumberTrivia(tNumberTriviaModel);
    // assert
    final expectedJsonString = json.encode(tNumberTriviaModel.toJson());
    verify(mockSharedPreferences.setString(
      CACHED_NUMBER_TRIVIA,
      expectedJsonString,
    ));
  });
});

implementation.dart

@override
Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache) {
  return sharedPreferences.setString(
    CACHED_NUMBER_TRIVIA,
    json.encode(triviaToCache.toJson()),
  );
}

¿Qué sigue?

En esta parte, implementamos la clase NumberTriviaLocalDataSource haciendo TDD. La última parte restante de la capa de datos es la fuente de datos remota que implementaremos a continuación. Esto significa que haremos un desarrollo basado en pruebas con el paquete http.

 

Aquí les comparto el video


Compartir

Entradas relacionadas

Deja tu comentario