Alura > Cursos de Mobile > Cursos de Flutter > Conteúdos de Flutter > Primeiras aulas do curso Flutter: praticando e adaptando arquitetura limpa

Flutter: praticando e adaptando arquitetura limpa

Responsabilidades e camada domínio - Apresentação

Você já ouviu falar de Clean Architecture (Arquitetura Limpa)? Já utilizou em um contexto Flutter?

Te desejo boas-vinda a mais um curso de arquitetura e Clean Code! Meu nome é Matheus e serei seu instrutor nesta jornada!

Audiodescrição: Matheus se autodescreve como um homem branco, de cabelos curtos e lisos na cor castanho escuro. Veste uma camiseta azul marinho com a logo da Alura em branco e está sentado sob uma cadeira gamer na cor preta. Ao fundo, uma parede iluminada sob tons de roxo e azul. À sua direita, uma estante com artigos decorativos.

Neste curso, vamos desenvolver um aplicativo Hyrule, que é um compêndio de informações do jogo Zelda Breath of the Wild. Mas qual é o resultado que vamos atingir? Teremos uma tela com várias categorias de itens, como monstros e criaturas. Ao clicar em uma dessas categorias, baixaremos entradas vindas de uma API. Para fazer essa conexão com a API, vamos utilizar a biblioteca dio.

Clicando em uma entrada, temos a possibilidade de adicionar essa entrada no nosso banco de dados. Para fazer essa conexão entre a tela e o banco de dados, vamos usar a biblioteca Floor. Essa biblioteca vai permitir que nós também façamos inserção e remoção de dados no banco de dados, além da visualização de todas as entradas em uma tela de favoritos. Podemos também deslizar uma entrada para excluí-la do banco de dados.

Para acompanhar neste curso, é importante que você já tenha conhecimentos prévios de Flutter, saiba construir layouts, entenda de persistência de dados e consiga fazer transições de telas, passando informações de uma para a outra, além de fazer requisições HTTP.

Com todo esse conhecimento pronto, podemos prosseguir e começar a desenvolver o aplicativo Hyrule. Vejo você no próximo vídeo!

Responsabilidades e camada domínio - Entendendo as camadas para organizar o projeto

Antes de começar, é importante que você já tenha baixado os arquivos do projeto disponibilizados na atividade anterior de preparação do ambiente.

Vale ressaltar que esse curso foi inteiramente planejado, pensado e inspirado pelo Clean Code (Código Limpo) e também pelo Clean Dart (Dart Limpo), que é um padrão de projeto desenvolvido pelo time do Fluterando. Todas as decisões que serão tomadas durante o desenvolvimento do projeto são adaptações minhas, como instrutor, inspiradas nessas ferramentas. Vale reforçar que o projeto não deve se adaptar à nossa arquitetura, nossa arquitetura é que precisa se adaptar ao nosso projeto.

Com esse esclarecimento resolvido, vamos ao desenvolvimento do projeto!

Nós vamos desenvolver um aplicativo chamado Hyrule, que será um estilo compendium do jogo Zelda Breath of the Wild. Este aplicativo servirá como uma ferramenta para as pessoas jogadoras terem acesso às informações de itens, monstros e outras características do jogo.

Analisando rapidamente o layout que temos no Figma, nós temos quatro telas para desenvolver, onde teremos uma categoria para escolher, as informações da categoria selecionada que será buscada de uma API, os detalhes e, finalmente, uma tela de itens salvos, onde as informações ficarão armazenadas dentro do dispositivo.

O aplicativo terá uma lista de requisitos que inclui uso da API do Hyrule Compendium, disponível também na descrição do projeto no GitHub. Nós vamos buscar as informações de acordo com a categoria selecionada pela pessoa usuária. Esta pessoa também deve ser capaz de visualizar todas as entradas daquela categoria, adicionar e remover uma entrada aos favoritos e, além disso, todas as informações precisam ser salvas localmente no dispositivo. Assim, teremos acesso à API e ao nosso banco de dados.

