Quando desenvolvemos um aplicativo é muito comum termos dependência de dados que vem da internet, entretanto garantir que o usuário esteja 100% do tempo conectado á rede mundial de computadoress é uma tarefa impossível.
Com este problema em vista várias empresas adotam o conceito do Offline First, para garantir que algumas funcionalidades permaneçam funcionando mesmo que o usuário esteja desconectado a uma rede de internet.
O offline first trabalha com o conceito de local storage, que basicamente iremos armazenar informações que virão da internet (uma requisição da api) para quando o dispositivo do usuário estiver desconectado (ou até mesmo para não precisar fazer várias requisições desnecessárias) ele consiga utilizar algumas funções.
Para este artigo utilizaremos o Dio como cliente HTTP e o hive como banco de dados local, também precisaremos do build_runner e do hive_generator
dependencies: | |
flutter: | |
sdk: flutter | |
dio: ^4.0.6 | |
hive: ^2.2.3 | |
dev_dependencies: | |
build_runner: ^2.1.11 | |
hive_generator: ^2.0.0 |
Primeiramente vamos construir o modelo que iremos salvar nossos dados offline (cache) , nele iremos precisar de um campo identificador, um para a data de atualização e um campo que irá conter o Map do resultado da API.
import 'dart:convert'; | |
import 'package:hive/hive.dart'; | |
part 'cache_model_database.g.dart'; | |
@HiveType(typeId: 1) | |
class CacheModel extends HiveObject { | |
@HiveField(0) | |
@override | |
final String id; | |
@HiveField(1) | |
@override | |
final DateTime date; | |
@HiveField(2) | |
@override | |
final Map<String, dynamic> data; | |
CacheModelDatabase({ | |
required this.id, | |
required this.data, | |
DateTime? date, | |
}) : date = date ?? DateTime.now(); | |
@override | |
Map<String, dynamic> toMap() { | |
return <String, dynamic>{ | |
'id': id, | |
'date': date.millisecondsSinceEpoch, | |
'data': data, | |
}; | |
} | |
@override | |
String toJson() => json.encode(toMap()); | |
} |
Será necessário executar o build_runner
flutter pub run build_runner build
Nesse momento podemos criar a classe que ficará responsável por gerenciar este banco de dados local, chamaremos de cache_adapter. Nele teremos um método para recuperar o dado salvo em um determinado id, e outro para inserir/atualizar o valor armazenado
import 'dart:async'; | |
import 'package:hive/hive.dart'; | |
import 'package:path_provider/path_provider.dart'; | |
import 'models/cache_model.dart'; | |
class CacheAdapter { | |
final completer = Completer<Box>(); | |
CacheAdapter() { | |
_initDb(); | |
} | |
Future _initDb() async { | |
var appDocDirectory = await getApplicationDocumentsDirectory(); | |
Hive | |
..init(appDocDirectory.path) | |
..registerAdapter(CacheModelAdapter()); | |
final box = await Hive.openBox('cache'); | |
if (!completer.isCompleted) completer.complete(box); | |
} | |
Future<void> put(CacheModel data) async { | |
final box = await completer.future; | |
box.put(data.id, data.toMap()); | |
} | |
Future<CacheModel?> get(String id) async { | |
final box = await completer.future; | |
final data = await box.get(id); | |
if (data == null) return null; | |
return CacheModel( | |
id: data['id'], | |
data: data['data'], | |
date: DateTime.fromMillisecondsSinceEpoch(data['date']), | |
); | |
} | |
} |
Podemos melhorar este código e fazer a segregação por interface
import 'models/cache_model.dart'; | |
abstract class ICacheAdapter { | |
Future<CacheModel?> get(String id); | |
Future<void> put(CacheModel data); | |
} |
import 'dart:convert'; | |
class CacheModel { | |
final String id; | |
final DateTime date; | |
final Map<dynamic, dynamic> data; | |
CacheModel({ | |
required this.id, | |
required this.date, | |
required this.data, | |
}); | |
Map<String, dynamic> toMap() { | |
return <String, dynamic>{ | |
'id': id, | |
'date': date.millisecondsSinceEpoch, | |
'data': data, | |
}; | |
} | |
String toJson() => json.encode(toMap()); | |
} |
import 'dart:async'; | |
import 'package:hive/hive.dart'; | |
import 'package:path_provider/path_provider.dart'; | |
import '../cache_adapter.dart'; | |
import '../models/cache_model.dart'; | |
import 'models/cache_model_database.dart'; | |
class CacheHive implements ICacheAdapter { | |
final completer = Completer<Box>(); | |
CacheHive() { | |
_initDb(); | |
} | |
Future _initDb() async { | |
var appDocDirectory = await getApplicationDocumentsDirectory(); | |
Hive | |
..init(appDocDirectory.path) | |
..registerAdapter(CacheModelDatabaseAdapter()); | |
final box = await Hive.openBox('cache'); | |
if (!completer.isCompleted) completer.complete(box); | |
} | |
@override | |
Future<void> put(CacheModel data) async { | |
final box = await completer.future; | |
box.put(data.id, data.toMap()); | |
} | |
@override | |
Future<CacheModel?> get(String id) async { | |
final box = await completer.future; | |
final data = await box.get(id); | |
if (data == null) return null; | |
return CacheModel( | |
id: data['id'], | |
data: data['data'], | |
date: DateTime.fromMillisecondsSinceEpoch(data['date']), | |
); | |
} | |
} |
// ignore_for_file: public_member_api_docs, sort_constructors_first | |
import 'dart:convert'; | |
import 'package:hive/hive.dart'; | |
import '../../models/cache_model.dart'; | |
part 'cache_model_database.g.dart'; | |
@HiveType(typeId: 1) | |
class CacheModelDatabase extends HiveObject implements CacheModel { | |
@HiveField(0) | |
@override | |
final String id; | |
@HiveField(1) | |
@override | |
final DateTime date; | |
@HiveField(2) | |
@override | |
final Map<String, dynamic> data; | |
CacheModelDatabase({ | |
required this.id, | |
required this.data, | |
DateTime? date, | |
}) : date = date ?? DateTime.now(); | |
@override | |
Map<String, dynamic> toMap() { | |
return <String, dynamic>{ | |
'id': id, | |
'date': date.millisecondsSinceEpoch, | |
'data': data, | |
}; | |
} | |
@override | |
String toJson() => json.encode(toMap()); | |
} |
Agora vamos fazer um simples caso de uso que irá verificar se o dispositivo está conectado a internet.
import 'dart:io'; | |
class CheckInternetUsecase { | |
Future<bool> call() async { | |
try { | |
final result = await InternetAddress.lookup('google.com'); | |
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { | |
return true; | |
} | |
} on SocketException catch (_) { | |
return false; | |
} | |
return false; | |
} | |
} |
Agora no Dio precisamos criar um Interceptador pois iremos interceptar o métodos onRequest e onResponse. Neles vamos receber via injeção de dependências pelo construtor tanto a classe que irá gerenciar o cache, quanto o caso de uso que irá verificar a conectividade
Se não faz ideia do que são interceptadores do DIO tem um artigo bem legal que escrevi a um tempo explicando-os https://toshiossada.medium.com/consumindo-api-utilizando-o-dio-9ec72aeceeaa
import 'dart:convert'; | |
import 'dart:developer'; | |
import 'package:dio/dio.dart'; | |
class CustomInterceptors extends InterceptorsWrapper { | |
final durationCacheInMinutes = 5; | |
final ICacheAdapter cacheAdapter; | |
final CheckInternetUsecase checkInternetUsecase; | |
CustomInterceptors({ | |
required this.cacheAdapter, | |
}); | |
@override | |
Future<void> onRequest( | |
RequestOptions options, | |
RequestInterceptorHandler handler, | |
) async { | |
handler.next(options); | |
} | |
@override | |
Future<void> onResponse( | |
Response response, | |
ResponseInterceptorHandler handler, | |
) async { | |
handler.next(response); | |
} | |
@override | |
void onError( | |
DioError err, | |
ErrorInterceptorHandler handler, | |
) { | |
handler.next(failure); | |
} | |
} |
Agora no método onResponse vamos armazenar o resultado da consulta quando fizermos uma requisição GET e colocaremos um tempo de validade de cinco minutos no cache armazenado. Note que como id usaremos o path da requisição GET, pois desta maneira toda vez que ele fizer requisição para o mesmo endpoint iremos atualizar o valor para o mais atual.
Future<void> onResponse( | |
Response response, | |
ResponseInterceptorHandler handler, | |
) async { | |
if (response.requestOptions.method == 'GET') { | |
final id = response.requestOptions.path; | |
var dataCached = await cacheAdapter.get(id); | |
// Se passou de 5 minutos, atualiza o cache | |
if (dataCached?.date == null || | |
DateTime.now() | |
.difference(dataCached?.date ?? DateTime.now()) | |
.inMinutes > 5) { | |
final data = CacheModel( | |
data: json.decode(response.data), | |
date: DateTime.now(), | |
id: id, | |
); | |
cacheAdapter.put(data); | |
} | |
} |
No Método onRequest chamamos o caso de uso para verificar a conectividade do dispositivo e caso não esteja conectado iremos recuperar o valor armazenado para aquela URL.
Future<void> onRequest( | |
RequestOptions options, | |
RequestInterceptorHandler handler, | |
) async { | |
final online = await checkInternetUsecase(); | |
if (!online) { | |
var dataCached = await cacheAdapter.get(options.path); | |
// Se não passou de 5 minutos, retorna o cache | |
handler.resolve( | |
Response( | |
data: json.encode(dataCached?.data), | |
extra: options.extra, | |
statusCode: 200, | |
requestOptions: options, | |
), | |
true, | |
); | |
return; | |
} | |
handler.next(options); | |
} |
Desta forma mesmo com o dispositivo offline conseguiremos continuar fazendo a busca
Legal né? Agora você pode utilizar esta ideia e evoluir seus aplicativo para funcionar de forma offline
Acesse o projeto de exemplo
https://github.com/toshiossada/termo_hack
Entre em nosso discord para interagir com a comunidade: https://discord.com/invite/flutterbrasil
https://linktr.ee/flutterbrasil
Top comments (0)