DEV Community

Cover image for Eu tenho um sonho ... offline-first em Flutter
Filipe Nanclarez
Filipe Nanclarez

Posted on • Updated on

Eu tenho um sonho ... offline-first em Flutter

Quando pensamos em desenvolvimento de aplicativos, logo nos lembramos dos problemas de conexão que todos os usuários enfrentam. E como desenvolvedor com certeza você já deve ter ouvido a pergunta de um milhão: "como fazer offline-first?". Sempre que fiz essa pergunta a resposta foi "isso é complicado". Ao mesmo tempo sempre ouço "tem o firebase, mas ...".

De fato a solução da Google é muito atraente. Principalmente para quem está começando e não quer gastar muita energia com a parte de backend, servidores, configuração de clusters, etc.

Mas ele tem suas limitações, e essas limitações acabam levando os desenvolvedores a buscar outras soluções cedo ou tarde, principalmente quando esbarram em algum problema que ele não pode resolver.

No meu caso, eu até poderia aceitar a ideia de pagar um pouco mais pelo serviço, afinal era só uma questão matemática, o quanto eu iria cobrar dos meus usuários seria equivalente ao uso. Quanto mais usuários, mais receita e o custo ia crescer de acordo. Mas o que me fez ir atrás de outra solução foi uma limitação técnica. O limite do tamanho do registro do Firestore. Conforme a documentação um documento não pode ter mais de 1 MiB. Meu aplicativo precisa de mais que isso. Tentei quebrar o conteúdo, mas isso me trouxe mais problemas do que soluções.

Acabei focando em outras partes do aplicativo que precisavam ficar prontas, e deixei esse assunto na gaveta. Até que um dia me deparei com o artigo How to make your Flutter app offline-first with Couchbase Lite. Isso me chamou bastante atenção e após ler ele superficialmente deixei ele guardado na gaveta.

Algum tempo depois meu aplicativo já estava publicado, os usuários estavam contentes, e o problema que eu havia conscientemente jogado para o canto da mesa, agora precisava ser resolvido.

Então arregacei as mangas e comecei a trilhar esse caminho.

O que vem a seguir é baseado nesse artigo do Gabriel Terwesten e na documentação do próprio site da Couchbase. Algumas imagens foram tiradas dessas fontes.

Os pacotes usados nesse artigo até o momento não dão suporte para web, então se você tem projetos web não vai conseguir, por enquanto, usar essa solução. 😉

Índice

Mas o que é essa tal de Couchbase?

A Couchbase fornece uma solução de banco de dados bem robusta e com características interessantes. A solução é de código aberto, e possui uma versão gratuita mantida pela comunidade.

No caso do flutter, o autor do artigo mencionado acima explica porque ele criou os packages que vamos usar abaixo.

Ao procurar esses packages no pub.dev você talvez fique confuso porque são vários packages separados. Segundo o artigo, basicamente o pacote com que nós iremos interagir é o cbl_flutter ou o cbl_dart. Se você como eu está se perguntado se deve usar o cbl_dart porque seu aplicativo precisa rodar em qualquer plataforma, fique tranquilo, o cbl_flutter também roda.

Esse pacote depende do clb, que depende do cbl_ffi que depende de outros e assim por diante. Para facilitar vamos nos concentrar apenas nas dependências necessárias e fazer uma POC simples pra ver o negócio funcionando.

O aplicativo...

Nosso foco é o couchlite, então não vamos perder tempo com layout nem perfumaria. Vamos fazer o seguinte, vamos pegar o projeto padrão do flutter, e na rotina que incrementa o contador, vamos fazer o aplicativo gravar uma informação no banco. Simples assim! Com isso vamos entender o funcionamento e arquitetura da solução da Couchbase sem gastar energia com detalhes que apenas ofuscariam nossa visão do conceito principal.

Então vamos lá.

Crie uma pasta com o nome que quiser, e estando dentro dela abra seu prompt e inicie o projeto padrão do flutter.

flutter create .

Isso vai criar aquele projeto que todos nós conhecemos do contador:

Projeto padrão flutter em windows

Agora vamos adicionar as dependências no pubspec.yaml. Se você for no pub.dev procurar o package cbl, e for ler o readme, vai ficar surpreso com a parte da instalação:

pubspec.yaml do package cbl

Legal, eles não colocaram as versões. Mas isso nunca foi um problema certo, basta eu usar o método flutter pub add.

Então vamos fazer isso para todos os pacotes que precisamos. Você já vai entender porque estou detalhando um passo tão óbvio.

De cima pra baixo, vamos instalar:

O pacote principal da biblioteca:
flutter pub add cbl

Depois o pacote do clb_flutter
flutter pub add cbl_flutter

E por último o pacote especifico do flutter, na versão comunity do couchlite:
flutter pub add cbl_flutter_ce

E aí somos surpreendidos com esse erro curioso:

Because cbl_flutter_platform_interface >=1.0.0-beta.1 <1.0.0-beta.2 depends on cbl ^1.0.0-beta.7 and cbl_flutter_platform_interface <1.0.0-beta.1 depends on cbl ^1.0.0-beta.6, cbl_flutter_platform_interface <1.0.0-beta.2 requires cbl ^1.0.0-beta.6.
And because cbl_flutter_ce <1.0.0-beta.2 depends on cbl_flutter_platform_interface ^1.0.0-beta.0 and cbl_flutter_ce >=1.0.0-beta.2 <1.0.0-beta.3 depends on cbl_flutter_platform_interface ^1.0.0-beta.2, cbl_flutter_ce <1.0.0-beta.3 requires cbl ^1.0.0-beta.6 or cbl_flutter_platform_interface ^1.0.0-beta.2.
And because cbl_flutter_platform_interface >=1.0.0-beta.2 <1.0.0-beta.3 depends on cbl ^1.0.0-beta.8 and cbl_flutter_ce >=1.0.0-beta.3 <1.0.0-beta.4 depends on cbl_flutter_platform_interface ^1.0.0-beta.3, cbl_flutter_ce <1.0.0-beta.4 requires cbl ^1.0.0-beta.6 or cbl_flutter_platform_interface ^1.0.0-beta.3.
And because cbl_flutter_platform_interface >=1.0.0-beta.3 <1.0.0-beta.4 depends on cbl ^1.0.0-beta.10 and cbl_flutter_ce >=1.0.0-beta.4 <1.0.0-beta.6 depends on cbl_flutter_platform_interface ^1.0.0-beta.4, cbl_flutter_ce <1.0.0-beta.6 requires cbl ^1.0.0-beta.6 or cbl_flutter_platform_interface ^1.0.0-beta.4.
Because cbl_flutter_platform_interface >=1.0.0-beta.5 <1.0.0-beta.6 depends on cbl ^1.0.0-beta.13 and cbl_flutter_platform_interface >=1.0.0-beta.4 <1.0.0-beta.5 depends on cbl ^1.0.0-beta.11, cbl_flutter_platform_interface >=1.0.0-beta.4 <1.0.0-beta.6 requires cbl ^1.0.0-beta.11.
Thus, cbl_flutter_ce <1.0.0-beta.6 requires cbl ^1.0.0-beta.6 or cbl_flutter_platform_interface ^1.0.0-beta.6.
And because cbl_flutter_ce >=1.0.0-beta.6 depends on cbl_flutter_platform_interface ^1.0.0-beta.6 which depends on cbl ^1.0.0-beta.15, every version of cbl_flutter_ce requires cbl ^1.0.0-beta.6.
So, because poc_flutter_couchbase depends on both cbl ^0.6.0+1 and cbl_flutter_ce any, version solving failed.
Enter fullscreen mode Exit fullscreen mode

O erro já explica o que está acontecendo. As versões do pub.dev não estão sincronizadas corretamente.

Pra conseguir pegar a versão correta, basta clicar na versão 'Prerelease' lá no pub.dev. Com isso será possível ver as versões que ainda estão em desenvolvimento

link pre-release no pub.dev

Se você não quer perder tempo com isso, basta colocar as seguintes versões no seu pubspec.yaml:

  cbl: ^1.0.0-beta.15
  cbl_flutter: ^1.0.0-beta.13
  cbl_flutter_ce: ^1.0.0-beta.6
Enter fullscreen mode Exit fullscreen mode

