Couchbase: o que é e como usá-lo no Flutter para criar um app offline first
Fala, galera dev! Tudo certo?
Você já criou alguma aplicação offline first que faz persistência de dados localmente? E uma que se comunica com APIs externas?
Cada uma dessas tarefas já é um desafio por si só; agora imagine a complexidade ao lidar com ambas ao mesmo tempo.
Neste artigo, vamos explorar como gerenciar dados tanto localmente quanto remotamente em uma aplicação e mostrar como o Couchbase simplifica esse processo.
Conhecendo o app Mood
Para entender o gerenciamento de dados locais e remotos, vamos trabalhar com um aplicativo Flutter de humor, em que a pessoa usuária pode escrever como está se sentindo.
Começaremos analisando a classe MyMood
, que representa o humor:
class MyMood {
final String message;
MyMood({required this.message});
factory MyMood.fromJson(Map<String, dynamic> json) {
return MyMood(
message: json['message'],
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'message': message,
};
}
}
Essa classe MyMood
representa um modelo de dados com uma única propriedade message
.
Ela inclui métodos para criar uma instância a partir de um JSON e para converter a instância de volta para um mapa (Map) de dados.
A tela principal da aplicação está implementada da seguinte forma:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'App de Humor',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MoodScreen(),
);
}
}
class MoodScreen extends StatefulWidget {
@override
_MoodScreenState createState() => _MoodScreenState();
}
class _MoodScreenState extends State<MoodScreen> {
String _moodMessage = "Estou feliz";
final TextEditingController _controller = TextEditingController();
void _saveMood() {
if (_controller.text.isNotEmpty) {
setState(() {
_moodMessage = _controller.text;
_controller.clear();
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Seu Humor do Dia'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Seu Humor:',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
_moodMessage,
style: TextStyle(fontSize: 20, fontStyle: FontStyle.italic),
),
SizedBox(height: 20),
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Digite uma frase sobre o seu humor',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _saveMood,
child: Text('Salvar Humor'),
),
],
),
),
);
}
}
O código cria um app Flutter com uma tela em que você registra o seu humor. Ao clicar no botão, o humor digitado é exibido na tela.
No entanto, isso acontece enquanto o aplicativo está aberto. Ou seja, se fechamos, todo o progresso é perdido e o humor da pessoa usuária não é registrado.
Nosso objetivo é fazer com que o app armazene o humor digitado pela pessoa usuária, para que o progresso se mantenha mesmo se ela fechar, desinstalar ou usar o aplicativo em outro dispositivo.
Qual o problema de usar apenas dados remotos em aplicativos Flutter?
Talvez, ao pensar na solução do problema dos dados que somem, tenha vindo à sua mente a ideia de usar um servidor remoto para armazenar o humor da pessoa usuária e recuperá-lo sempre que necessário. Algo como:
dynamic getMood(url) async {
dynamic mood = // faz requisição GET na URL
return mood;
}
E, de fato, essa solução não estaria de todo equivocada, mas traz alguns problemas. Primeiro, vamos analisar a imagem a seguir:
A abordagem de armazenar os dados exclusivamente no servidor nos deixa totalmente dependentes da resposta dele (como ilustrado pela imagem). E tem mais.
O grande problema é que a comunicação entre o cliente e o servidor pode falhar por diversos motivos, o que inutiliza o aplicativo.
Outro contra é que, ao abrir o app, o sistema acessa o servidor para buscar os dados, embora esses dados sejam os mesmos de um acesso anterior.
Por último, ao depender de dados remotos, as consultas constantes ao servidor podem sobrecarregar a infraestrutura, o que aumenta custos e impacta negativamente a performance do app.
Com essas limitações em mente, é hora de conhecermos um novo conceito: a persistência de dados.
O que é persistência de dados no Flutter?
A persistência de dados no Flutter é uma técnica de desenvolvimento em que salvamos os dados do aplicativo localmente.
É como se, dentro do dispositivo móvel, houvesse uma caixa onde guardamos os dados.
Esses dados podem ser itens de uma lista, informações de login, configurações e preferências da pessoa usuária (dark mode, por exemplo).
Ao armazenar dados diretamente no dispositivo, você elimina as requisições constantes ao servidor, reduzindo a dependência da conexão de internet e permitindo que o app continue funcionando sem rede.
E as consultas locais são muito mais rápidas que aquelas feitas a servidores remotos. Isso melhora a performance do app, principalmente em regiões com conexões lentas ou instáveis, onde a latência pode ser um fator limitante.
Outra vantagem é que o armazenamento local reduz a carga no servidor, pois muitas consultas, antes remotas, são processadas diretamente no dispositivo.
Entendendo nosso problema, chegou o momento de criar nosso banco local e, para fazê-lo, usaremos o Couchbase.
Para começar, adicione as seguintes dependências no arquivo pubspec.yaml
:
dependencies:
cbl_flutter:
cbl_flutter_ce:
cbl:
## Lembrando que você deve utilizar as versões mais recentes
Depois, execute:
flutter pub get
Com as dependências instaladas, vamos começar configurando o banco de dados local:
Em seguida, crie a classe que será responsável por armazenar os dados da nossa coleção do Couchbase:
class CouchbaseContants {
static const String channel = 'moodcollection';
static const String collection = 'moodcollection';
static const String scope = 'moodscope';
}
Com as constantes criadas, vamos partir para a criação do nosso serviço:
class DatabaseService {
AsyncDatabase? database;
Future<void> init() async {
database ??= await Database.openAsync('database');
}
}
Neste trecho de código, criamos a classe que será responsável pelo armazenamento local da nossa aplicação e criamos um método de inicialização.
Como salvar dados no banco local com persistência no Flutter?
Agora que a estrutura do banco está pronta, vamos criar a função add
para salvar o humor no banco:
class DatabaseService {
AsyncDatabase? database;
//Outros métodos
Future<bool> addMood(MyMood mood) async {
final collection = await database?.createCollection(
CouchbaseContants.collection,
CouchbaseContants.scope,
);
if (collection != null) {
final document = MutableDocument(mood.toMap());
final resultSave = await collection.saveDocument(
document,
ConcurrencyControl.lastWriteWins,
);
return resultSave;
}
return false;
}
//Outros métodos
}
Essa função cria um MutableDocument
com os dados do humor e o salva no banco de dados. O campo type
é usado para identificar o tipo de documento, o que facilita consultas posteriores.
Consultando os dados armazenados no dispositivo
Agora, vamos buscar o humor salvo no banco com a função abaixo:
class DatabaseService {
AsyncDatabase? database;
//Outros métodos
Future<List<MyMood>?> fetch({
required String collectionName,
}) async {
await init();
await database?.createCollection(
collectionName,
CouchbaseContants.scope,
);
final query = await database?.createQuery(
'SELECT META().id, * FROM ${CouchbaseContants.scope}.$collectionName''}',
);
final result = await query?.execute();
final results = await result?.allResults();
final data = results
?.map((e) => {
'id': e.string('id'),
...(e.toPlainMap()[collectionName] as Map<String, dynamic>)
})
.toList();
final moodsFromData = data?.map((e) => MyMood.fromJson(e)).toList();
return moodsFromData ?? [];
}
//Outros métodos
}
Neste código, através do método fetch
, fazemos a busca do humor armazenado e fazemos o retorno dele.
O foco do artigo não será fazer operações com um banco de dados local, mas se você quiser explorar um pouquinho mais sobre persistência, acesse este artigo: Persistência de Dados no Flutter: o que é? Qual ferramenta usar?
Com as funções prontas, precisamos modificar o nosso app Flutter e implementar a nova funcionalidade de persistência de dados.
Nossa aplicação vai ficar da seguinte forma:
import 'package:flutter/material.dart';
import 'package:cbl_flutter/cbl_flutter.dart';
import 'package:cbl/cbl.dart';
//Outras classes
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await CouchbaseLiteFlutter.init();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'App de Humor',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MoodScreen(),
);
}
}
class MoodScreen extends StatefulWidget {
@override
_MoodScreenState createState() => _MoodScreenState();
}
class _MoodScreenState extends State<MoodScreen> {
String _moodMessage = "Estou feliz";
final TextEditingController _controller = TextEditingController();
final DatabaseService _dbService = DatabaseService();
@override
void initState() {
super.initState();
_initializeDatabase();
}
Future<void> _initializeDatabase() async {
await _dbService.init();
_loadMood();
}
Future<void> _loadMood() async {
try {
final storedMood =
await _dbService.fetch(collectionName: 'moodCollection');
if (storedMood != null) {
setState(() {
_moodMessage = storedMood.last.message;
});
} else {
setState(() {
_moodMessage = "Estou feliz";
});
}
} catch (e) {
setState(() {
_moodMessage = "Estou feliz";
});
}
}
void _saveMood() async {
if (_controller.text.isNotEmpty) {
final newMood = MyMood(message: _controller.text);
bool isSaved = await _dbService.addMood(newMood);
if (isSaved) {
setState(() {
_moodMessage = _controller.text;
});
_controller.clear();
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Seu Humor do Dia'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Seu Humor:',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
_moodMessage,
style: TextStyle(fontSize: 20, fontStyle: FontStyle.italic),
),
SizedBox(height: 20),
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Digite uma frase sobre o seu humor',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _saveMood,
child: Text('Salvar Humor'),
),
],
),
),
);
}
}
Pronto, usamos como base a mesma aplicação que havíamos criado anteriormente e adicionamos o armazenamento local dos dados.
Como sincronizar dados locais e remotos em apps Flutter?
Já entendemos os problemas de usar apenas dados remotos, vimos como o armazenamento local auxilia e até criamos um banco de dados local. Mas ainda falta algo, certo?
Se um aplicativo utiliza o Coucbase (armazenamento local), mas em outras funcionalidades utiliza o banco de dados na nuvem, é preciso programar o aplicativo de forma que os dois bancos se comuniquem, evitando erros nos dados.
Assim, vamos ver como conectar o banco local com o servidor.
Felizmente, o Couchbase facilita esse processo.
O que é Couchbase?
O Couchbase é uma plataforma de banco de dados que combina o melhor do NoSQL e SQL.
Se você ainda não está familiarizado com esses conceitos, aproveite o artigo: SQL e NoSQL: trabalhando com bancos relacionais e não relacionais.
O Couchbase oferece uma infinidade de funcionalidades, e entre elas está a sincronização entre banco local e remoto, que iremos usar neste artigo. Para começar, vamos configurar a sincronização usando o Couchbase Capella e o Sync Gateway.
Vamos começar!
Como utilizar o Couchbase no Flutter?
Aqui vai um passo a passo de como usar o Couchbase no Flutter:
Passo 1: Criando uma conta no Couchbase
- Acesse a plataforma Couchbase;
- Crie uma conta gratuita (ou entre com a sua);
- Crie um projeto com o nome da sua preferência;
- No painel, crie um novo cluster e defina um nome.
Com a conta e o cluster criados, siga para o próximo passo.
Passo 2: Bucket e Collection
Para armazenar nossos dados, precisamos criar um bucket e uma collection. O bucket é como uma grande caixa, e dentro dele estão as caixas menores, as collections, que são as responsáveis por armazenar, de fato, os dados. Siga os passos abaixo:
- Acesse o data tools (ferramentas de dados);
- Dentro do data tools, crie um bucket chamado
moodbucket
; - Dentro do
moodbucket
, crie o scopemoodscope
; - No escopo, adicione a collection
moodcollection
.
Passo 3: Configuração de serviço de app e endpoint:
- Vá para App Services e crie um serviço chamado
moodservice
; - Abra o serviço e adicione um endpoint e conecte à collection
moodcollection
; - Dentro do endpoint, acesse App Users (se necessário clique em “resume endpoint” para ativar o endpoint);
- Configure um novo user e atribua o canal
moodcollection
.
Passo 4: Configurando o projeto
Agora, dentro do nosso projeto Flutter, modificaremos a nossa classe de constantes do Couchbase, e adicionaremos alguns atributos que irão nos auxiliar a fazer a sincronização com o servidor remoto:
class CouchbaseContants {
static String userName = 'teste'; //nome do seu user
static String password = 'teste'; //senha do user
static String publicConnectionUrl ='endpoint'; //seu endpoint
static const String channel = 'moodcollection';
static const String collection = 'moodcollection';
static const String scope = 'moodscope';
}
Com todos os atributos adicionados, modifique a classe do nosso banco de dados para adicionar as funções e atributos que serão responsáveis pela sincronização:
class DatabaseService {
AsyncDatabase? database;
AsyncReplicator? replicator;
//outros métodos
Future<bool> addMood(MyMood mood) async {
final collection = await database?.createCollection(
CouchbaseContants.collection,
CouchbaseContants.scope,
);
if (collection != null) {
final document = MutableDocument(mood.toMap());
final resultSave = await collection.saveDocument(
document,
ConcurrencyControl.lastWriteWins,
);
if (resultSave) {
startReplication(
collectionName: CouchbaseContants.collection,
onSynced: () {
print('Sincronizado');
},
);
}
return resultSave;
}
return false;
}
Future<void> startReplication({
required String collectionName,
required Function() onSynced,
}) async {
final collection = await database?.createCollection(
collectionName,
CouchbaseContants.scope,
);
if (collection != null) {
final replicatorConfig = ReplicatorConfiguration(
target: UrlEndpoint(
Uri.parse(CouchbaseContants.publicConnectionUrl),
),
authenticator: BasicAuthenticator(
username: CouchbaseContants.userName,
password: CouchbaseContants.password,
),
continuous: true,
replicatorType: ReplicatorType.pushAndPull,
enableAutoPurge: true,
);
replicatorConfig.addCollection(
collection,
CollectionConfiguration(
channels: [CouchbaseContants.channel],
conflictResolver: ConflictResolver.from(
(conflict) {
return conflict.remoteDocument ?? conflict.localDocument;
},
),
),
);
replicator = await Replicator.createAsync(replicatorConfig);
replicator?.addChangeListener(
(change) {
if (change.status.error != null) {
print('Ocorreu um erro na replicação');
}
if (change.status.activity == ReplicatorActivityLevel.idle) {
print('ocorreu uma sincronização');
onSynced();
}
},
);
await replicator?.start();
}
}
Além de adicionar o atributo replicator
, criamos também a função startReplication
, que é responsável por sincronizar os nossos dados. O método de carregar humor foi modificado e, agora, usa a nossa função de sincronizar dados.
Pronto, finalizamos o app! Agora é só abrir a aplicação e testar.
Offline first no Flutter e exemplos
Nossa aplicação foi desenvolvida para sincronizar o banco de dados local com o remoto. O mais interessante é que ela funciona sem conexão com a internet, graças ao armazenamento local.
Na prática, sem perceber, adotamos o conceito de Offline First. Você provavelmente já encontrou essa abordagem em aplicativos populares.
No WhatsApp, por exemplo, quando você envia uma mensagem e está offline, ela fica pendente e é enviada automaticamente assim que a conexão volta.
No Google Maps, você pode navegar por rotas salvas mesmo sem internet. E esses são só alguns exemplos, já que essa abordagem está presente em muitos outros aplicativos.
O que é offline first?
Offline First é quando um aplicativo Flutter continua funcionando sem conexão com a internet. Assim, a pessoa usuária tem uma experiência mais fluida e sem interrupções, mesmo quando estiver sem internet, e neste artigo, fizemos tudo isso na prática.
Conclusão
Você mergulhou no Offline First, utilizando o Couchbase.
Mas não paramos por aqui!
Se você quer aprender ainda mais sobre os princípios do Offline First, a Alura preparou uma lista de conteúdos especiais:
- Flutter: implemente Offline First com Couchbase em um app
- Couchbase Lite e Capella App Services com Dart e Flutter
- Use Supabase with Flutter
- Adicionar o Firebase ao seu app Flutter
Até a próxima!