Prototipagem de Aplicação Móvel Multiplataforma com Flutter e AWS Amplify



I’m going to show you how you can use Flutter and AWS Amplify to quickly go from nothing to a working cross-platform mobile application with authentication and backend infrastructure. What would usually take a small dev team a week or so to setup can be achieved in a fraction of the time using this toolkit.

Se você seguir este tutorial, deveria levar não mais do que uma hora. Bem, levei várias horas lutando com vários problemas, mas espero ter documentado-os bem o suficiente para que você não os encontre.

Aqui está o produto final. Se você quiser a versão “aqui está uma que eu fiz antes”, siga os passos no readme, você deve ter funcionando em cerca de quinze minutos. Aqui está o link do GitHub

Este tutorial é composto por cinco partes:

  1. Pré-requisitos e Configuração do Código
  2. Adicionando Autenticação
  3. Carregando uma Foto de Perfil
  4. Armazenando Detalhes do Usuário
  5. Adicionando Um Toque de Design

Recomendação

Flutter é uma plataforma muito madura que vem sendo usada há vários anos, com uma comunidade próspera e muitos plugins e extensões para realizar a maioria das coisas.

Amplify também é uma plataforma forte; no entanto, achei a funcionalidade da API difícil de trabalhar e as bibliotecas do Flutter não estavam atualizadas com as últimas notícias e recursos do Amplify. Em particular, trabalhar com AppSync GraphQL e DataStore (para armazenamento de dados offline e sincronização) foram bastante frágeis (como você verá mais tarde).

Juntas, estas duas são uma ótima combinação para acelerar o desenvolvimento de protótipos de aplicativos móveis, mas quando sentir que está forçando o Amplify à sua maneira, não tenha medo de abandoná-lo em favor de trabalhar diretamente com os serviços AWS que ele abstrai.

A demo app que construí armazena informações de perfil do usuário—um requisito comum de muitos apps. Você pode criar uma conta e fazer login, fazer upload de uma foto de perfil e enviar alguns detalhes sobre si mesmo. Vamos entrar em detalhes sobre o full-stack—trabalhando com Flutter e Dart para o código do app até o DynamoDB para oferecer uma visão abrangente do que você precisa saber.

Parte Um: Pré-requisitos e Configuração do Repositório de Código

Este tutorial pressupõe que você já tenha o seguinte configurado em sua máquina:

Code Editor/ IDE I use VSCode as it has a good set of Flutter and Dart plugins that speed up development, such as auto loading of dependencies, Dart linting, and intellisense. You’re free to use whatever IDE works for you though
AWS Account Create an AWS account if you don’t already have one. Visit AWS’ official page for steps to create an AWS account.

All of what we’ll use today is part of the free tier, so it shouldn’t cost you anything to follow this tutorial.

AWS CLI and AWS Amplify CLI Install AWS and Amplify CLI tooling.

Make sure you have an up-to-date version of Node and NPM (this is what the CLIs use). Visit Node.Js’ official website to download the up-to-date version.

If you need to run multiple versions of Node, I recommend using NVM to manage and switch between them.

To install AWS CLI, visit AWS’ official page.

XCode (for iOS) If you don’t have access to a Mac, you can deploy EC2 instances running MacOS in AWS these days, which you can use when you need to build iOS artifacts.

Download Xcode through the Mac App Store.

Follow the rest of the steps here to set it up ready for iOS Flutter development.

Android Studio (for Android) Follow the steps here to be ready for Android Flutter development.
Flutter SDK Follow these steps to get Flutter and its dependencies up and running (if you’re on a Mac that is, other guides are available for other OSes).

Flutter e Amplify têm ferramentas de scaffolding que criam a estrutura inicial do seu projeto. É importante fazer isso em uma certa ordem; caso contrário, a estrutura de pastas não estará alinhada com o que as ferramentas esperam, o que causará um incômodo para corrigir mais tarde.

Certifique-se de criar a estrutura do repositório de código usando Flutter primeiro, e então inicialize o Amplify dentro dele.

I used the official Flutter getting started documentation to kick things off for my demo.

Vamos ver se conseguimos fazer o Flutter funcionar. Primeiro, para verificar se está corretamente instalado e adicionado ao seu PATH, você pode executar flutter doctor.

Se esta for sua primeira incursão no desenvolvimento móvel, haverá alguns itens que precisam ser tratados aqui. Para mim, foi:

  • Instalar o Android Studio (e o Android SDK CLI).
  • Instalar o XCode e o CocoaPods.
  • Aceitar os termos e condições para as ferramentas Android e iOS.

Criando o Repositório de Código do seu App

Quando tiver todos os pré-requisitos prontos, você pode criar o scaffolding do Flutter. Fazer isso cria a pasta em que trabalharemos, então execute este comando a partir de um diretório pai:

 
flutter create flutterapp --platforms=android,ios

I’ve specified Android and iOS as target platforms to remove the unnecessary config for other platforms (e.g. web, Windows, Linux).