Agora vamos ao código. Você pode vasculhar a documentação no site da Couchbase e também dos pacotes para ver exemplos de como fazer um CRUD simples.

No nosso caso, vamos manter o foco em inserir alguma coisa no banco. Então primeiro precisamos iniciar o banco. Para isso vamos adicionar no método iniState um 'PostFrameCallback' com a chamada para iniciar e abrir o banco. Se você é novo em flutter, estamos colocando aqui, para que essa inicialização aconteça apenas uma vez.

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
      await CouchbaseLiteFlutter.init();
      database = await Database.openAsync('database-example');

    });
  }
Enter fullscreen mode Exit fullscreen mode

Depois de iniciar o banco, agora podemos usar ele em nossa aplicação.

Na função _incrementCounter() vamos inserir os comandos para inclusão de um registro no banco. Basicamente criamos um documento e depois usamos o método saveDocument() pra salvar ele no banco:

  Future<void> _incrementCounter() async {
    final doc = MutableDocument({
      'type': 'logMessage',
      'createdAt': DateTime.now(),
      'message': 'teste',
    });

    await database?.saveDocument(doc);

    setState(() {
      _counter++;
    });
  }
Enter fullscreen mode Exit fullscreen mode

Agora para podermos ver o registro que foi inserido, vamos precisar consultar o banco e trazer os resultados.

Então vamos fazer um select simples, e imprimir isso no log. Mude novamente a função para isso:

    Future<void> _incrementCounter() async {
    final doc = MutableDocument({
      'type': 'logMessage',
      'createdAt': DateTime.now(),
      'message': 'teste',
    });

    await database?.saveDocument(doc);

    // consultando dados no banco
    final query = const QueryBuilder()
        .select(
          SelectResult.expression(Meta.id),
          SelectResult.property('createdAt'),
          SelectResult.property('message'),
        )
        .from(DataSource.database(database!))
        .where(
          Expression.property('type').equalTo(Expression.value('logMessage')),
        )
        .orderBy(Ordering.property('createdAt'));

    ResultSet resultSet = await query.execute();
    var results = await resultSet.asStream().map((result) => result.toPlainMap()).toList();

    print(results[0]['id']);
    print(results[0]['createdAt']);
    print(results[0]['message']);

    await replicator.start();

    setState(() {
      _counter++;
    });
  }
Enter fullscreen mode Exit fullscreen mode

Execute o aplicativo, aperte o botão de incrementar e se tudo der certo você verá os dados que acabou de inserir no console assim:

console exibindo os dados inseridos

Com isso já temos um aplicativo gravando dados e exibindo esses dados. Uma POC bem simples, pra mostrar como funciona a parte 'lite' do Couchbase.

Vamos agora dar um passo a mais.

O servidor...

Nós começamos esse artigo falando sobre a necessidade do offline-first e do Firestore. Agora nós criamos uma POC gravando dados num app. Mas isso está mais parecido com Sqlite do que com Firestore certo? Certo!

Para que tudo isso fique mais parecido com o Firestore, precisamos de um servidor, e precisamos que o aplicativo e o servidor sincronizem os dados de forma transparente para o usuário ( se também for transparente para os desenvolvedores... melhor ainda ).

Então vamos lá, vamos instalar um servidor de banco de dados. Vamos continuar mantendo as coisas simples e diretas, mas para que você entenda a proposta da solução, a ideia é que no final você possa contratar uma VM na nuvem, e você tenha seu próprio servidor. Por exemplo você poder usar o serviço Lightsail da AWS ou ainda usar o EC2 e ter um servidor gratuito por um ano. Ou usar outros provedores para ter seu próprio servidor na nuvem. Depois você instala o Couchserver nesse servidor, e com isso você terá uma solução para usar no lugar do Firestore, com a vantagem de não ter as limitações que já mencionamos.

Então vamos lá. Vamos instalar o servidor no nosso computador local mesmo, e fazer isso tudo funcionar. Depois pensamos em como colocar isso em um servidor na nuvem.

Para baixar o Couchbase server acesse a página oficial nesse link

Em Couchbase Server escolha a versão community, no meu caso estou usando essa aqui:

página de downloads do site couchbase.com

Depois da instalação, seguindo a documentação em Create a Cluster devemos acessar o console do Couchbase Server pelo navegador na porta padrão 8091. Vamos cair nessa página:

pagina inicial do couchbase

Vamos escolher a opção "Setup New Cluster"

O nome do cluster fica a seu critério. Deixei o meu setup da seguinte forma:

Image description

Após isso verá a tela dos termos. Após as opções de aceitar os termos, há duas opções; terminar com os padrões ou configurar disco memória e serviços.

Para fins didáticos, vamos escolher a opção 'Configure Disk, Memory, Services'.

Image description

Nessa tela, podemos ver as opções disponíveis. Eu não vou mudar nada no meu setup, mas achei bom trazer vocês até aqui para vocês conhecerem as opções disponíveis.

Image description

Após clicar em finalizar, seremos redirecionados para o Dashboard principal desse nó, conforme imagem abaixo:

Image description

Agora vamos precisar criar nosso bucket, para armazenar nossos dados. Clique em bucket no menu de opções:

Image description

Depois clique em 'ADD BUCKET'

Image description

Aqui vou apenas escolher o nome. No print abaixo, deixei aberta as opções avançadas apenas para fins didáticos:

Image description

O gateway...

A comunicação com o servidor não é direta. É necessário um intermediário entre uma instancia local do CouchLite (seu aplicativo) e o Servidor Couchbase.

Para entender melhor o fluxo, veja essa imagem tirada do site da couchbase:

Image description

Ou seja, nosso aplicativo fala como gateway, e o gateway fala com o servidor. Então bora instalar esse cara!

A instalação segue o padrão NNF (Next, Next, Finish).

Após finalizar, vamos ter as informações sobre a porta em que o gateway estará rodando. Anote isso!

Image description

Agora vamos acessar esse endereço que anotamos.

Deve aparecer isso na tela:

Image description

Isso significa que deu certo a instalação. Caso não tenha aparecido esse retorno da API pare aqui e repasse a instalação antes de seguir.

Agora precisamos configurar o Gateway para acessar o servidor do CouchBase. Primeiro vamos criar um usuário para que o gateway possa acessar o servidor.

Acesse Security e depois o botão 'ADD USER' conforme imagem abaixo:

Image description

Preencha o nome, e a senha, e nas permissões deixe da seguinte forma:

Image description

Agora vamos parar serviço do gateway lá em services.msc (no windows). Depois que o serviço estiver parado vamos editar o arquivo de configurações do gateway. Na minha instalação está em c:\Program Files\Couchbase\Sync Gateway\serviceconfig.json. Após achar o arquivo edite ele e altere o json pra ficar parecido com isso (preencha com as suas informações é claro, principalmente a senha):

{
  "adminInterface": "127.0.0.1:4985",
  "interface": "0.0.0.0:4984",
  "databases": {
    "my-database": {
      "server": "http://127.0.0.1:8091",
      "bucket": "my-database", 
      "username": "sync_gateway", 
      "password": "******",
      "enable_shared_bucket_access": true, 
      "import_docs": true, 
      "num_index_replicas": 0, 
      "users": {
        "GUEST": { "disabled": false, "admin_channels": ["*"] } 
      }
    }
  },
  "logging": { 
    "console": {
      "log_level": "debug",
      "log_keys": ["*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Depois disso, inicie novamente o serviço. Você deve conseguir ver o retorno da API novamente ao tentar acessar o gateway pelo navegador em localhost/4985. Se não conseguir, pare e reveja passo a passo. Uma dica importante. Eu perdi um tempo considerável aqui, por conta de porta travada no computador. Se você reviu o passo a passo, e ainda não consegue acessar, sugiro que reinicie o computador. No meu caso foi o suficiente para o gateway responder corretamente após os ajustes.

Conferindo a comunicação

Para termos certeza de que tudo está ok, vamos inserir dados no servidor via API do gateway. Poderíamos fazer essa requisição de várias formas, usando postman por exemplo. Mas para facilitar vou usar a requisição com o curl, via powershell com o cmdlet Invoke-WebRequest. Abra uma janela do powershell e cole o comando abaixo (lembre de trocar o nome do bucket na url):

iwr http://localhost:4984/my-database/ `
-Method 'POST' `
-ContentType 'application/json; charset=utf-8' `
-Body '{ 
  "_id": "first-doc", 
  "name": "Hello Word Couchbase", 
  "type": "teste-doc", 
  "data": "Hello Word" 
}'
Enter fullscreen mode Exit fullscreen mode

Devemos obter esse retorno:

Image description

Agora vamos no couchserver, verificar o documento criado:

Image description

Image description

Olha ele aí:

Image description

Com isso sabemos que o gateway está ok. Agora vamos voltar ao flutter.

O replicador...

Vamos adicionar no PostFrameCallback um replicador. Esse cara será chamado toda vez que quisermos sincronizar alguma coisa com o servidor. Pode ser após inserir uma informação, ou quando o aplicativo iniciar, ou quando ele voltar a ter conexão com a internet, enfim, você que escolhe em que momento ele deverá fazer isso. No momento da sincronização ele envia e recebe os dados do servidor (via gateway) e em caso de conflito (se o registro foi editado tanto no servidor quanto na aplicação antes da sincronização) a resolução de conflitos padrão escolhe como vencedor o último registro que foi gravado.

Abaixo o código modificado do método iniState(). Repare que também adicionamos um listener para printar o status do replicador. Você pode por exemplo usar isso para exibir algum loader no aplicativo. Aqui estamos apenas imprimindo no console as mudanças de estado:

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
      await CouchbaseLiteFlutter.init();
      database = await Database.openAsync('example.couchbase');

      // replicador
      replicator = await Replicator.create(
        ReplicatorConfiguration(
          database: database!,
          target: UrlEndpoint(Uri.parse('ws://localhost:4984/my-database')),
        ),
      );

      await replicator.addChangeListener((change) {
        debugPrint('Replicator activity: ${change.status.activity}');
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

esse código só cria o 'replicator' mas não dispara uma sincronização. Então vamos adicionar uma sincronização logo após o usuário apertar o botão para incrementar o contador. Assim o sistema irá adicionar um registro e logo em seguida irá disparar uma sincronização:

  Future<void> _incrementCounter() async {
    final doc = MutableDocument({
      'type': 'logMessage',
      'createdAt': DateTime.now(),
      'message': 'teste',
    });

    await database?.saveDocument(doc);

    await replicator.start();

    setState(() {
      _counter++;
    });
  }
Enter fullscreen mode Exit fullscreen mode

Faça o teste, e se tudo der certo, você verá no console do couchserver os documentos. Se você for como eu, talvez dê alguns pulos de alegria também.

Image description

Você pode fazer alguns testes parando o serviço do Couchbase Server, gravando dados no aplicativo, depois iniciando o serviço denovo e incluindo mais dados e depois verificando se tanto os dados gerados enquanto você estava com o servidor parado quanto os dados gerados após o servidor estar novamente em execução foram gravados corretamente.

Conclusão

O que fizemos até aqui já deve ter dado uma bela clareada na sua mente sobre como isso tudo funciona. Mas é claro que isso é uma POC bem simples, e com certeza precisamos fazer mais coisas, como exibir os dados de maneira mais elegante, filtrar os dados a serem sincronizados (afinal não queremos dados de todos os outros usuários que usam o aplicativo sobrecarregando nosso celular), criar alguma forma de autenticar em nosso gateway para evitar que qualquer um possa inserir dados via powershell como fizemos, sem falar na parte de configurar um servidor na nuvem para que a coisa fique mais parecida com a realidade.

Ainda temos um bom caminho pela frente.

No próximo artigo vamos dar uma olhada em tudo isso.

Oldest comments (3)

Collapse
 
joelsongsouzza profile image
Joelson Gonçalves

Parece bem interessante! Queria ter lido antes de ter feito uma solução própria, teria me ajudado bastante. Foi muito bem escrito, obrigado!

Collapse
 
adilsonjunior profile image
Adilson Junior

Muito bom! Obrigado por compartilhar!

Collapse
 
douglascarterbor profile image
Douglas Carteri Bordignon

Parabéns Filipe, muito interessante a proposta e muito bem escrito!