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 demorar no máximo uma hora. Bem, levei várias horas lutando com vários problemas, mas espero ter documentado bem o suficiente para que você não os encontre.
Aqui está o produto final. Se você quiser a “versão que eu fiz anteriormente”, siga os passos no readme, você deve conseguir que funcione em cerca de quinze minutos. Aqui está o link do GitHub.
Este tutorial é composto por cinco partes:
- Pré-requisitos e Configuração do Repositório
- Adicionando Autenticação
- Envio de Foto de Perfil
- Armazenando Detalhes do Usuário
- 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 de 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) eram bastante frágeis (como você verá mais tarde).
Juntas, essas 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 a seguir seu caminho, não tenha medo de abandoná-lo em favor de trabalhar diretamente com os serviços AWS que ele abstrai.
O aplicativo de demonstração que construí armazena informações de perfil do usuário—um requisito comum de muitos aplicativos. 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 a pilha completa—trabalhando com Flutter e Dart para o código do aplicativo, até mesmo com serviços como o DynamoDB, para fornecer uma ampla gama 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). |
O Flutter e o Amplify possuem ferramentas de scaffolding que criam a estrutura inicial do seu projeto. É importante fazer isso em uma determinada ordem; caso contrário, sua estrutura de pastas não se alinhará com o que as ferramentas esperam, o que causará muitas dores de cabeça para corrigir posteriormente.
Certifique-se de criar a estrutura do repositório de código usando o Flutter primeiro, e depois 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 ele está corretamente instalado e adicionado ao seu PATH, você pode executar o comando flutter doctor
.
Se esta for sua primeira incursão no desenvolvimento móvel, haverá alguns itens que precisarão de atenção aqui. Para mim, foram:
- Instalar o Android Studio (e o Android SDK CLI).
- Instalar o XCode e o CocoaPods.
- Concordar com os termos e condições para as ferramentas Android e iOS.
Criando o Repositório de Código do Aplicativo
Quando todos os pré-requisitos estiverem prontos, você pode criar o scaffolding do Flutter. Ao fazer isso, é criada 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 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 de plataforma e suas plataformas alvo, 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 a configuração mínima do XCode, isso passou de nada até executar um simulador de iPhone 14 Pro Max com o aplicativo Flutter scaffoldado em execução, o que é bem legal.
Se você vir o seguinte: parabéns, você conseguiu gerar com sucesso o esqueleto.
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 estiver estabilizado, 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 do aplicativo de exemplo fornecido quando você cria um novo projeto Flutter. Ao longo deste tutorial, iremos gradualmente substituindo esse código pelo nosso próprio.
Este é um bom momento para fazer um commit do git, agora que temos as bases configuradas para o desenvolvimento do Flutter. Agora estamos em um ponto em que podemos começar a mexer com o código do 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 no diretório 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 dar suporte à funcionalidade do aplicativo.
Vamos usar o AWS e seus numerosos serviços para dar suporte ao 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 quais serviços utilizar, focaremos nas funcionalidades que queremos implantar.
Para começar, dentro do seu diretório de código execute o seguinte:
amplify init
Este comando inicializa o AWS Amplify dentro do seu projeto. Se você ainda não o utilizou, será solicitado que responda a uma série de perguntas. Para as pessoas subsequentes que colaboram no projeto, ao executar este comando, o ambiente local será configurado 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.
O indicador de progresso e status da implantação mencionados acima podem parecer familiares a alguns — trata-se do CloudFormation, e assim como o AWS CDK, o Amplify utiliza o CFN nos bastidores para provisionar todos os recursos necessários. Você pode abrir o console das stacks do CloudFormation para ver isso em ação:
Finalmente, quando o CLI estiver completo, você deve ver uma confirmação semelhante ao exemplo 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 tinha que ser criada dentro de qualquer ecossistema que você possuísse: (por exemplo, CloudFormation, CDK), utilizando convenções de nomenclatura e parâmetros. No Amplify, isso é uma primeira classe — você pode ter múltiplos ambientes que permitem padrões, como provisionamento de ambientes compartilhados, em que as mudanças são promovidas através de (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 em 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:
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 tubulação.
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 colocar. Se você optar por “Configuração Padrão”, o Amplify configurará a autenticação com padrões sensíveis para que você possa 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 e-mail 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 aplicativo Amplify (e o 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 compila e implanta serviços de backend e frontend. Push provisiona apenas recursos de backend (e isso é tudo que precisaremos neste tutorial, pois estaremos criando aplicativos móveis).
Ao adicionar autenticação (ou qualquer recurso Amplify), o Amplify adiciona um arquivo Dart chamado lib/amplifyconfiguration.dart
. Este arquivo é ignorado pelo git 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 ambiente de desenvolvimento criados e o Cognito configurado para autenticação. É um bom momento para fazer um commit no git se você estiver acompanhando, para que possa voltar a este ponto se necessário. O Amplify já 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 fornecidos 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 no 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
.
Existe uma abordagem de início rápido para integrar a autenticação que usa uma biblioteca de interface de usuário de autenticador pré-fabricada, ideal para iniciar rapidamente um fluxo de entrada que pode ser personalizado posteriormente. Usaremos isso neste tutorial para demonstrar o amplo conjunto de bibliotecas 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 transpor 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ê rodou "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 IU. Praticamente tudo no Flutter são widgets—colunas, andaimes, preenchimento e estilo, componentes complexos, etc. O exemplo nas documentações 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 se conectar automaticamente ao seu AWS Cognito User Pool.
Após executar o Flutter e demonstrar a integração de autenticação, levei algum tempo para que a etapa “Executando a instalação do pod” fosse concluída. Tenha paciência (quase 5 minutos).
Uma vez que essas alterações de autenticação foram feitas e o aplicativo é iniciado, você é recebido por uma tela de login básica, mas funcional.
Usando o fluxo “Criar Conta”, você pode fornecer seu número de telefone e uma senha, e então é apresentado a um desafio de MFA para concluir o registro. Você pode ver então 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, portanto, 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 iniciar a depuração. A experiência é bem 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 nessa situação, pois logo após essa pilha de rastreamento ser descarregada, 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 da 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 a API 16 para ser utilizada (declarada em 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 enviem uma foto de si mesmos para serem usadas como avatar dentro da aplicação.
Quer adicionar recursos de armazenamento à sua aplicação? Sem problema, 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 integra facilmente o Flutter com o S3 para permitir 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 os 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 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 que 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 entre plataformas, cujos estilos podem ser substituídos para criar uma experiência específica para sua marca.
O modelo de inicialização do Flutter já estrutura 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 aninhado, 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. Um pouco de 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 foto:
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 do Xcode <raiz do projeto>/ios/Runner/Info.plist
para solicitar acesso às fotos do usuário; caso contrário, o aplicativo irá 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 ??
não preocupado com nulos fornece um placeholder padrão para o perfil quando o usuário ainda não selecionou uma imagem.
Você também precisará adicionar uma imagem de placeholder de perfil ao 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 na aplicação. No entanto, essa imagem não é persistida em nenhum lugar – uma vez que a aplicação é fechada, ela é perdida (e nenhum outro usuário conseguiria ver sua imagem também).
O que faremos a seguir é conectar isso a algum armazenamento em nuvem – o AWS S3. Quando o usuário seleciona uma foto da galeria do seu dispositivo, nós a enviaremos para sua área privada no S3, e então o widget de imagem irá buscar a imagem a partir daí (em vez de diretamente do dispositivo)ele mesmo:
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 do seu dispositivo, nossa aplicação a enviará para o S3 e então a exibirá na tela.
A seguir, faremos com que a aplicação baixe o avatar do usuário do S3 quando ela 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");
});
}
}
Em seguida, refatoraremos a lógica do avatar em seu próprio componente reutilizável. Você pode ver o componente concluído no meu repositório GitHub que abriga toda a lógica acima. Você pode então 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 finalizar 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 controlar quando a imagem está sendo carregada, o que alternará entre mostrar o spinner ou a imagem:
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 (Resumo)
Ótimo, agora temos um esqueleto de aplicativo móvel que possui usuários, autenticação e fotos de perfil. Em seguida, vamos ver se podemos 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 é onde a maior parte do esforço e solução de problemas ocorreu, porque, dependendo da configuração escolhida, 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, que podem rapidamente se tornar custosos e lentos.
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 Índice Global Secundário no DynamoDB para evitar varreduras na tabela. Se você deseja entender como cheguei aqui e quais são as várias armadilhas, confira “Desafios ao Trabalhar e Ajustar o AWS Amplify e o 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 ponto de extremidade GraphQL (para aproveitar o DataStore), e a autorização da API deve ser tratada pelo Pool de Usuários 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 criarmos a API, o Amplify cria um esquema básico de tipo GraphQL ToDo. 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 “público”, impedimos 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 de nossa piscina de identidades Cognito (ou seja, deslogados) para visualizar detalhes de usuários.
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 em nuvem que suporta o recurso da API:
amplify push
Ao alterar o modelo de GraphQL existente de ToDo para UserProfile, se você já executou um amplify push
e provisionou a infraestrutura, pode receber um erro indicando que a alteração solicitada exigiria a destruição da tabela DynamoDB existente. O Amplify impede que você faça isso no caso de dados perdidos ao excluir a tabela ToDo existente. Se você receber este erro, precisará executar amplify push --allow-destructive-graphql-schema-updates
.
Ao executar um amplify push
, o Amplify e o CloudFormation criarão uma API GraphQL AppSync, resolvers 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 alterações 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 um 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 da nuvem. 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, segura com o Cognito e apoiada pelo DynamoDB. Muito legal, considerando que não precisei escrever nenhum código de infraestrutura.
Portanto, neste momento, temos uma maneira de consultar, exibir e atualizar as informações de perfil do usuário. Parece outro ponto de salvamento para mim.
Parte Cinco: Adicionando um Toque de Design
Finalmente, o aplicativo de amostra que estendemos parece um pouco simples. É hora de trazê-lo à vida um pouco.
Agora, eu não sou um 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
Primeiro, queria adicionar uma imagem de fundo para trazer um pouco de cor ao aplicativo.
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 se parece com isto:
@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 criado pelo método
// App.build e o utilizamos 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(
...
Esta é uma maneira de fazê-lo, embora suspeite que haja uma abordagem mais idiomática que utilize o sistema de design material. Talvez uma para outro post.
Mude o Ícone e o Título do Aplicativo no Menu
Se você deseja 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 de origem do seu ícone do aplicativo, ele pode gerar todas as permutações necessárias para ambos os plataformas.
Para este aplicativo de demonstração, eu apenas peguei um ícone de aplicativo aleatório das Imagens do Google:
Fonte: Google Images
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, mudar o nome do aplicativo, pelo que pude descobrir, ainda é um processo manual. Usando um artigo chamado “How to Change App Name in Flutter—The Right Way in 2023” no Flutter Beads como guia, mudei 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 bom ícone de aplicativo e título agora:
Conclusão
Então, isso conclui como começar com Flutter e AWS Amplify, e espero demonstrar o quão rápido é implantar os recursos de suporte e o código de andaime necessário para prototipar rapidamente um aplicativo móvel multiplataforma.
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 do Android Ausentes
O local do meu gerenciador do Android SDK é:
/Users/benfoster/Library/Android/sdk/tools/bin/sdkmanager
Fazendo 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
Durante o desenvolvimento, eu queria repetir o processo de login no aplicativo. Infelizmente (para mim), o aplicativo estava mantendo as informações do usuário entre os fechamentos do aplicativo – fechar e reabrir o aplicativo mantinha-o conectado.
Minha experiência anterior com o desenvolvimento Android e o Amplify me convenceu de que remover o aplicativo e reiniciar o “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 tela limpa:
Source:
https://dzone.com/articles/cross-platform-mobile-app-prototyping-with-flutter-and-aws-amplify