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

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

AWS Security LIVE!

Hosted by security experts, AWS Security LIVE! showcases AWS Partners tackling real-world security challenges. Join live and get your security questions answered.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️