Tendo em vista todas essas informações, por onde vamos começar? Pensando no que o cliente solicita, nosso aplicativo é dividido em várias camadas. Vamos iniciar pelo núcleo, onde temos nosso domínio, modelos, entidades e regras de negócio.

É importante lembrar, falando de arquitetura, que o desenvolvimento móvel, de forma geral (Android, iOS, Flutter), já trabalha com arquitetura em camadas naturalmente. O Clean será apenas um padrão a mais que vamos adicionar a isso.

Vamos então definir nossas camadas através das pastas dentro do nosso projeto.

No Visual Studio Code, que é a IDE escolhida para desenvolver esse aplicativo, dentro da pasta lib, criaremos uma pasta chamada domain, onde ficarão nossos domínios. Dentro da pasta domain, criaremos outra pasta chamada models, pois poderemos ter mais de um modelo no futuro. Nela, criaremos nosso arquivo de entrada, entry.dart.

Este arquivo conterá a estrutura das informações do aplicativo, que inclui o nome, título do cartão, ID e uma sequência de informações que esse cartão precisa. Para saber como montar nossa entrada, podemos visualizar o resultado da API e usar isso como referência para definir nosso modelo.

Vamos abrir o resultado de uma API que contém categorias de monstros:

'data": [
    {
        "category": "monsters",
        "common_locations": [
            "Eldin Mountains",
            "Tabantha Frontier"
        ],
        "description": "A spirit of fire has taken the form of this giant dragon. Making its home in the Eldin region, it's said to have served the Spring of Power since ancient times. An old saying goes, \"The dragon ascends to the heavens as the sun begins to set,\" but nobody has witnessed this in the current age. The flames that coat its body make it dangerous to get near, but Dinraal bears no ill will toward people.",
        "dlc": false,
        "drops": null,
        "id": 153,
        "image": "https://botw-compendium.herokuapp.com/api/v3/compendium/entry/dinraal/image",
        "name": "dinraal"

Note que ispomos de algumas informações como category (categoria), common_locations (localização comum), description (descrição), id (ID), image (imagem) e name (nome). Essas são as seis informações que precisamos.

Vamos começar de baixo para cima, ou seja, pelo ID, assim seguimos uma ordem de importância. Vamos alternar entre essa página e o editor de código, para que possamos usá-la como uma espécie de referência para escrever esse modelo de entrada.

No editor de código, definiremos uma classe Entry. Ela precisa ter um int, que será nosso id. Também precisamos de uma string para o nome, uma para a imagem e uma para a descrição.

class Entry {
    int id;
    String name;
    String image;
    String description;
}

Temos também commonLocations. Mas há uma ressalva: temos uma lista de strings. Contudo, considerando que temos que salvar essas informações localmente no banco de dados, e sabendo que no banco de dados não podemos salvar um vetor ou uma lista, mas apenas strings, não conseguiremos lidar com isso diretamente.

Portanto, embora a gente declare commonLocations como string, posteriormente teremos que transformar isso em um objeto JSON e de JSON para uma lista novamente.

class Entry {
    int id;
    String name;
    String image;
    String description;
    String commonLocations
}

Por último, temos a string de category (categoria).

class Entry {
    int id;
    String name;
    String image;
    String description;
    String commonLocations
    String category;
}

Bem, concluímos a escrita da nossa entrada, mas ainda faltam algumas informações. Estamos recebendo erros porque falta adicionar nosso construtor e nossa estrutura de dados da classe. Mas podemos começar a usar ferramentas para facilitar nosso trabalho ao invés de fazer tudo manualmente.

À medida que desenvolvemos aplicativos maiores ou aumentamos a frequência de desenvolvimento, ganhamos familiaridade com a forma como o código é realmente escrito. É normal usarmos essas ferramentas, afinal não vale a pena fazer tudo manualmente toda vez se já temos maneiras de facilitar nosso trabalho.

A primeira ferramenta que vamos implementar é um plugin do Visual Studio Code, que vai gerar para nós a nossa estrutura de dados. O nome do plugin que vamos instalar é "Data Class Generator", criado pelo hzgood, que será capaz de gerar todas as informações da estrutura de dados no Dart. Vamos pesquisá-lo na aba de extensões vou instalá-lo.

Após instalado, podemos fechar a aba e reiniciar o Visual Studio Code. O atalho "Command + Shift + P" reinicia o VS Code no MAC; já no Windows é possível reiniciar a IDE com o comando "Ctrl + Shift + P". Feito isso, digitamos reload e selecionamos a opção "Developer: Reload Window" para recarregar a janela do VS Code. Com isso temos acesso ao novo plugin que acabamos de instalar.

Note que o editor está emitindo vários erros na nossa entrada. Se colocarmos o cursor em cima de id e pressionarmos "Command + ponto" ou "Ctrl + ponto", temos acesso às opções de que gerar a classe de dados, o construtor, a serialização JSON e várias outras. Queremos nosso construtor, então clicaremos em "Generate constructor".

Ao clicar, o construtor é gerado:

// ignore_for_file: public_member_api_docs, sort_constructors_first

class Entry {
    int id;
    String name;
    String image;
    String description;
    String commonLocations;
    String category;
    Entry({
        required this.id,
        required this.name,
        required this.image,
        required this.description,
        required this.commonLocations,
        required this.category,
    });
}

Agora, posicionamos o cursor sobre Entry, usamos o atalho "Command + ponto" ou "Ctrl + ponto" e clicamos na opção "Generate JSON serialization" para gerar a serialização JSON:

// ignore_for_file: public_member_api_docs, sort_constructors_first

class Entry {
    int id;
    String name;
    String image;
    String description;
    String commonLocations;
    String category;
    Entry({
        required this.id,
        required this.name,
        required this.image,
        required this.description,
        required this.commonLocations,
        required this.category,
    });
    
    Map<String, dynamic> toMap() { 
        return <String, dynamic>{
            'id': id,
            'name': name,
            'image': image,
            'description': description,
            'commonLocations': commonLocations,
            'category': category,
        };
    }
    
    factory Entry.fromMap(Map<String, dynamic> map) {
        return Entry(
                id: map['id'] as int, 
                name: map['name'] as String,
                image: map['image'] as String,
                description: map['description'] as String,
                commonLocations: map['commonLocations'] as String,
                category: map['category'] as String,
        );
    }

    String toJson() => json.encode(toMap());
    
    factory Entry.fromJson(String source) => Entry.fromMap (json.decode(source) as Map<String, dynamic>);
}

Todo o código já foi gerado. Portanto, agora temos a função toMap(), da nossa entrada, fromMap() e também toJson(), encode() e fromJson(), com todas as informações que precisamos.

Ainda falta um detalhe: o commonLocations que teremos que converter de uma string para uma lista e salvar no banco de dados para então transformá-lo em uma string novamente. Faremos isso no próximo vídeo!

Responsabilidades e camada domínio - Convertendo objetos e listas

Um detalhe muito importante a ser lembrado é que escrevemos commonLocations, dentro da nossa entrada, utilizando Camel Case. Ou seja, as palavras são delimitadas por letras maiúsculas. Assim, "commons" começa com a letra minúscula e "Locations" com a letra maiúscula.

Contudo, se observarmos a API, veremos que a chave de mesmo nome está escrita como Snake Case. O que significa que a palavra é separada por um underscore (sublinhado): common_locations.

'data": [
    {
        "category": "monsters",
        "common_locations": [
            "Eldin Mountains",
            "Tabantha Frontier"
        ],

Então, quando formos buscar essa informação na API, teremos um problema, afinal, nós usamos o padrão Dart de escrever como Camel Case. Portanto, temos um ajuste a ser feito ao extrair as informações da API.

Voltando ao VS Code, abaixo da linha 41, criaremos uma função que irá converter nossa string em uma lista de strings. Esta função será chamada commonLocationsConverter() e irá retornar um jsonDecode(), pois queremos transformar os dados originados do JSON, que serão armazenados no banco de dados, em uma lista de strings que possa ser usada posteriormente em outras partes da API.

Nossa source será nossa própria commonLocations dentro da entrada. A partir disso, precisamos converter isso para uma lista usando .as List. Como será uma lista e será um JSON, teoricamente, temos que dar seu tipo como dynamic.

factory Entry.fromMap(Map<String, dynamic> map) {
    return Entry(
        id: map['id'] as int, 
        name: map['name'] as String,
        image: map['image'] as String,
        description: map['description'] as String,
        commonLocations: map['commonLocations'] as String,
        category: map['category'] as String,
        );
    }
        
List<String> commonLocationsConverter() {
    return (jsonDecode(commonLocations) as List<dynamic>)
}

Depois disso, precisamos mapear, pois agora que é uma lista, temos que pegar cada um desses itens, convertê-los em uma string e depois retornar. Então, será e as String. No final de tudo isso, faremos um toList().

List<String> commonLocationsConverter() {
    return (jsonDecode(commonLocations) as List<dynamic>).map((e) => e as String).toLidt();
}

Essa função está convertendo o JSON recebido do banco de dados em uma lista de string para que possamos usar em outras partes do projeto.

Agora, para corrigir commonLocations, precisamos transformar esse o map para outra coisa.

Na linha 38, temos map['commonLocations']. Esse map[], com essa chave, é a partir de onde a busca na API acontece. Mas temos que alterar isso, pois precisamos usar a chave correta. Além disso, ao recebermos a informação da API, também precisamos convertê-la para algo que o banco consiga salvar, que será o objeto JSON.

Faremos um jsonEncode(). O objeto que receberemos será o map[], passando a chave. Só que a chave agora, ao invés de ser 'commonLocations', em Camel Case, será 'common_locations', em Snake Case.

factory Entry.fromMap(Map<String, dynamic> map) {
        return Entry(
                id: map['id'] as int, 
                name: map['name'] as String,
                image: map['image'] as String,
                description: map['description'] as String,
                commonLocations: jsonEncode(map['common_locations']),
                category: map['category'] as String,
        );
    }

Aqui, temos um detalhe interessante a ser realizado, que é validar se receberemos uma localização ou não. Ao analisar a API, esse commonLocations pode vir vazio e não queremos que um erro ocorra ou que nossa aplicação quebre por falta de informação.

Por isso, adicionaremos duas interrogações (??) para um valor padrão a ser retornado. Isso faz parte do recurso que nos permite validar se algo pode vir nulo ou não. Retornaremos uma string contendo o texto "Sem localização". Assim, se não tivermos esse parâmetro dentro da API, receberemos a mensagem "Sem localização" e nossa aplicação não quebrará.

factory Entry.fromMap(Map<String, dynamic> map) {
        return Entry(
                id: map['id'] as int, 
                name: map['name'] as String,
                image: map['image'] as String,
                description: map['description'] as String,
                commonLocations: jsonEncode(map['common_locations'] ?? ['Sem localização']),
                category: map['category'] as String,
        );
    }

Com isso, basicamente terminamos toda a nossa entrada e podemos prosseguir para a nossa regra de negócios!

Sobre o curso Flutter: praticando e adaptando arquitetura limpa

O curso Flutter: praticando e adaptando arquitetura limpa possui 110 minutos de vídeos, em um total de 58 atividades. Gostou? Conheça nossos outros cursos de Flutter em Mobile, ou leia nossos artigos de Mobile.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Flutter acessando integralmente esse e outros cursos, comece hoje!

Plus

De
R$ 1.800
12X
R$109
à vista R$1.308
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

Matricule-se

Pro

De
R$ 2.400
12X
R$149
à vista R$1.788
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Luri powered by ChatGPT

    Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com Luri até 100 mensagens por semana.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas