Alura > Cursos de Mobile > Cursos de Flutter > Conteúdos de Flutter > Primeiras aulas do curso Flutter com Firebase: guardando arquivos na nuvem com Storage

Flutter com Firebase: guardando arquivos na nuvem com Storage

Conhecendo o Cloud Storage - Apresentação

Fala, galera!

Meu nome é Ricarth Lima e hoje eu serei seu instrutor em mais um curso do maravilhoso mundo do Flutter com Firebase!

Autodescrição: Sou um homem negro de pele clara e tenho cabelo crespo bem volumoso. Tenho sobrancelhas grossas, olhos castanhos e boca e nariz largos. Uso óculos retangulares de armação escura e lentes transparentes, e tenho uma barba espessa. Durante a maior parte do curso, estarei com uma camisa azul marinho da Alura. Estou sentado em uma cadeira com encosto estofado preto e, ao fundo, tenho uma parede iluminada pelas cores azul e rosa.

Agora que já nos conhecemos, boas-vindas ao curso Flutter com Firebase: guardando arquivos na nuvem com Storage.

O Projeto

Nesse curso, daremos continuidade ao nosso projeto de lista de compras colaborativas, o Listin. Para quem não o conhece, esse é um projeto que estamos construindo ao longo da formação Flutter com Firebase. Basicamente, ele é um aplicativo onde conseguimos fazer lista nas quais conseguimos adicionar produtos.

As pessoas que estiverem usando esse aplicativo colaborativamente conseguirão saber quais produtos já estão no carrinho e o preço de cada produto. É bem interessante para conseguirmos fazer uma feira junto com outras pessoas.

O que aprenderemos

Neste curso descobriremos como:

Pré-requisitos

É necessário ter uma boa base de Flutter com Dart para este curso, mas, principalmente, que tenham assistido ao Alura+ Como configurar o Firebase no Flutter, onde configuramos um projeto Flutter com o Firebase. Esse Alura+ é obrigatório porque iremos pressupor que vocês fizeram as configurações do Firebase, que servem para qualquer ferramenta do Firebase.

O ideal é que tenham feito a formação A partir do zero: crie projetos em Dart, a linguagem utilizada no Flutter e venham fazer a formação de Firebase.

Dados todos os recados, chegou a hora de irmos para o curso.

Vejo vocês no próximo vídeo!

Conhecendo o Cloud Storage - Entendendo nosso problema

Agora que conhecemos a proposta do curso e nosso projeto, vamos nos aprofundar em entender o que é o Listin e qual o problema precisamos resolver neste curso.

Conhecendo o Listin

Caso não tenham intimidade com o Listin, ele é uma lista de compras colaborativa. Na tela de login do aplicativo, podemos fazer o login ou o cadastro.

Tela de login do Listin no emulador. O fundo da tela é verde-claro e, no centro da tela, há um retângulo branco com o formulário de login. No centro superior do retângulo tem um desenho de uma jaca partida. Abaixo dela tem a mensagem "Bem-vindo ao Listin! Faça login para criar sua lista de compras". Após a mensagem tem o campo para o E-mail, seguido do campo para senha. Centralizado abaixo do campo da senha tem o link "Esqueci minha senha" escrito em marrom. Em seguida tem um botão marrom escrito "Entrar" em branco. Abaixo dele tem o link "Ainda não tem conta? Clique aqui para cadastrar" escrito em azul.

Como já tenho um cadastro, farei o login.

E-mail: ricarth.limao@gmail.com

Senha: 123321

Ao logarmos com o Firebase Authentication, acessamos a tela inicial, onde temos um trabalho feito com o Cloud Firestore, o banco de dados. Assim conseguimos realmente ter uma lista de compras colaborativa, onde conseguimos contribuir com outras pessoas para fazermos compras juntos.

Tela inicial do Listin no emulador. A app bar é marrom e tem os cantos inferiores arredondados. No lado esquerdo da app bar tem um menu hambúrguer e, no centro, há o texto "Listin - Feira colaborativa". O fundo do aplicativo é o mesmo verde-claro da tela inicial e, logo abaixo da app bar, tem o listin "Feira de Maio". No canto inferior direito da tela tem um botão flutuante vermelho com o sinal de "+" branco dentro dele.

Podemos criar uma lista de compras, clicando no botão com o sinal de "+", no canto inferior direito da tela, ou podemos entrar na lista já existente, clicando em "Feira de Maio". Ao acessarmos a "Feira de Maio", reparamos que já existe um produto na seção "Produtos Planejados", chamado "Pão doce".

Tela do aplicativo com a "Feira de Maio" aberta no emulador. Na app bar tem o botão de voltar à esquerda, o título "Feira de Maio" no centro e um menu kebab à direita. Abaixo da app bar, estão as informações da lista de compras. A lista tem três divisões. No topo está o espaço para o valor total previsto para compra em reais, que atualmente é "R$0.00". Em seguida tem a seção de "Produtos Planejados", onde tem o item "Pão doce (x12)", que custa R$1.0. À esquerda do item há um sinal de check e, à direita, um ícone de lixeira. A terceira e última seção é a de "Produtos Comprados", que está vazia. No canto inferior direito está o botão flutuante vermelho com sinal de "+".

Podemos adicionar outro produto, clicando no botão com sinal de "+" no canto inferior direito. Ao fazermos isso, um formulário aparece na parte inferior da tela, onde podemos preencher as informações de "Nome", "Quantidade" e "Preço". Vamos adicionar, por exemplo, "Pão Francês".

Nome: Pão Francês

Quantidade: 12

Preço: 1

Tela do aplicativo, no emulador, com o formulário de "Adicionar Produto" aberto sobre a "Feira de Maio". No campo "Nome do produto" está escrito "Pão Francês", no campo "Quantidade" está escrito "12" e no campo "Preço" está escrito "1". No canto inferior direito do formulário estão os botões "Cancelar" e "Salvar".

Na parte inferior direita do formulário estão os botões "Cancelar" e "Salvar". Ao clicarmos em "Salvar", voltamos para a tela da lista de compras, onde o "Pão Francês" foi adicionado aos "Produtos Planejados".

Durante as compras, conforme adicionarmos os produtos no carrinho, podemos clicar no botão de check (✓) no lado esquerdo do nome do produto. Dessa forma, ele será movido para o "Produtos Comprados", e isso acontece em tempo real. Todo mundo que estiver usando o aplicativo nessa conta conseguirá observar essas alterações enquanto estiver fazendo a feira.

Isso é bem legal, mas temos uma nova demanda. Ao clicarmos no menu hambúrguer no canto esquerdo da app bar para abrir o drawer, que é um menu lateral na esquerda. Nele temos algumas informações da pessoa usuária logada na barra superior do menu: o nome e o e-mail. Logo abaixo, temos as opções "Remover conta", que é autoexplicativa, e "Sair", para deslogar.

Além disso, no canto superior esquerdo do menu, acima do nome, tem um círculo branco onde deveria ter uma foto de perfil. Entretanto, não damos a opção para pessoa adicionar uma foto de perfil em nenhum lugar do aplicativo.

É justamente o que queremos fazer, mas não podemos simplesmente deixar uma imagem localmente salva no dispositivo. Precisamos que ela esteja na nuvem para, quando acessarem a conta em qualquer outro dispositivo, todas as pessoas logadas consigam ver a foto de perfil.

Onde adicionar a funcionalidade

Podemos pensar em vários locais que poderíamos colocar a opção de adicionar uma foto de perfil, inclusive no próprio menu lateral. Por exemplo, poderíamos colocar um botão dentro do círculo branco.

Porém, esse não é o ideal, porque não teríamos muito espaço para trabalhar. Ele não seria muito bom para configurarmos as opções de listagem e exclusão dessa foto para pessoa usuária. O ideal é criarmos uma nova tela para fazermos a adição e gerenciamento do arquivo de imagem de perfil.

É essa tela que criaremos a seguir.

Conhecendo o Cloud Storage - Criando tela de upload

Agora que entendemos o contexto do problema que precisamos resolver, começaremos a codar para criarmos a tela de configuração da foto de perfil.

Após acessarmos o VS Code, abriremos o Explorador de pastas, clicando no ícone do canto superior direito ou pressionando "Ctrl + Shift + E". Vamos clicar na pasta "lib" e selecionar "New Folder" (Nova Pasta) para criarmos uma nova pasta, que nomearemos como "storage" (armazenamento). Estamos separando em pastas as funcionalidades e, consequentemente, os cursos.

Dentro da pasta "storage", criaremos um novo arquivo. Para isso, clicaremos na pasta com o botão direito e selecionaremos "New File" (Novo Arquivo), que nomearemos como storage_screen.dart. Após escrevermos o nome do arquivo e pressionarmos "Enter", o arquivo é aberto.

Na primeira linha do storage_screen.dart, importaremos o Material, escrevendo import 'material' e selecionando o pacote Flutter do Material entre as opções sugeridas do VS Code. Outra opção é escrever diretamente import 'package:flutter/material.dart';.

Pressionaremos "Enter" duas vezes e codaremos o atalho stf para encontrarmos a sugestão do "Flutter Stateful Widget". Pressionando "Enter", aparece a estrutura do Stateful Widget, que chamaremos de StorageScreen.

import 'package:flutter/material.dart';

class StorageScreen extends StatefulWidget {
    const StorageScreen({super.key});

    @override
    State<StorageScreen> createState() => _StorageScreenState();
}

class _StorageScreenState extends State<StorageScreen> {
    @override
    Widget build(BuildContext context) {
        return const Placeholder()
    }
}

Podemos salvar o código e acessar o main.dart, onde adicionaremos a StorageScreen. Dessa forma conseguiremos visualizá-la sem precisarmos passar pela HomeScreen para chegar até ela. Quando terminarmos essa tela, faremos o caminho natural, mas é normal que, enquanto estamos criando uma tela, deixemos ela diretamente na Main para uma visualização mais rápida.

Então vamos abrir o main.dart e descer o código até a linha do home: const RoteadorTelas(),. Mudaremos ela para home: StorageScreen() //const RoteadorTelas(). Como o StorageScreen() não foi sugerido, temos uma marcação de erro nele.

Para corrigir, voltaremos para o começo do código e codaremos import 'storage/storage_screen.dart';, importando a tela. Ao salvarmos o código, o erro some. Às vezes o analisador do Dart dá algum problema, então precisamos fazer a importação manualmente.

//código omitido
import 'storage/storage_screen.dart';

//código omitido

home: const StorageScreen(), // const RoteadorTelas(),

Abrindo o emulador, aparece uma tela preta com um "X" enorme, mostrando que não tem nada. É nesse espaço que construiremos nossa tela. Começaremos construindo a lógica da tela e depois construiremos os widgets.

Para começar, dentro da classe de estado, precisaremos de dois atributos que serão muito úteis para nós. O primeiro é a String? urlPhoto;, ou seja, uma string que pode ser nula chamada urlPhoto.

Esse código representa que, quando tiver uma string nessa variável, temos uma imagem a ser mostrada. Se estiver nula, não temos uma imagem. Dessa forma, construímos nossa tela pensando nessa possibilidade.

O segundo atributo que queremos é uma lista de Strings chamada de listFiles (Lista de arquivos), lembrando que se não a iniciarmos, teremos um erro. Então codaremos List<String> listFiles; = []. Esse atributo representa a lista de imagens que já subimos para o aplicativo. Se ela estiver vazia, não mostraremos nada.

//código omitido

class _StorageScreenState extends State<StorageScreen> {
    String? urlPhoto;
    List<String> listFiles = [];