Você pode querer renomear o diretório de nível superior criado neste ponto caso não queira que corresponda ao nome do seu aplicativo. Alterei de “flutterapp” para “flutter-amplify-tutorial” (o nome do meu repositório do git).

Neste ponto, o Flutter criou setenta e três arquivos para nós. Vamos dar uma olhada no que são:


As pastas com as quais passaremos a maior parte do tempo são ios/android e lib/. Dentro das pastas ios e android estão os recursos do projeto que podem ser abertos com o XCode e o Android Studio, respectivamente. Esses projetos atuam como a interoperação entre o código Dart independente da plataforma e suas plataformas de destino, e você pode usá-los para testar seu aplicativo contra as respectivas plataformas. Vamos tentar isso com o iOS agora:

Configuração do iOS

 

open -a Simulator
flutter run

No meu Mac, com uma configuração mínima do XCode, isso passou de nada até executar um simulador de iPhone 14 Pro Max com o aplicativo Flutter estruturado em execução, o que é bem legal. 


Se vir o seguinte: parabéns, você conseguiu gerar com sucesso a estrutura.


Você também pode abrir o projeto ios/Runner.xcodeproj dentro do XCode, explorar seus conteúdos e executar contra simuladores e dispositivos físicos como faria com qualquer outro projeto do XCode.

Configuração do Android

O Android é um pouco menos direto, pois você tem que configurar explicitamente um emulador no Android Studio antes de poder executá-lo. Abra o projeto android/flutterapp_android.iml dentro do Android Studio para começar, e então você pode configurar e executar um emulador para testar o aplicativo.


Dê a Android Studio alguns minutos para baixar o Gradle e todas as dependências necessárias para executar o aplicativo—você pode acompanhar o progresso disso na barra de progresso no canto inferior direito.

Quando o Android Studio se estabilizar, se você já tiver um dispositivo simulado configurado no AVD, deverá ser capaz de pressionar o botão de play no canto superior direito da janela:


E eis que, o mesmo aplicativo no Android:


Isso está demonstrando o código de exemplo de aplicativo fornecido quando você cria um novo projeto Flutter. Ao longo deste tutorial, iremos gradualmente substituir este código pelo nosso próprio.

Este é um bom momento para fazer um commit no git, agora que temos as bases configuradas para o desenvolvimento Flutter. Estamos agora em um ponto em que podemos começar a mexer com o código Flutter e ver os resultados no iOS e Android simultaneamente.

O Flutter usa Dart como linguagem intermediária entre Android e iOS, e todo o código com o qual você estará interagindo reside na pasta lib/. Deve haver um arquivo main.dart, que é onde começaremos a mexer.

Configurar e implantar um novo aplicativo usando Amplify

Agora que temos as ferramentas móveis prontas para trabalhar, precisamos de uma infraestrutura de backend para suportar a funcionalidade do aplicativo.

Vamos usar os vários serviços da AWS para suportar nosso aplicativo, mas tudo será gerenciado usando o serviço AWS Amplify. A maior parte será tratada de forma transparente para nós, e em vez de nos preocuparmos com que serviços utilizar, focaremos nas funcionalidades que queremos implantar.

Para começar, dentro da sua pasta de código execute o seguinte:

 
amplify init

Este comando inicializa o AWS Amplify dentro do seu projeto. Se você nunca o usou antes, ele fará algumas perguntas. Para as pessoas subsequentes que colaboram no projeto, ao executar este comando, configura o ambiente local com a configuração do Amplify já em vigor.



Isso irá provisionar alguns recursos iniciais da AWS para armazenar a configuração e o estado do seu aplicativo Amplify, especificamente um bucket S3.

A barra de progresso e status de implantação acima podem parecer familiares a alguns – é o CloudFormation, e assim como o AWS CDK, o Amplify usa o CFN nos bastidores para provisionar todos os recursos necessários. Você pode abrir o console de pilhas do CloudFormation para ver isso em ação:


Finalmente, quando o CLI estiver completo, você deve ver uma confirmação semelhante ao abaixo, e poderá ver seu novo aplicativo implantado no Console do Amplify:



Gerenciamento de Ambientes

O AWS Amplify possui a noção de “ambientes”, que são implantações isoladas do seu aplicativo e recursos. Historicamente, a noção de ambientes teve que ser criada dentro de qualquer ecossistema que você tinha: (por exemplo, CloudFormation, CDK), usando convenções de nomenclatura e parâmetros. No Amplify, é um cidadão de primeira classe – você pode ter múltiplos ambientes que permitem padrões, como provisionamento de ambientes compartilhados, que as alterações são promovidas através (por exemplo, Dev > QA > PreProd > Prod) bem como fornecendo ambientes por desenvolvedor ou ramificação de recurso.

O Amplify também pode configurar e provisionar serviços CI/CD para você usando o Amplify hosting e integrá-los em seus aplicativos para fornecer um ecossistema de desenvolvimento end-to-end. Isso configura o CodeCommit, CodeBuild e CodeDeploy para gerenciar o controle de versão, construção e implantação de aplicativos. Isso não é abordado neste tutorial, mas poderia ser usado para automatizar a construção, teste e publicação de lançamentos nas lojas de aplicativos.

Parte Dois: Adicionando Autenticação

Geralmente, você precisaria aprender sobre o serviço de autenticação do AWS Cognito e serviços de apoio, como IAM, conectá-los usando algo como CloudFormation, Terraform ou CDK. No Amplify, é tão simples quanto fazer:

 
amplify add auth

Amplify add permite adicionar várias “funcionalidades” ao seu projeto. Por trás dos panos, o Amplify vai implantar e configurar todos os serviços necessários que você precisa usando CloudFormation, para que você possa se concentrar mais nas funcionalidades do seu aplicativo e menos na parte técnica.

Quando digo que é tão fácil quanto digitar essas três palavras mágicas acima… não é tão simples assim. O Amplify fará várias perguntas para entender como você deseja que as pessoas se autentiquem e quais controles você deseja estabelecer. Se você escolher “Configuração Padrão”, o Amplify configurará a autenticação com valores padrão sensíveis para você começar rapidamente. Vou escolher “Configuração Manual” para demonstrar o quão configurável o Amplify é.


A configuração acima permite criar contas apenas com seu número de telefone (sem endereço de email necessário) e verifica que você é o verdadeiro proprietário desse número usando MFA para verificação e tentativas adicionais de login. Recomendo fortemente o uso do OAuth como mecanismo de autenticação padronizado, mas não o usei aqui por simplicidade.

Agora, ao adicionar recursos, eles não são provisionados imediatamente. É por isso que os comandos são misteriosamente rápidos para serem concluídos. Todos esses comandos preparam a configuração do seu App Amplify (e ambiente local) para implantar esses recursos.

Para implantar recursos (ou quaisquer alterações de configuração) você precisa fazer um push:

 
amplify push

Nota: isso é diferente do comando amplify publish, que constrói e implanta serviços de backend e frontend. Push provisiona apenas recursos de backend (e isso é tudo que precisaremos neste tutorial, pois construiremos aplicativos móveis).

Ao adicionar autenticação (ou qualquer recurso Amplify), o Amplify adiciona um arquivo Dart chamado lib/amplifyconfiguration.dart. Este git é ignorado porque contém credenciais sensíveis relacionadas aos recursos implantados e é sincronizado automaticamente com o ambiente Amplify em que você está trabalhando. Você pode obter mais informações sobre isso aqui.

Neste ponto, configuramos o Amplify com um aplicativo e um ambiente de desenvolvimento criado e o Cognito configurado para autenticação. É um bom momento para fazer um commit no git se você estiver seguindo, para que possa voltar a este ponto se necessário. O Amplify deve ter criado um arquivo .gitignore para você, excluindo todos os arquivos desnecessários.

Agora que temos a infraestrutura de autenticação do backend em vigor, podemos começar a construir nosso aplicativo móvel com Flutter.

Autenticando Usuários em Nosso Aplicativo

I’m following the steps outlined in the authentication for the AWS Amplify tutorial here.

Isso está usando as telas e fluxo de autenticação padrão incluídos dentro do amplify_flutter. Adicione as dependências do Amplify Flutter adicionando o seguinte sob “dependencies” no arquivo pubspec.yaml:

 

amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0

Se você não estiver usando as extensões Flutter e Dart dentro do VSCode (ou usando o VSCode), precisará seguir com um comando flutter pub get. Se estiver, o VSCode executará automaticamente isso quando salvar o arquivo pubspec.yaml.

Há uma abordagem de inicialização rápida para integrar a autenticação que usa uma biblioteca de UI de autenticador pré-fabricada, ótima para inicializar rapidamente um fluxo de login que pode ser personalizado mais tarde. Vamos usar isso neste tutorial para demonstrar o amplo conjunto de bibliotecas do Amplify disponíveis e como você pode integrá-las rapidamente em seu aplicativo.

Os passos para integrar a biblioteca de Autenticação OOTB estão aqui.

Podemos transportar o widget de autenticador decorativo configurado no código de exemplo para o código fornecido no exemplo de arranque rápido do Flutter assim:

 

class _MyAppState extends State {
  @override
  Widget build(BuildContext context) {
    return Authenticator(
        child: MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          // Este é o tema do seu aplicativo.
          //
          // Tente executar seu aplicativo com "flutter run". Você verá que
          // o aplicativo tem uma barra de ferramentas azul. Então, sem sair do app, tente
          // alterar o primarySwatch abaixo para Colors.green e então invoque
          // "recarregamento quente" (pressione "r" no console onde você executou "flutter run",
          // ou simplesmente salve suas alterações para "recarregamento quente" em um IDE Flutter).
          // Observe que o contador não voltou a zero; o aplicativo
          // não é reiniciado.
          primarySwatch: Colors.blue,
          useMaterial3: true),
      home: const MyHomePage(title: 'Flutter Amplify Quickstart'),
      builder: Authenticator.builder(),
    ));
  }

  @override
  void initState() {
    super.initState();
    _configureAmplify();
  }

  void _configureAmplify() async {
    try {
      await Amplify.addPlugin(AmplifyAuthCognito());
      await Amplify.configure(amplifyconfig);
    } on Exception catch (e) {
      print('Error configuring Amplify: $e');
    }
  }
}

O que é um Widget? 

É o bloco de construção básico no Flutter usado para compor layouts e componentes de UI. Quase tudo no Flutter são widgets—colunas, andaimes, preenchimento e estilo, componentes complexos, etc. O exemplo nas documentos de introdução ao Flutter usa um Widget “Center” seguido de um Widget “Text” para exibir um pedaço de texto centralizado que diz “Hello World”.

O código acima decora o widget MyHomePage com um widget de autenticador, adiciona o plugin AmplifyAuthCognito, e usa a configuração que o comando anterior amplify add auth gerou em lib/amplifyconfiguration.dart para conectar automaticamente ao seu AWS Cognito User Pool.

Após executar o Flutter, ao demonstrar a integração de autenticação, levei algum tempo para que a etapa “Running pod install” fosse concluída. Tenha paciência (cerca de 5 minutos).


Uma vez que as alterações de autenticação foram feitas e o aplicativo inicia, você é recebido com uma tela de login básica, mas funcional.



Utilizando o fluxo “Criar Conta”, você pode fornecer seu número de telefone e uma senha, e então é apresentado com um desafio de MFA para concluir o registro. Você pode ver que o usuário é criado dentro do Cognito User Pool:


Você pode testar isso facilmente em um dispositivo Android virtual também. Você nem precisa sair do VSCode se tiver instalado os plugins do Flutter e Dart, então não é necessário abrir o Android Studio. Basta selecionar o nome do dispositivo ativo atual (iPhone) no canto inferior direito do VSCode, trocar para um dispositivo Android virtual que você já criou, e depois pressionar “F5” para começar a depuração. A experiência é bastante similar ao iOS:



Ao implantar pela primeira vez após implementar a biblioteca de autenticação, encontrei a seguinte exceção ao tentar construir o aplicativo:

 
uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared in library [:amplify_auth_cognito_android]

O Flutter é realmente útil nesta situação, pois logo após o despejo desta pilha de rastreamento, ele fornece uma recomendação:


O SDK do Flutter parece já estar sobrescrevendo isso em nosso arquivo build.gradle:

 

apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    ...
    defaultConfig {
        // TODO: Especifique seu próprio ID de Aplicação único (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutterapp"
        // Você pode atualizar os seguintes valores para corresponder às necessidades de sua aplicação.
        // Para mais informações, consulte: https://docs.flutter.dev/deployment/android#revisando-a-configuração-de-build.
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

Enquanto o Flutter, como mínimo, requer o uso da API 16 (declarado no flutter.gradle), a biblioteca Amplify Auth precisa de pelo menos 21. Para corrigir isso, basta alterar o minSdkVersion de “flutter.minSdkVersion” para “21”.

Uma vez autenticado, você é apresentado à aplicação de exemplo “botão clicador” mostrada anteriormente. Agora, é hora de começar a personalizar de acordo com nossas necessidades.

Parte Três: Upload de Foto de Perfil

Neste exemplo, vamos usar essa capacidade para permitir que os usuários façam upload de uma foto de si mesmos para serem usadas como avatar dentro da aplicação.

Quer adicionar recursos de armazenamento à sua aplicação? Sem problemas, basta fazer:

 
amplify add storage

e o Amplify provisionará os serviços de backend necessários para que sua aplicação use armazenamento baseado em nuvem. O Amplify facilita a integração do Flutter com o S3, permitindo que os usuários de sua aplicação armazenem objetos. A flexibilidade do S3 permite que você armazene todos os tipos de ativos, e em conjunto com o Cognito e o Amplify, você pode facilmente provisionar áreas privadas para usuários armazenarem fotos, vídeos, arquivos, etc.

Os arquivos podem ser salvos com acesso público, protegido ou privado:

Public Read/Write/Delete by all users
Protected Creating Identify can Write and Delete, everyone else can Read
Private Read/Write/Delete only by Creating Identity

Para a nossa foto de perfil, vamos criá-la com acesso protegido para que apenas o usuário possa atualizar e excluir seu avatar, mas outras pessoas na aplicação poderiam visualizá-lo.

Aqui é onde começaremos a estilizar e construir a estrutura do nosso aplicativo. O Flutter está intimamente integrado com o sistema de design material, amplamente utilizado no desenvolvimento de aplicativos móveis para fornecer uma aparência e sensação consistentes. Ele fornece um conjunto de componentes compatíveis em várias plataformas, cujos estilos podem ser substituídos para criar uma experiência específica para sua marca.

O template de introdução ao Flutter já monta alguns widgets usando o widget MaterialApp. Previamente decoramos isso com um widget de autenticador. Agora, vamos expandir o widget filho MyHomePage do MaterialApp para fornecer uma foto de perfil.

Você compõe widgets juntos em uma árvore, conhecida como “Hierarquia de Widgets”. Você sempre começa com um widget de nível superior. No nosso aplicativo, é o widget de envoltório de autenticador que lida com o login inicial. Scaffold é um bom widget para basear seus layouts: é comumente usado como o widget de nível superior com aplicativos materiais; e tem vários espaços reservados, como um botão de ação flutuante, folha inferior (para deslizar para cima com detalhes adicionais), uma barra de aplicativos, etc.

Primeiro, vamos apenas adicionar um widget de imagem que aponta para um URL de rede. Mais tarde, substituiremos isso por um que capturamos e carregamos no S3. Usei os seguintes recursos para adicionar uma imagem com um espaço reservado redondo:

  • API Flutter
  • Google Flutter

No array de filhos do widget de coluna aninhada, adicione o seguinte widget de container:

 

children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            Container(
              width: 200,
              height: 200,
              decoration: const BoxDecoration(
                shape: BoxShape.circle,
                image: DecorationImage(
                    image: NetworkImage(
                        'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
                    fit: BoxFit.fill),
              ),
            )
          ],

Agora podemos exibir uma imagem da web:


Em seguida, permitiremos que o usuário escolha um avatar a partir de uma imagem em seu dispositivo. Uma breve pesquisa no Google revelou esta biblioteca que abstrai os detalhes de seleção de imagens:

  • Google: “Pub Dev Packages Image Picker”

São necessárias apenas duas linhas de código para solicitar ao usuário que selecione uma imagem:

 

final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);

No iOS, é preciso adicionar a chave NSPhotoLibraryUsageDescription ao arquivo de configuração <raiz do projeto>/ios/Runner/Info.plist do Xcode para solicitar acesso a visualização das fotos do usuário; caso contrário, o aplicativo falhará.

Vamos integrar isso a um widget GestureDetector, que ao receber um toque, solicitará ao usuário que escolha uma imagem para seu avatar:

 

ImageProvider? _image;

...

children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            GestureDetector(
                onTap: _selectNewProfilePicture,
                child: Container(
                    width: 200,
                    height: 200,
                    decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    image: DecorationImage(
                        image: _image ?? _placeholderProfilePicture(), fit: BoxFit.fill),
                    ),
                ),
            )
            ...
]

void _selectNewProfilePicture() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {

      var imageBytes = await image.readAsBytes();

      setState(() {
        _image = MemoryImage(imageBytes);
      });
    }
  }

  _placeholderProfilePicture() {
    return const AssetImage("assets/profile-placeholder.png");
  }

Chame setState(), atualizando os campos do widget dentro do Lambda passado para o Flutter, para que ele saiba chamar a função build(), onde o estado atualizado pode ser usado para redesenhar o widget. No nosso caso, a imagem do perfil será preenchida, então criaremos um widget de container que exibe a imagem. O operador ?? nulo-consciente fornece um avatar padrão de placeholder para quando o usuário ainda não selecionou uma imagem.

Você também precisará adicionar uma imagem de placeholder de perfil em seu repositório e referenciá-la no arquivo pubspec.yml para que seja incluída na compilação. Você pode usar a imagem do meu repositório, enquanto adiciona isso ao seu arquivo pubspec.yml:

 

# A seção a seguir é específica para pacotes Flutter.
flutter:
...

  # Para adicionar ativos ao seu aplicativo, adicione uma seção de ativos, como esta:
  assets:
    - assets/profile-placeholder.png


Neste ponto, somos capazes de selecionar um avatar da galeria de fotos do dispositivo e exibi-lo como uma imagem redonda no aplicativo. No entanto, essa imagem não é salva em nenhum lugar—uma vez que o aplicativo é fechado, ela se perde (e nenhum outro usuário seria capaz de ver sua foto também).

O que faremos a seguir é conectar isso a um armazenamento em nuvem—o AWS S3. Quando o usuário seleciona uma foto da galeria de fotos de seu dispositivo, nós a enviaremos para sua área privada no S3, e então o widget de imagem irá puxar a imagem de lá (em vez de diretamente do dispositivo) em si:

 

void _selectNewProfilePicture() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {

      var imageBytes = await image.readAsBytes();

      final UploadFileResult result = await Amplify.Storage.uploadFile(
          local: File.fromUri(Uri.file(image.path)),
          key: profilePictureKey,
          onProgress: (progress) {
            safePrint('Fraction completed: ${progress.getFractionCompleted()}');
          },
          options:
              UploadFileOptions(accessLevel: StorageAccessLevel.protected));

      setState(() {
        _image = MemoryImage(imageBytes);
      });
    }
  }

Agora, quando nosso usuário seleciona uma foto de seu dispositivo, nosso aplicativo a enviará para o S3 e a exibirá na tela.

Em seguida, faremos com que o aplicativo baixe o avatar do usuário do S3 quando ele inicia:

 

