Flutter Arquitectura Limpia [8] – Fuente de Datos Local
[wp_ad_camp_1]
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
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.
[wp_ad_camp_2]
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.
[wp_ad_camp_4]
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.
[wp_ad_camp_5]
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
Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.