    @override
    Widget build(BuildContext context) {
        return const Placeholder()
    }
}

Feitos os atributos, vamos construir os métodos. Pensaremos nos dois métodos que serão bem úteis, mas construiremos só a estrutura deles agora. O primeiro é o método de upload, que chamaremos de uploadImage(), para subirmos uma imagem.

O outro é o método para recarregar, que chamaremos de refresh(), para atualizarmos a tela quando fizermos alguma alteração. Na verdade, vou deixar como reload() para seguir um padrão. Esses dois métodos são bem padrões e iremos usá-los.

//código omitido

class _StorageScreenState extends State<StorageScreen> {
    String? urlPhoto;
    List<String> listFiles = [];

    @override
    Widget build(BuildContext context) {
        return const Placeholder()
    }

    uploadImage(){

    }

    reload(){

    }
}

Salvamos o código e agora conseguimos criar nossa tela. No return que está dentro do build(), removeremos o Placeholder() e escreveremos o Scaffold().

Começaremos montando a tela pela app bar, codando appBar: AppBar(title: Text("Foto de Perfil"), actions: [],). Colocamos o título "Foto de perfil" e deixamos o espaço para adicionarmos botões de ação.

//código omitido

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text("Foto de Perfil"),
                actions: [],
        ),
    }

//código omitido

Ao salvarmos o código e voltarmos para o emulador, percebemos que agora aparece uma tela com o tema do aplicativo, ou seja, o fundo verde-claro e a app bar marrom com fonte branca. No centro da app bar está escrito "Foto de Perfil".

Voltando para o código, adicionaremos dois botões em actions. O primeiro é um IconButton() para chamar o uploadImage() quando pressionado, com ícone de upload.

//código omitido

actions: [
    IconButton(
        onPressed: () {
            uploadImage();
        },
        icon: Icon(Icons.upload),
    )
],

Ao salvarmos e abrirmos o emulador, percebemos que no canto direito da app bar apareceu um ícone de uma seta para cima com uma linha horizontal embaixo dela, que é o ícone de upload. Então ficou exatamente como queremos.

Retornando ao código, copiaremos o IconButton() que acabamos de criar, escreveremos uma vírgula após o parêntese de fechamento do botão e, na linha abaixo, colaremos o código. Em seguida, substituiremos o ícone de upload pelo de refresh, porque não tem o de reload. Além disso, quando pressionado, ele chamará o reload() ao invés do uploadImage().

Finalizadas as alterações, reparamos que os dois Icon() no IconButton e o Text() no title da AppBar() estão com marcação de atenção. Isso porque precisamos escrever const antes deles, já que os widgets não mudarão. Então teremos const Text("Foto de Perfil"), const Icon(Icons.upload) e const Icon(Icons.refresh).

//código omitido

appBar: AppBar(
    title: const Text("Foto de Perfil"),
    actions: [
        IconButton(
            onPressed: () {
                uploadImage();
            },
            icon: const Icon(Icons.upload),
        ),
        IconButton(
            onPressed: () {
                reload();
            },
            icon: const Icon(Icons.refresh),
        ),
    ],
)

Voltando para o emulador, encontramos o botão de reload, com ícone da seta circular, no canto esquerdo da app bar, ao lado do botão de upload. Percebemos que a tela está tomando forma e a aparência está quase como queremos.

Já finalizamos a app bar, agora criaremos o corpo da tela. Portanto, no código, logo após o parêntese de fechamento da app bar, escreveremos uma vírgula e, na linha abaixo, escrevermos body: Container(),, adicionando um container ao corpo do Scaffold().

Esse container terá algumas características visuais, começando por uma margem de 32. Para isso, escreveremos margin: const EdgeInsets.all(32),. Além disso, escreveremos um padding de 16, com padding: const EdgeInsets.all(16),.

//código omitido

),
body: Container(
    margin: const EdgeInsets.all(32),
    padding: const EdgeInsets.all(16),
    ),