@override
  void initState() {
    super.initState();
    _retrieveProfilePicture();
  }

  void _retrieveProfilePicture() async {
    final userFiles = await Amplify.Storage.list(
        options: ListOptions(accessLevel: StorageAccessLevel.protected));
    if (userFiles.items.any((element) => element.key == profilePictureKey)) {
      final documentsDir = await getApplicationDocumentsDirectory();
      final filepath = "${documentsDir.path}/ProfilePicture.jpg";
      final file = File(filepath);
      await Amplify.Storage.downloadFile(
          key: profilePictureKey,
          local: file,
          options:
              DownloadFileOptions(accessLevel: StorageAccessLevel.protected));

      setState(() {
        _image = FileImage(file);
      });
    } else {
      setState(() {
        _image = const AssetImage("assets/profile-placeholder.png");
      });
    }
  }

A seguir, refatoraremos a lógica do avatar em seu próprio componente reutilizável. Você pode ver o componente concluído no meu repositório do GitHub que abriga toda a lógica acima. Em seguida, você pode limpar o componente _MyHomePageStage e inserir seu novo widget na hierarquia da seguinte forma:

 

children: <Widget>[
                        Padding(
                          padding: const EdgeInsets.symmetric(vertical: 20),
                          child: Text(
                          'User Profile',
                          style: Theme.of(context).textTheme.titleLarge,
                        )),
                        const ProfilePicture(),
                        TextField(...

Para concluir sobre o avatar, adicionaremos um spinner de carregamento para fornecer feedback aos usuários de que algo está acontecendo. Usaremos um campo booleano _isLoading para acompanhar quando a foto está sendo carregada, o que alternará entre mostrar o spinner ou a foto:

 

class _ProfilePictureState extends State<ProfilePicture> {
  ImageProvider? _image;
  bool _isLoading = true;

...

void _retrieveProfilePicture() async {
    ...
      setState(() {
        _image = FileImage(file);
        _isLoading = false;
      });
    } else {
      setState(() {
        _image = const AssetImage("assets/profile-placeholder.png");
        _isLoading = false;
      });
    }
  }

void _selectNewProfilePicture() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {
      setState(() {
        _isLoading = true;
      });

      ....

      setState(() {
        _image = MemoryImage(imageBytes);
        _isLoading = false;
      });
    }
  }

Parte Quatro: Armazenando Detalhes do Usuário (Abridgado)

Ótimo, agora temos um esqueleto de aplicativo móvel em vigor que possui usuários, autenticação e fotos de perfil. Em seguida, vamos ver se conseguimos criar uma API que utilize as credenciais do usuário para recuperar informações adicionais sobre eles.

Normalmente, eu diria: “você quer uma API? Simples:”

 
amplify add api

É aqui que a maior parte do esforço e solução de problemas foi, porque, dependendo da configuração que você escolher, não é totalmente suportada dentro do ecossistema Amplify e Flutter. O uso do data store e modelo padrão também pode resultar em padrões de leitura ineficientes, o que pode rapidamente se tornar caro e lento.

O Amplify fornece uma API de alto nível para interagir com dados no AppSync, mas neste tutorial, estarei usando GraphQL com consultas de baixo nível, pois isso oferece mais flexibilidade e permite usar um Global Secondary Index no DynamoDB para evitar varreduras na tabela. Se você quiser entender como cheguei aqui e quais são as várias armadilhas, confira “Desafios de Trabalho com e Ajuste do AWS Amplify e Appsync com Flutter”.


O Amplify tenta definir por padrão as perguntas feitas ao criar uma API, mas você pode substituir qualquer uma dessas rolando para cima até a opção que deseja alterar. Neste cenário, queremos um endpoint GraphQL (para aproveitar o DataStore) e a autorização da API a ser tratada pelo Pool de Usuários do Cognito, pois queremos aplicar controle de acesso refinado para que apenas o usuário possa atualizar ou excluir seus próprios detalhes (mas outros usuários podem visualizá-los).

Ao criar a API, o Amplify cria um esquema básico de tipo GraphQL de Tarefas. Atualizaremos isso e adicionaremos algumas regras de autorização antes de enviar as alterações da API.

Modifique o esquema GraphQL do modelo “ToDo” para atender às nossas informações de perfil do usuário informações.

 

type UserProfile @model 
@auth(rules: [
  { allow: private, operations: [read], provider: iam },
  { allow: owner, operations: [create, read, update, delete] }
])
{
  userId: String! @index
  name: String!
  location: String
  language: String
}

A regra privada permite que usuários logados visualizem os perfis de qualquer outra pessoa. Ao não usar o público, estamos impedindo que pessoas que não estão logadas visualizem perfis. O provedor IAM impede que os usuários acessem diretamente a API GraphQL; eles precisam estar usando o aplicativo e usar o papel “não autenticado” dentro do nosso pool de identidades Cognito (ou seja, deslogado) para visualizar detalhes do usuário.

A regra “proprietário” permite que o usuário que criou o perfil crie, leia e atualize seu próprio perfil. Neste exemplo, não estamos permitindo que eles excluam seu próprio perfil, no entanto.

Neste ponto, podemos provisionar nossa infraestrutura de nuvem que suporta o recurso da API:

 
amplify push

Quando você muda o modelo de GraphQL existente de ToDo para UserProfile, se você já fez um amplify push e provisionou a infraestrutura, você pode receber um erro dizendo que a mudança solicitada exigiria destruir a tabela DynamoDB existente. O Amplify impede que você faça isso em caso de perda de dados da exclusão da tabela ToDo existente. Se você receber este erro, você precisa executar amplify push --allow-destructive-graphql-schema-updates.

Quando você faz um amplify push, o Amplify e o CloudFormation criarão uma API GraphQL AppSync, resolvedores intermediários e uma tabela DynamoDB de apoio semelhante a esta:



Uma vez que definimos um esquema GraphQL, podemos usar o Amplify para gerar o código Dart que representa a camada de modelo e repositório que pode trabalhar com a API:

 
amplify codegen models

Neste ponto, podemos adicionar alguns campos de entrada em nossa página para preencher o nome do usuário, localização e linguagem de programação favorita.

Veja como as mudanças nos campos de texto se parecem em nosso componente _MyHomePageState:

 

class _MyHomePageState extends State<MyHomePage> {
  final _nameController = TextEditingController();
  final _locationController = TextEditingController();
  final _languageController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    ...
                      children: <Widget>[
                        Padding(
                          padding: const EdgeInsets.symmetric(vertical: 20),
                          child: Text(
                          'User Profile',
                          style: Theme.of(context).textTheme.titleLarge,
                        )),
                        const ProfilePicture(),
                        TextField(
                          decoration: const InputDecoration(labelText: "Name"),
                          controller: _nameController,
                        ),
                        TextField(
                          decoration:
                              const InputDecoration(labelText: "Location"),
                          controller: _locationController,
                        ),
                        TextField(
                          decoration: const InputDecoration(
                              labelText: "Favourite Language"),
                          controller: _languageController,
                        )
                      ]

Em seguida, conectamos nossos TextFields ao AppSync GraphQL API para que, quando o usuário pressionar o botão de ação flutuante “Salvar”, as alterações sejam sincronizadas com o DynamoDB:

 

floatingActionButton: FloatingActionButton(
            onPressed: _updateUserDetails,
            tooltip: 'Save Details',
            child: const Icon(Icons.save),
          ),
        )
      ],
    );
  }

  Future<void> _updateUserDetails() async {
    final currentUser = await Amplify.Auth.getCurrentUser();

    final updatedUserProfile = _userProfile?.copyWith(
            name: _nameController.text,
            location: _locationController.text,
            language: _languageController.text) ??
        UserProfile(
            name: _nameController.text,
            location: _locationController.text,
            language: _languageController.text);

    final request = _userProfile == null
        ? ModelMutations.create(updatedUserProfile)
        : ModelMutations.update(updatedUserProfile);
    final response = await Amplify.API.mutate(request: request).response;

    final createdProfile = response.data;
    if (createdProfile == null) {
      safePrint('errors: ${response.errors}');
    }
  }

