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.
Si sigues este tutorial, no debería llevarte más de una hora. Bueno, me llevó varias horas lidiando con varios problemas, pero espero haberlos documentado lo suficientemente bien para que no los enfrentes.
Aquí está el producto terminado. Si quieres la versión “aquí tienes una que hice antes”, sigue los pasos en el readme, deberías tenerlo en funcionamiento en unos quince minutos. Aquí está el enlace de GitHub.
Este tutorial se compone de cinco partes:
- Requisitos previos y Configuración del Repositorio de Código
- Agregar Autenticación
- Subir una Imagen de Perfil
- Almacenamiento de Detalles del Usuario
- Agregar un Toque de Diseño
Recomendación
Flutter es una plataforma muy madura que se ha utilizado durante varios años, con una comunidad en auge y muchos complementos y extensiones para lograr la mayoría de las cosas.
Amplify, también, es una plataforma fuerte; sin embargo, encontré que la funcionalidad de la API era difícil de trabajar y las bibliotecas de Flutter no estaban actualizadas con los últimos anuncios y características en Amplify. En particular, trabajar con AppSync GraphQL y DataStore (para almacenamiento de datos sin conexión y sincronización) eran bastante frágiles (como verás más adelante).
Juntas, estas dos son una gran combinación para acelerar el desarrollo de prototipos de aplicaciones móviles, pero cuando sientas que estás obligando a Amplify a adaptarse a tus necesidades, no tengas miedo de abandonarlo a favor de trabajar directamente con los servicios de AWS que abstrae.
La aplicación de demostración que he construido almacena información de perfil de usuario, un requisito común de muchos aplicaciones. Puedes crear una cuenta y iniciar sesión, subir una foto de perfil y enviar algunos detalles sobre ti mismo. Entraremos en detalles sobre el full-stack—trabajando con Flutter y Dart para el código de la aplicación hasta el uso de DynamoDB para ofrecerte una amplia gama de lo que necesitas saber.
Parte Uno: Prerequisitos y Configuración del Repositorio de Código
Este tutorial asume que tienes lo siguiente ya configurado en tu 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 y Amplify tienen herramientas de scaffolding que crean la estructura inicial de tu proyecto. Es importante hacerlo en un cierto orden; de lo contrario, la estructura de tu carpeta no coincidirá con lo que las herramientas esperan, lo que te causará dolores de cabeza para solucionarlo más adelante.
Asegúrate de crear la estructura de tu repositorio de código usando Flutter primero, y luego inicializa Amplify dentro de él.
I used the official Flutter getting started documentation to kick things off for my demo.
Veamos si podemos hacer que Flutter funcione. Primero, para verificar que está instalado correctamente y agregado a tu PATH, puedes ejecutar flutter doctor
.
Si esta es tu primera incursión en el desarrollo móvil, habrá algunos elementos que necesitarán atención aquí. Para mí fue:
- Instalar Android Studio (y Android SDK CLI).
- Instalar XCode y CocoaPods.
- Aceptar los términos y condiciones para las herramientas de Android e iOS.
Creando el Repositorio de Código de Tu Aplicación
Cuando tengas todos los prerrequisitos listos, puedes crear el scaffolding de Flutter. Al hacerlo, se crea la carpeta en la que estaremos trabajando, así que ejecuta este comando desde un directorio padre:
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).
Podrías querer cambiar el nombre del directorio de nivel superior creado en este punto en caso de que no quieras que coincida con el nombre de tu aplicación. Lo cambié de “flutterapp” a “flutter-amplify-tutorial” (el nombre de mi repositorio de git).
En este punto, Flutter ha creado setenta y tres archivos para nosotros. Echemos un vistazo a qué son estos:
Las carpetas con las que pasaremos la mayor parte del tiempo son ios/android
y lib/
. Dentro de las carpetas ios
y android
se encuentran los recursos del proyecto que se pueden abrir con XCode y Android Studio respectivamente. Estos proyectos actúan como la interoperación entre el código Dart independiente de la plataforma y tus plataformas objetivo, y puedes usarlos para probar tu aplicación en las respectivas plataformas. Probemos eso con iOS ahora:
Configuración de iOS
open -a Simulator
flutter run
En mi Mac, con una configuración mínima de XCode, esto pasó de nada a ejecutar un simulador de iPhone 14 Pro Max con la aplicación Flutter en blanco, lo cual es bastante genial.
Si ves lo siguiente: entonces felicitaciones, has logrado generar con éxito el esqueleto.
También puedes abrir el proyecto ios/Runner.xcodeproj
dentro de XCode, explorar su contenido y ejecutarlo contra simuladores y dispositivos físicos como lo harías con cualquier otro proyecto de XCode.
Configuración de Android
Android es un poco menos directo, ya que debes configurar explícitamente un emulador en Android Studio antes de poder ejecutarlo. Abre el proyecto android/flutterapp_android.iml
dentro de Android Studio para comenzar, y luego puedes configurar y ejecutar un emulador para probar la aplicación.
Dale unos minutos a Android Studio para que descargue Gradle y todas las dependencias necesarias para ejecutar la aplicación; puedes seguir el progreso de esto en la barra de progreso en la esquina inferior derecha.
Cuando Android Studio se haya estabilizado, si ya tienes un dispositivo simulado configurado en AVD, deberías poder presionar el botón de reproducción en la esquina superior derecha de la ventana:
Y he aquí, la misma aplicación en Android:
Esto está demostrando el código de la aplicación de ejemplo proporcionado cuando creas un nuevo proyecto de Flutter. A lo largo de este tutorial, gradualmente reemplazaremos este código con el nuestro.
Este es un buen momento para hacer un commit de git, ahora que tenemos las bases configuradas para el desarrollo de Flutter. Ya estamos en un punto en el que podemos empezar a experimentar con el código de Flutter y ver nuestros resultados en iOS y Android simultáneamente.
Flutter utiliza Dart como lenguaje intermedio entre Android e iOS, y todo el código con el que interactuarás reside dentro del directorio lib/
. Debería haber un archivo main.dart
, que es donde comenzaremos a experimentar.
Configurar y Desplegar una Nueva Aplicación Usando Amplify
Ahora que tenemos las herramientas para aplicaciones móviles listas para trabajar, necesitamos alguna infraestructura de backend para apoyar la funcionalidad de la aplicación.
Utilizaremos AWS y sus numerosos servicios para respaldar nuestra aplicación, pero todo será gestionado usando el servicio AWS Amplify. La mayor parte será manejada de manera transparente para nosotros, y en lugar de preocuparnos por qué servicios utilizar, nos enfocaremos en qué características queremos desplegar.
Para comenzar, dentro de tu carpeta de código ejecuta lo siguiente:
amplify init
Esta instrucción inicializa AWS Amplify en tu proyecto. Si no lo has utilizado antes, te hará varias preguntas. Para las personas que colaboran en el proyecto y ejecutan este comando, configura su entorno local con la configuración de Amplify ya establecida.
Esto proporcionará algunos recursos iniciales de AWS para almacenar la configuración y el estado de tu aplicación Amplify, principalmente un bucket de S3.
El avance y el estado de la barra de despliegue mencionados podrían resultar familiares para algunos: se trata de CloudFormation, y al igual que AWS CDK, Amplify utiliza CFN en el fondo para proporcionar todos los recursos necesarios. Puedes abrir la consola de CloudFormation stacks para verlo en acción:
Finalmente, cuando la CLI esté completa, deberías ver una confirmación similar a la siguiente, y podrás ver tu nueva aplicación desplegada en la Consola de Amplify:
Administración del Entorno
AWS Amplify tiene la noción de “entornos”, que son despliegues aislados de tu aplicación y recursos. Históricamente, la noción de entornos se tenía que crear dentro del ecosistema que tuvieras: (por ejemplo, CloudFormation, CDK), utilizando convenciones de nombres y parámetros. En Amplify, es ciudadano de primera clase: puedes tener múltiples entornos que permiten patrones, como la provisión de entornos compartidos, a través de los cuales se promueven los cambios (por ejemplo, Dev > QA > PreProd > Prod) así como también proporcionar entornos por desarrollador o rama de característica.
Amplify también puede configurar y aprovisionar servicios CI/CD para usted utilizando la funcionalidad de alojamiento de Amplify y los puede integrar en sus aplicaciones para proporcionar un ecosistema de desarrollo integral. Esto configura CodeCommit, CodeBuild y CodeDeploy para manejar el control de versiones, la compilación y la implementación de aplicaciones. Esto no está cubierto en este tutorial, pero podría usarse para automatizar la compilación, las pruebas y la publicación de versiones en las tiendas de aplicaciones.
Parte Dos: Agregar Autenticación
Normalmente, tendría que aprender sobre el servicio de autenticación de AWS, Cognito, y servicios de apoyo, como IAM, conectar todo utilizando algo como CloudFormation, Terraform o CDK. En Amplify, es tan simple como hacer:
amplify add auth
Amplify add
le permite agregar varias “características” a su proyecto. Detrás de escena, Amplify desplegará y configurará todos los servicios requeridos que necesita utilizando CloudFormation, por lo que puede centrarse más en las características de sus aplicaciones y menos en la conexión.
Cuando digo que es tan fácil como escribir esas tres palabras mágicas mencionadas anteriormente… no es exactamente así. Amplify le hará varias preguntas para comprender cómo desea que las personas se autentiquen y qué controles desea implementar. Si elige “Configuración Predeterminada”, Amplify configurará la autenticación con valores predeterminados sensibles para que pueda comenzar rápidamente. Voy a elegir “Configuración Manual” para demostrar cuán configurable es Amplify.
La configuración anterior te permite crear cuentas solo con tu número de teléfono (no se necesita una dirección de correo electrónico), y verifica que eres el propietario real de ese número utilizando MFA para la verificación y posteriores intentos de inicio de sesión. Recomiendo encarecidamente el uso de OAuth como mecanismo de autenticación estandarizado, pero no lo he utilizado aquí por simplicidad.
Ahora, cuando agregas características, no se aprovisionan inmediatamente. Por eso los comandos se completan de manera misteriosa y rápida. Todos estos comandos preparan la configuración de tu Aplicación Amplify (y el entorno local) para desplegar estas características.
Para desplegar características (o cualquier cambio de configuración) necesitas hacer un push:
amplify push
Nota: esto es diferente del comando amplify publish
, que compila y despliega tanto los servicios backend como frontend. Push solo aprovisiona recursos backend (y eso es todo lo que necesitaremos en este tutorial, ya que estaremos construyendo aplicaciones móviles).
Cuando agregas autenticación (o cualquier característica de Amplify), Amplify agrega un archivo de Dart llamado lib/amplifyconfiguration.dart
. Este archivo se ignora en Git porque contiene credenciales sensibles relacionadas con los recursos desplegados y se sincroniza automáticamente con el entorno de Amplify en el que estás trabajando. Puedes encontrar más información sobre esto aquí.
En este punto, tenemos Amplify configurado con una aplicación y un entorno de desarrollo creados, y Cognito configurado para la autenticación. Es un buen momento para hacer un commit en git si estás siguiendo, para que puedas revertir a este punto si es necesario. Amplify ya debería haber creado un archivo .gitignore
para ti, excluyendo todos los archivos innecesarios.
Ahora que tenemos la infraestructura de autenticación de backend en su lugar, podemos comenzar a construir nuestra aplicación móvil con Flutter.
Autenticando Usuarios en Nuestra Aplicación
I’m following the steps outlined in the authentication for the AWS Amplify tutorial here.
Esto utiliza las pantallas y flujo de autenticación predeterminadas dentro de amplify_flutter
. Agrega las dependencias de Amplify Flutter agregando lo siguiente bajo “dependencies” dentro del archivo pubspec.yaml
:
amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0
Si no estás utilizando las extensiones de Flutter y Dart dentro de VSCode (o no estás usando VSCode), necesitarás seguir esto con un comando flutter pub get
. Si lo estás, entonces VSCode ejecutará esto automáticamente cuando guardes el archivo pubspec.yaml
.
Hay un enfoque de inicio rápido para integrar la autenticación que utiliza una biblioteca de UI de Authenticator prefabricada, ideal para impulsar rápidamente un flujo de inicio de sesión que puede ser personalizado más tarde. Usaremos eso en este tutorial para demostrar el extenso conjunto de bibliotecas de Amplify disponibles, y cuán rápidamente puedes integrarlas en tu aplicación.
Los pasos para integrar la biblioteca de Autenticación OOTB están aquí.
Podemos trasponer el autenticador que decoramos en el widget configurado en el código de ejemplo sobre el código proporcionado en el ejemplo de inicio rápido de Flutter de la siguiente manera:
class _MyAppState extends State {
@override
Widget build(BuildContext context) {
return Authenticator(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// Este es el tema de tu aplicación.
//
// Intenta ejecutar tu aplicación con "flutter run". Verás que
// la aplicación tiene una barra de herramientas azul. Luego, sin cerrar la app, intenta
// cambiar el primarySwatch a continuación a Colors.green y luego invoca
// "recarga en caliente" (presiona "r" en la consola donde ejecutaste "flutter run",
// o simplemente guarda tus cambios para "recargar en caliente" en un IDE de Flutter).
// Observa que el contador no se restableció a cero; la aplicación
// no se reinicia.
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');
}
}
}
¿Qué es un Widget?
Es el bloque de construcción básico en Flutter utilizado para componer diseños de IU y componentes. Casi todo en los diseños de Flutter son widgets: columnas, andamios, relleno y estilo, componentes complejos, etc. El ejemplo en la documentación de inicio de Flutter utiliza un Widget “Center” seguido de un Widget “Text” para mostrar un texto centrado que dice “Hello World”.
El código anterior decora el widget MyHomePage
con un widget de autenticador, agrega el plugin AmplifyAuthCognito
, y toma la configuración que el comando anterior amplify add auth
generó en lib/amplifyconfiguration.dart
para conectarse automáticamente a tu AWS Cognito User Pool.
Después de ejecutar Flutter, ejecute la demostración de la integración de autenticación, me tomó un tiempo que el paso “Ejecutando pod install” se completara (casi 5 minutos). Solo ten paciencia.
Una vez que se hayan realizado esas modificaciones de autenticación y el aplicativo se inicie, se te presenta una pantalla de inicio de sesión básica pero funcional.
Utilizando el flujo de “Crear Cuenta”, puedes proporcionar tu número de teléfono y una contraseña, y luego se te presenta un desafío de MFA para completar el registro. Luego puedes ver que el usuario se ha creado dentro de la Piscina de Usuarios de Cognito:
Puedes probar esto fácilmente en un dispositivo Android virtual también. Ni siquiera necesitas salir de VSCode si has instalado los plugins de Flutter y Dart, por lo que no es necesario abrir Android Studio. Solo selecciona el nombre del dispositivo activo actual (iPhone) en la esquina inferior derecha de VSCode, cambia a un dispositivo Android virtual que ya hayas creado, luego presiona “F5” para comenzar a depurar. La experiencia es bastante similar a iOS:
Al desplegar por primera vez después de implementar la biblioteca de autenticación, encontré la siguiente excepción al intentar compilar el aplicativo:
uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared in library [:amplify_auth_cognito_android]
Flutter es realmente útil en esta situación, ya que inmediatamente después de que se descarga esta traza de pila, proporciona una recomendación:
El SDK de Flutter parece estar ya sobreescribiendo esto en nuestro archivo build.gradle
:
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
...
defaultConfig {
// TODO: Especifica tu propio ID de aplicación único (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutterapp"
// Puedes actualizar los siguientes valores para que coincidan con las necesidades de tu aplicación.
// Para obtener más información, consulta: https://docs.flutter.dev/deployment/android#revisando-la-configuración-de-compilación.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Si bien Flutter, como mínimo, requiere que se utilice la API 16 (declarada en flutter.gradle
), la biblioteca Amplify Auth necesita al menos 21. Para corregir esto, solo cambia la minSdkVersion
de “flutter.minSdkVersion” a “21”.
Una vez que te autentiques, se te presenta la aplicación de ejemplo “botón para hacer clic” mostrada anteriormente. Ahora, es hora de comenzar a personalizarla según nuestras necesidades.
Parte Tres: Cargar una Imagen de Perfil
En este ejemplo, utilizaremos esta capacidad para permitir a los usuarios cargar una foto de sí mismos para ser utilizada como avatar dentro de la aplicación.
¿Quieres agregar características de almacenamiento a tu aplicación? No hay problema, solo haz:
amplify add storage
y Amplify proporcionará los servicios backend necesarios para que tu aplicación utilice almacenamiento basado en la nube. Amplify integra fácilmente Flutter con S3 para permitir a los usuarios de tu aplicación almacenar objetos. La flexibilidad de S3 te permite almacenar todo tipo de activos, y en combinación con Cognito y Amplify, puedes facilitar la provisión de áreas privadas para que los usuarios guarden fotos, videos, archivos, etc.
Los archivos pueden guardarse con acceso público, protegido o 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 nuestra imagen de perfil, la crearemos con acceso protegido para que solo el usuario pueda actualizar y eliminar su avatar, pero otros en la aplicación podrían verlo.
Aquí es donde comenzaremos a dar estilo y a construir la estructura de nuestra aplicación. Flutter está estrechamente integrado con el sistema de diseño material, utilizado ampliamente en el desarrollo de aplicaciones móviles para proporcionar una apariencia y sensación consistentes. Proporciona un conjunto de componentes compatibles entre plataformas, cuyos estilos pueden ser sobreescritos para construir una experiencia específica para tu marca.
La plantilla de inicio de Flutter ya encoge algunas widgets utilizando el widget MaterialApp. Previamente decoramos esto con un widget de autenticador. Ahora, expandiremos el widget hijo MyHomePage de MaterialApp para proporcionar una imagen de perfil.
Compones widgets juntos en un árbol, conocido como el “Jerarquía de Widgets“. Siempre comienzas con un widget de nivel superior. En nuestra aplicación es el widget envolvente de autenticador que maneja el inicio de sesión inicial. Scaffold
es un buen widget para basar tus diseños: se usa comúnmente como widget de nivel superior con aplicaciones material; y tiene una serie de marcadores de posición, como un botón de acción flotante, hoja inferior (para deslizar hacia arriba detalles adicionales), una barra de aplicaciones, etc.
Primero, agreguemos simplemente un widget de imagen que apunte a una URL de red. Más adelante reemplazaremos esto con uno que tomamos y subimos a S3. Utilicé los siguientes recursos para agregar una imagen con un marcador redondeado:
- API de Flutter
- Google Flutter
En el array de hijos del widget de columna anidada, agregue el siguiente widget de contenedor:
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),
),
)
],
Ahora podemos mostrar una imagen desde la web:
A continuación, permitiremos al usuario seleccionar una imagen de perfil desde una imagen en su dispositivo. Un poco de búsqueda en Google reveló esta biblioteca que abstrae los detalles de seleccionar imágenes:
- Google: “Pub Dev Packages Image Picker”
Solo se necesitan dos líneas de código para solicitar al usuario que seleccione una imagen:
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
Para iOS, debes agregar la clave NSPhotoLibraryUsageDescription
al archivo de configuración de Xcode <raíz del proyecto>/ios/Runner/Info.plist
para solicitar acceso a las fotos del usuario; de lo contrario, la aplicación se cerrará.
Vamos a integrar esto a un widget GestureDetector
, que al recibir un toque, solicitará al usuario que elija una imagen para su imagen de perfil:
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");
}
Llama a setState()
, actualizando los campos del widget dentro de la Lambda pasada a Flutter, para que sepa que debe llamar a la función build()
, donde el estado actualizado se puede utilizar para redibujar el widget. En nuestro caso, la imagen de perfil se llenará, por lo que crearemos un widget de contenedor que muestre la imagen. El operador ??
nulo-consciente proporciona un marcador de posición de perfil predeterminado para cuando el usuario aún no ha seleccionado una imagen.
También necesitarás agregar una imagen de marcador de posición de perfil en tu repositorio y referenciarla en tu archivo pubspec.yml
para que se incluya en la compilación. Puedes usar la imagen de mi repositorio, mientras agregas esto a tu archivo pubspec.yml
:
# La siguiente sección es específica para paquetes de Flutter.
flutter:
...
# Para agregar activos a tu aplicación, agrega una sección de activos, como esta:
assets:
- assets/profile-placeholder.png
En este punto, podemos seleccionar una foto de perfil de la galería de fotos del dispositivo y mostrarla como una imagen redondeada en la aplicación. Sin embargo, esta imagen no se guarda en ningún lugar; una vez que se cierra la aplicación, se pierde (y ningún otro usuario podría ver su foto tampoco).
Lo que haremos a continuación es conectar esto con un almacenamiento en la nube, como AWS S3. Cuando el usuario selecciona una foto de la galería de su dispositivo, la subiremos a su área privada en S3, y luego el widget de imagen extraerá la imagen desde allí (en lugar de directamente desde el dispositivo)en sí:
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);
});
}
}
Ahora, cuando nuestro usuario selecciona una foto de su dispositivo, nuestra aplicación la subirá a S3 y luego la mostrará en la pantalla.
A continuación, haremos que la aplicación descargue la foto de perfil del usuario desde S3 cuando se inicie:
@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");
});
}
}
Luego, refactorizaremos la lógica de la foto de perfil en su propio componente reutilizable. Puedes ver el componente terminado en mi repositorio de GitHub que contiene toda la lógica anterior. Luego puedes limpiar el componente _MyHomePageStage
y colocar tu nuevo widget en la jerarquía de la siguiente manera:
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
'User Profile',
style: Theme.of(context).textTheme.titleLarge,
)),
const ProfilePicture(),
TextField(...
Para concluir con la foto de perfil, agregaremos un spinner de carga para proporcionar feedback a los usuarios de que algo está ocurriendo. Utilizaremos un campo booleano _isLoading
para realizar un seguimiento de cuándo se está cargando la imagen, lo que cambiará entre mostrar el spinner o una imagen:
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 Cuatro: Almacenamiento de Detalles del Usuario (Abreviado)
Genial, ahora tenemos un esqueleto de aplicación móvil en su lugar que tiene usuarios, autenticación y fotos de perfil. A continuación, veamos si podemos crear una API que utilice las credenciales de los usuarios para recuperar información adicional sobre ellos.
Normalmente, diría: “¿Quieres una API? Simple:”
amplify add api
Aquí es donde la mayor parte del esfuerzo y la solución de problemas fue porque, dependiendo de la configuración que elijas, no está completamente soportada dentro del ecosistema de Amplify y Flutter. El uso de la tienda de datos y el modelo de la caja también puede resultar en patrones de lectura ineficientes, lo que puede volverse muy costoso y lento rápidamente.
Amplify proporciona una API de alto nivel para interactuar con los datos en AppSync, pero en este tutorial, estaré usando GraphQL con consultas de bajo nivel ya que proporciona más flexibilidad y permite usar un Índice Secundario Global en DynamoDB para evitar escaneos de tablas. Si quieres entender cómo llegué aquí y cuáles son las diversas trampas, consulta “Desafíos al trabajar con y ajustar AWS Amplify y Appsync con Flutter”.
Amplify intenta establecer por defecto las preguntas formuladas al crear una API, pero puedes anular cualquiera de estas al desplazarte hacia arriba hasta la opción que deseas cambiar. En esta situación, queremos un punto final de GraphQL (para aprovechar DataStore) y la autorización de la API será manejada por el Grupo de Usuarios de Cognito, ya que queremos aplicar un control de acceso detallado para que solo el usuario pueda actualizar o eliminar sus propios detalles (pero otros usuarios pueden verlos).
Cuando creamos la API, Amplify crea un esquema básico de tipo GraphQL ToDo. Actualizaremos esto y agregaremos algunas reglas de autorización antes de enviar los cambios de la API.
Modificar el esquema de GraphQL del “ToDo” para adaptarlo a la información de nuestro perfil de usuario.
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
}
La regla privada permite a los usuarios conectados ver los perfiles de cualquier otro. Al no utilizar public, evitamos que las personas que no están conectadas vean los perfiles. El proveedor de IAM impide que los usuarios accedan directamente a la API de GraphQL; necesitan usar la aplicación y utilizar el rol “no autenticado” dentro de nuestra piscina de identidades de Cognito (es decir, desconectados) para ver los detalles de los usuarios.
La regla “owner” permite al usuario que creó el perfil crear, leer y actualizar su propio perfil. En este ejemplo, sin embargo, no les permitimos eliminar su propio perfil.
En este punto, podemos aprovisionar nuestra infraestructura en la nube que soporta la función de la API:
amplify push
Cuando cambias el modelo de GraphQL existente de ToDo a UserProfile, si has realizado previamente un amplify push
y aprovisionado la infraestructura, es posible que recibas un error indicando que el cambio solicitado requeriría destruir la tabla de DynamoDB existente. Amplify te impide hacer esto en caso de pérdida de datos al eliminar la tabla ToDo existente. Si recibes este error, necesitas ejecutar amplify push --allow-destructive-graphql-schema-updates
.
Cuando realizas un amplify push
, Amplify y CloudFormation desplegarán una API de GraphQL de AppSync, resoluciones intermedias y una tabla de DynamoDB de respaldo similar a esto:

Una vez que hemos definido un esquema GraphQL, podemos utilizar Amplify para generar el código Dart que representa la capa de modelo y repositorio que puede funcionar con la API:
amplify codegen models
En este punto, podemos añadir algunos campos de entrada en nuestra página para poblar el nombre del usuario, ubicación y lenguaje de programación favorito.
Esto es lo que las modificaciones del campo de texto parecen en nuestro 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,
)
]
Luego conectamos nuestros TextFields al API de GraphQL de AppSync para que cuando el usuario pulse un botón de acción flotante “Guardar”, las modificaciones se sincronicen con 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, cuando nuestros usuarios abran la aplicación, queremos extraer el perfil más reciente de la nube. Para lograr esto, hacemos una llamada como parte de la inicialización de _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 ?? "";
});
}
}
Ahora tenemos una API en la que podemos almacenar datos, protegida con Cognito y respaldada por DynamoDB. Bastante genial considerando que no he tenido que escribir ninguna infraestructura como código.
Así que en este punto, tenemos una forma de consultar, mostrar y actualizar la información del perfil de un usuario. Me parece otro punto de guardado para mí.
Parte Cinco: Añadiendo un Toque de Diseño
Finalmente, la aplicación de muestra que hemos extendido parece un poco simple. Es hora de darle vida.
Ahora bien, no soy un experto en IU, así que tomé algo de inspiración de dribbble.com y decidí un fondo llamativo y una zona de tarjeta blanca contrastante para los detalles del perfil.
Añadiendo una Imagen de Fondo
En primer lugar, quería añadir una imagen de fondo para aportar color a la aplicación.
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.
El código resultante es el siguiente:
@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(
// Aquí tomamos el valor del objeto MyHomePage que fue creado por
// el método build de App, y lo utilizamos para establecer el título de nuestra appbar.
title: Text(widget.title),
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
),
body: Center(
...
Bueno, eso se ve bastante bonito, pero el fondo es un poco llamativo en comparación con los elementos editables en la pantalla:
Así que envolví los campos de texto y la imagen de perfil en un Card
de la siguiente manera, estableciendo un margen y un relleno para que no se vea apretado:
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 es una forma de hacerlo, aunque sospecho que hay un enfoque más idiomático que utiliza el sistema de diseño material. Tal vez uno para otra publicación.
Cambiar el Icono y Título de la Aplicación en el Menú
Si deseas cambiar el icono de tu aplicación, debes proporcionar una serie de variantes de tu logotipo, todas a diferentes resoluciones para iOS y Android por separado. Ambos tienen requisitos separados también (algunos de los cuales ignorarás para evitar que tu aplicación sea aprobada), por lo que esto rápidamente se convierte en un trabajo tedioso.
Afortunadamente, hay un paquete Dart que hace todo el trabajo pesado. Dada una imagen fuente de tu icono de aplicación, puede generar todas las permutaciones requeridas para ambos plataformas.
Para esta aplicación demo, simplemente tomé un icono de aplicación al azar de Google Images:
Fuente: Google Images
Siguiendo el readme, llegué a definir este conjunto mínimo de configuración para generar iconos con éxito. Coloca esto al final de tu archivo pubspec.yaml
:
flutter_icons:
android: true
ios: true
remove_alpha_ios: true
image_path: "assets/app-icon.png"
Con lo anterior en su lugar, ejecuta este comando para generar las variantes de iconos necesarias para ambos iOS y Android:
flutter pub run flutter_launcher_icons
Deberías ver una serie de archivos de icono generados para Android e iOS en android/app/src/main/res/mipmap-hdpi/
y ios/Runner/Assets.xcassets/AppIcon.appiconset/
respectivamente.
Lamentablemente, cambiar el nombre de la aplicación sigue siendo un proceso manual. Usando un artículo llamado “Cómo Cambiar el Nombre de la Aplicación en Flutter—La Forma Correcta en 2023” en Flutter Beads como guía, cambié el nombre de la aplicación en los siguientes dos archivos para iOS y 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">
Esto te da un bonito icono de aplicación y título ahora:
Conclusión
Entonces, eso concluye cómo ponerse en marcha con Flutter y AWS Amplify, y con suerte demuestra lo rápido que es desplegar los recursos y código de soporte y andamiaje necesarios para prototipar rápidamente una aplicación móvil 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
Herramientas de Línea de Comando de Android Ausentes
La ubicación de mi Android SDK Manager es:
/Users/benfoster/Library/Android/sdk/tools/bin/sdkmanager
Haciendo lo siguiente se instalaron las herramientas de línea de comando de Android: Stack Overflow “Error al Instalar Android SDK Java Lang Noclassdeffounderror JavaX XML Bind A.”
flutter doctor --android-licenses
La Aplicación Permanece Iniciada en iOS
Al desarrollar, quise repetir el proceso de iniciar sesión en la aplicación. Desafortunadamente (para mí) la aplicación estaba retendiendo la información del usuario entre cierres de la aplicación; cerrar y volver a abrir la aplicación la mantenía iniciada.
Mi experiencia previa con el desarrollo en Android y Amplify me convenció de que eliminar la aplicación y volver a ejecutar “flutter run” eliminaría el estado del usuario y comenzaría de nuevo. Desafortunadamente, ni siquiera esto tuvo el efecto deseado, por lo que terminé borrando el teléfono cada vez que necesitaba comenzar con una pizarra limpia:
Source:
https://dzone.com/articles/cross-platform-mobile-app-prototyping-with-flutter-and-aws-amplify