Salvamos o código e ao voltarmos ao emulador, reparamos que nada apareceu ainda, porque o container está vazio. Sendo assim, voltaremos ao código e usaremos o decoration para mudar a cor e visualizarmos o conteúdo. Mudaremos a cor do container para branco e deixaremos a borda arredondada, escrevendo decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16),),.

body: Container(
    margin: const EdgeInsets.all(32),
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
    ),

Salvando o código e retornando para o emulador, percebemos que apareceu um container branco com bordas arredondadas centralizado na tela, tomando a forma que queremos. Como já adicionamos os comportamentos de upload e reload, basta adicionarmos os widgets dentro desse container da forma que esperamos para termos a nossa tela do jeito que queremos.

Para fazer isso, voltaremos ao código e, após o fechamento de parêntese do BoxDecoration(), escreveremos uma vírgula. Na linha abaixo, criaremos um filho para o container. Esse filho será uma coluna que também receberá filhos, e para isso codamos child: Column(children: []),.

Dentro dessa coluna, o primeiro filho será um teste para descobrirmos se a urlPhoto é ou não nula, porque declaramos que poderia ou não ser nula. Para isso, usaremos um operador ternário, escrevendo (urlPhoto != null)? :.

Se for diferente de nulo, adicionaremos um widget, logo após o ponto de interrogação, mas veremos isso depois. Por enquanto, podemos adicionar um Container() para testarmos. Se for nulo, ou seja, após os : adicionaremos um CircleAvatar() contendo um filho que será um ícone de uma pessoa.

//código omitido

child: Column(children: [
    (urlPhoto != null)
            ? Container()
            : CircleAvatar(
                    child: Icon(Icons.person),
                ),
]),

Ao salvarmos e acessarmos o emulador, percebemos que o container branco está com uma largura bem menor e está totalmente do lado esquerdo, mas na parte superior dele tem um círculo marrom com um ícone branco de pessoa dentro.

Não precisamos nos preocupar com o encolhimento do container, porque ele voltará para o tamanho correto com à medida que adicionarmos os widgets. Entretanto, o círculo do avatar está bastante pequeno, então voltaremos para o código e aumentaremos o radius dele para 64. Para isso, escreveremos radius: 64 acima do child: Icon().

//código omitido

child: Column(children: [
    (urlPhoto != null)
            ? Container()
            : CircleAvatar(
                    radius: 64,
                    child: Icon(Icons.person),
                ),
]),

Ao salvarmos e voltarmos para a tela do emulador, percebemos que o círculo marrom aumentou para um tamanho ideal, e com isso a largura do container também aumentou um pouco. Apesar disso o ícone dentro dele continua pequeno.

Na sequência, voltaremos para o código e faremos o teste para caso haja uma imagem. Caso haja, mudaremos o código para Image.network(urlPhoto!), de modo que usaremos a url da própria urlPhoto e, como já testamos que ela não é nula, colocamos um bang, que é uma exclamação, (!) depois dela. Além disso, escreveremos um const antes do CircleAvatar() para tirar o aviso.

//código omitido

child: Column(children: [
    (urlPhoto != null)
            ? Image.network(urlPhoto!)
            : const CircleAvatar(
                    radius: 64,
                    child: Icon(Icons.person),
                ),
]),

Faremos as configurações futuras do Image.network() quando estivermos exibindo a imagem. Depois desse teste que fizemos para mostrar a imagem, escreveremos um Divider() abaixo do ternário.

//código omitido

child: Column(children: [
    (urlPhoto != null)
            ? Image.network(urlPhoto!)
            : const CircleAvatar(
                    radius: 64,
                    child: Icon(Icons.person),
                ),
    Divider()
]),

Voltando para o emulador, reparamos que o Divider() adiciona uma linha cinza bem fina após o círculo do avatar. Com essa linha, o Container voltou à largura máxima. Queremos que essa linha esteja um pouco mais distante da imagem e do conteúdo que virá após ela.