Finalmente, quando nossos usuários abrem o aplicativo, queremos buscar o perfil mais recente do cloud. Para isso, fazemos uma chamada como parte da inicialização do _MyHomePageState:

 

@override
  void initState() {
    super.initState();
    _getUserProfile();
  }

void _getUserProfile() async {
    final currentUser = await Amplify.Auth.getCurrentUser();
    GraphQLRequest<PaginatedResult<UserProfile>> request = GraphQLRequest(
        document:
            '''query MyQuery { userProfilesByUserId(userId: "${currentUser.userId}") {
    items {
      name
      location
      language
      id
      owner
      createdAt
      updatedAt
      userId
    }
  }}''',
        modelType: const PaginatedModelType(UserProfile.classType),
        decodePath: "userProfilesByUserId");
    final response = await Amplify.API.query(request: request).response;

    if (response.data!.items.isNotEmpty) {
      _userProfile = response.data?.items[0];

      setState(() {
        _nameController.text = _userProfile?.name ?? "";
        _locationController.text = _userProfile?.location ?? "";
        _languageController.text = _userProfile?.language ?? "";
      });
    }
  }

Agora temos uma API na qual podemos armazenar dados, protegida com Cognito e apoiada pelo DynamoDB. Muito legal, considerando que não precisei escrever nenhum código de infraestrutura.

Então, neste ponto, temos um meio de consultar, exibir e atualizar as informações do perfil do usuário. Parece outro ponto de salvamento para mim.

Parte Cinco: Adicionando Um Toque de Design

Por fim, o aplicativo de amostra que estendemos parece um pouco simples. É hora de animá-lo um pouco.

Agora, eu não sou especialista em UI, então tirei alguma inspiração do dribbble.com e decidi por um fundo colorido e uma área de cartão branca contrastante para os detalhes do perfil.

Adicionando uma Imagem de Fundo

Em primeiro lugar, queria adicionar uma imagem de fundo para trazer um pouco de cor para o app.

I had a go at wrapping the children of my Scaffold widget in a Container widget, which you can then apply a decoration property to. It works and it’s the more upvoted solution, but it doesn’t fill the app bar too, which would be nice.

I ended up using this approach, which utilizes a Stack widget to lay a full-height background image under our Scaffold: “Background Image for Scaffold” on Stack Overflow. 

