DEV Community

Cover image for Offline First no Flutter utilizando o DIO e Hive
Toshi Ossada for flutterbrasil

Posted on

1

Offline First no Flutter utilizando o DIO e Hive

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
view raw pubspec.yaml hosted with ❤ by GitHub

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']),
);
}
}
view raw cache_hive.dart hosted with ❤ by GitHub

// 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);
}
}
view raw onResponse.dart hosted with ❤ by GitHub

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);
}
view raw onRequest.dart hosted with ❤ by GitHub

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

Image description

Entre em nosso discord para interagir com a comunidade: https://discord.com/invite/flutterbrasil
https://linktr.ee/flutterbrasil

Top comments (0)