Para delimitarmos essa distância, voltaremos ao código para adicionar um padding. Para isso, clicaremos no Divider() e depois no ícone de lâmpada, ou lupa, amarelo no lado esquerdo, selecionando "Wrap with Padding".

Aumentaremos o valor do padding para 16, ou seja, EdgeInsets.all(16.0). Também escreveremos um const antes do Padding() e apagaremos o const antes do EdgeInsets.all(), para o alerta que está marcando o padding e o child sumir.

//código omitido

child: Column(children: [
    (urlPhoto != null)
            ? Image.network(urlPhoto!)
            : const CircleAvatar(
                    radius: 64,
                    child: Icon(Icons.person),
                ),
    const Padding(
        padding: EdgeInsets.all(16.0),
        child: Divider(),
    ),

Quando voltamos para a nossa tela no emulador, percebemos que o espaçamento aumentou, ainda que a linha seja bem fina e de difícil visualização. Para resolver isso, adicionaremos uma cor para essa linha, escrevendo color: Colors.black dentro dos parênteses do Divider(). Ao salvarmos o código e voltarmos para o emulador, temos uma visualização um pouco melhor da linha.

//código omitido

const Padding(
    padding: EdgeInsets.all(16.0),
    child: Divider(color: Colors.black),
),

Após o Padding(), adicionaremos um texto, que será a representação do que queremos adicionar depois, que é o histórico de imagens. Codaremos const Text("Histórico de Imagens", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),),. Dessa forma definimos o texto "Histórico de Imagens" e o estilo, que é uma fonte em negrito de tamanho 18.

//código omitido

const Padding(
    padding: EdgeInsets.all(16.0),
    child: Divider(color: Colors.black),
),
const Text(
    "Histórico de Imagens",
    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),

Abaixo do Text(), criaremos outra coluna em que os filhos serão autogerados por uma lista, usando o List.generate(length, (index) => null). O tamanho da lista (length) será o listFiles.length e substituiremos a função de seta (=> null) por uma função normal. Temos então List.generate(listFiles.length, (index){}.

Dentro da função, passaremos as strings de URL que adicionamos a nossa lista, escrevendo String url = listFiles[index];, e o retorno será uma Image.network() que receberá a (url).

//código omitido

const Text(
    "Histórico de Imagens",
    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Column(
    children: List.generate(
        listFiles.length,
        (index) {
            String url = listFiles[index];
            return Image.network(url);
        },
    ),
)

Não temos nenhuma imagem para mostrar ainda, então quando voltamos para a tela do emulador, abaixo da divisão aparece apenas o título "Histórico de Imagens". Com isso, percebemos que a lógica foi estabelecida e está funcionando.

Tela "Foto de Perfil" exibida no emulador. Na app bar está o título "Foto de Perfil", centralizado, e os botões com ícone de upload e reload, no canto esquerdo. No centro da tela tem um retângulo branco com pontas arredondadas sobre o fundo verde-claro. No centro-superior do retângulo branco está um círculo marrom com um pequeno ícone de pessoa dentro dele. Abaixo do círculo há uma linha bastante fina preta que se estende de uma lateral a outra do retângulo, deixando apenas um pequeno espaçamento entre as bordas esquerda e direita. Abaixo da linha está o título "Histórico de Imagens" seguido de um espaço vazio.

Conseguimos criar a base tela que usaremos para trabalharmos nesse curso. Já temos os botões de upload e de atualização da tela, o espaço para a imagem de perfil e outro espaço para lista com o histórico de imagens de perfil apareça também.

A seguir faremos algumas configurações necessárias para usarmos o Firebase Storage no nosso projeto.

Sobre o curso Flutter com Firebase: guardando arquivos na nuvem com Storage

O curso Flutter com Firebase: guardando arquivos na nuvem com Storage possui 180 minutos de vídeos, em um total de 63 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!

Conheça os Planos para Empresas