O código resultante é este:

 

@override
  Widget build(BuildContext context) {

    return Stack(
      children: [
        Image.asset(
          "assets/background.jpg",
          height: MediaQuery.of(context).size.height,
          width: MediaQuery.of(context).size.width,
          fit: BoxFit.cover,
        ),
        Scaffold(
          backgroundColor: Colors.transparent,
          appBar: AppBar(
            // Aqui pegamos o valor do objeto MyHomePage que foi criado pelo método 
             // App.build e o usamos para definir o título da nossa AppBar. 
            title: Text(widget.title),
            backgroundColor: Colors.transparent,
            foregroundColor: Colors.white,
          ),
          body: Center(
          ...

Bem, isso parece meio bonito, mas o fundo é um pouco chocante em relação aos elementos editáveis na tela:


Então embrulhei os campos de texto e a foto de perfil em uma Card assim, definindo algum margin e padding para que não pareça apertado:

 

Scaffold(
          backgroundColor: Colors.transparent,
          appBar: AppBar(
            title: Text(widget.title),
            backgroundColor: Colors.transparent,
            foregroundColor: Colors.white,
          ),
          body: Center(
              child: Card(
                  margin: const EdgeInsets.symmetric(horizontal: 30),
                  child: Padding(
                    padding: const EdgeInsets.all(30),
                    child: Column(
                      ...


Essa é uma maneira de fazer, embora suspeite que haja uma abordagem mais idiomática que utilize o sistema de design material. Talvez um para outro post.

Alterar o Ícone e o Título do Aplicativo no Menu

Se você quiser alterar o ícone do seu aplicativo, precisa fornecer várias variantes do seu logotipo, todas em resoluções diferentes para iOS e Android separadamente. Ambos têm requisitos separados também (alguns dos quais você ignorará para evitar que seu aplicativo seja aprovado), então isso rapidamente se torna um trabalho tedioso.

Felizmente, há um pacote Dart que faz todo o trabalho pesado. Dado uma imagem fonte do seu ícone de aplicativo, ele pode gerar todas as permutações necessárias para ambos os plataformas.

Para este aplicativo de demonstração, apenas peguei um ícone de aplicativo aleatório das Imagens do Google:


Fonte: Imagens do Google

Seguindo o readme, me levou a definir este conjunto mínimo de configuração para gerar ícones com sucesso. Coloque isso no final do seu arquivo pubspec.yaml:

 

flutter_icons:
  android: true
  ios: true
  remove_alpha_ios: true
  image_path: "assets/app-icon.png"

Com o acima em vigor, execute este comando para gerar as variantes de ícone necessárias para ambos iOS e Android:

flutter pub run flutter_launcher_icons

Você deve ver uma série de arquivos de ícone gerados para Android e iOS respectivamente em android/app/src/main/res/mipmap-hdpi/ e ios/Runner/Assets.xcassets/AppIcon.appiconset/.

Infelizmente, alterar o nome do aplicativo parece ainda ser um processo manual. Usando um artigo intitulado “How to Change App Name in Flutter—The Right Way in 2023” no Flutter Beads como guia, alterei o nome do aplicativo nos seguintes dois arquivos para iOS e Android respectivamente:

ios/Runner/Info.plist

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleDisplayName</key>
    <string>Flutter Amplify</string> <--- App Name Here

android/app/src/main/AndroidManifest.xml

 

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.flutterapp">
   <application
        android:label="Flutter Amplify" <--- App Name Here
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

Isso lhe dá um pequeno ícone de aplicativo e título agora:


Conclusão

Então, isso conclui como começar a usar Flutter e AWS Amplify, e espero demonstrar como é rápido implantar os recursos e o código de scaffolding necessários para prototipar rapidamente um aplicativo móvel cross-platform.

I’m keen to get feedback on this tutorial, or any follow up tutorials people would like to see. All feedback is welcome and appreciated!

Problemas Encontrados

Ferramentas de Linha de Comando Android Ausentes

O local do meu Gerenciador do Android SDK é:

/Users/benfoster/Library/Android/sdk/tools/bin/sdkmanager

Fazer o seguinte instalou as ferramentas de linha de comando do Android: Stack Overflow “Failed to Install Android SDK Java Lang Noclassdeffounderror JavaX XML Bind A.”


flutter doctor --android-licenses

Aplicativo Fica Logado no iOS

Ao desenvolver, eu queria repetir o processo de login no aplicativo. Infelizmente (para mim) o aplicativo estava mantendo informações do usuário entre fechamentos do aplicativo—fechar e reabrir o aplicativo mantinha-o logado.

Minha experiência anterior com o desenvolvimento Android e o Amplify me convenceu de que remover o aplicativo e reiniciar com “flutter run” removeria o estado do usuário e começaria novamente. Infelizmente, nem mesmo isso teve o efeito desejado, então acabei apagando o telefone sempre que precisava começar com uma base limpa:


Source:
https://dzone.com/articles/cross-platform-mobile-app-prototyping-with-flutter-and-aws-amplify