Prototipazione di App Mobili Multistrato con 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 segui questo tutorial, dovresti impiegare non più di un’ora. Beh, per me ci sono voluti diversi ore lottando con vari problemi, ma spero di averli documentati abbastanza bene in modo che tu non dovrai affrontarli.

Ecco il prodotto finito. Se vuoi la versione “ecco quella che ho preparato in precedenza”, segui i passaggi nel readme, dovresti averlo in funzione in circa quindici minuti. Ecco il link GitHub

Questo tutorial è composto da cinque parti:

  1. Prerequisiti e Configurazione del Codebase
  2. Aggiunta dell’Autenticazione
  3. Caricamento di una Foto Profilo
  4. Archiviazione dei Dettagli Utente
  5. Aggiunta di Un Pò di Eleganza Grafica

Raccomandazione

Flutter è una piattaforma molto matura che viene utilizzata da diversi anni ormai, con una fiorente comunità e molti plugin ed estensioni per ottenere la maggior parte delle cose.

Amplify, inoltre, è una solida piattaforma; tuttavia, ho trovato la funzionalità dell’API difficile da utilizzare e le librerie Flutter non erano aggiornate con le ultime novità e funzionalità di Amplify. In particolare, lavorare con AppSync GraphQL e DataStore (per lo storage dati offline e la sincronizzazione) erano abbastanza fragili (come vedrai più avanti).

Accoppiate insieme, queste due sono una grande combinazione per accelerare lo sviluppo di prototipi di app mobili, ma quando ti senti come se stessi piegando Amplify ai tuoi scopi, non aver paura di abbandonarlo in favore di un lavoro direttamente con i servizi AWS che esso abstrae.

Il demo app che ho sviluppato contiene informazioni sul profilo utente, un requisito comune per molti app. Puoi creare un account e accedere, caricare un’immagine del profilo e inviare alcuni dettagli su te stesso. Approfondiremo i dettagli del full-stack, lavorando con Flutter e Dart per il codice dell’app fino ai più disparati servizi come DynamoDB, per offrirti una panoramica completa di ciò che devi sapere.

Parte Uno: Prerequisiti e Configurazione del Codebase

Questo tutorial presuppone che tu abbia già configurato quanto segue sul tuo computer:

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 offrono strumenti di scaffolding che creano la struttura iniziale del tuo progetto. È importante seguire un certo ordine; altrimenti, la struttura della cartella non corrisponderà a ciò che gli strumenti si aspettano, il che ti causerà problemi per correggerlo in seguito.

Assicurati di creare la struttura del codebase utilizzando Flutter prima, quindi inizializza Amplify al suo interno.

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

Vediamo se riusciamo a far funzionare Flutter. Innanzitutto, per verificare che sia correttamente installato e aggiunto alla tua variabile PATH, puoi eseguire flutter doctor.

Se questa è la tua prima esperienza nello sviluppo mobile, ci saranno alcuni elementi che dovranno essere affrontati. Per me erano:

  • Installazione di Android Studio (e CLI SDK Android).
  • Installazione di XCode e CocoaPods.
  • Accettazione dei termini e condizioni per gli strumenti Android e iOS.

Creazione del Codebase dell’App

Una volta che hai tutti i prerequisiti pronti, puoi creare lo scaffolding di Flutter. Questo crea la cartella in cui lavoreremo, quindi esegui questo comando da una directory genitore:

 
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).


Potresti voler rinominare il directory di primo livello creato in questo momento nel caso in cui non lo desideri corrispondente al nome del tuo app. L’ho cambiato da “flutterapp” a “flutter-amplify-tutorial” (il nome del mio repository git).

A questo punto, Flutter ha creato settantatré file per noi. Diamo un’occhiata a cosa sono:


Le cartelle con cui passeremo più tempo sono ios/android e lib/. All’interno delle cartelle ios e android ci sono risorse del progetto che possono essere aperte con XCode e Android Studio rispettivamente. Questi progetti fungono da interfaccia tra il codice Dart indipendente dalla piattaforma e le tue piattaforme target, e puoi usarli per testare il tuo app contro le rispettive piattaforme. Proviamo ora con iOS:

Configurazione iOS

 

open -a Simulator
flutter run

Sul mio Mac, con una configurazione minima di XCode, questo è passato da niente fino a far girare un simulatore iPhone 14 Pro Max con l’app Flutter scaffoldata in esecuzione, il che è piuttosto interessante.


Se vedi il seguente: allora congratulazioni, hai successo generare la scaffalatura.


Puoi anche aprire il progetto ios/Runner.xcodeproj all’interno di XCode, esplorare il suo contenuto e eseguire contro simulatore e dispositivi fisici come faresti con qualsiasi altro progetto XCode.

Configurazione Android

Android è un po’ meno semplice, poiché devi configurare esplicitamente un emulatore in Android Studio prima di poterlo eseguire. Apri il progetto android/flutterapp_android.iml all’interno di Android Studio per iniziare, e poi puoi configurare ed eseguire un emulatore per testare l’app.


Dài un po’ di tempo ad Android Studio per scaricare Gradle e tutte le dipendenze necessarie per far funzionare l’app—puoi monitorare il progresso di questo nella barra di avanzamento nell’angolo in basso a destra.

Una volta che Android Studio si è stabilizzato, se hai già configurato un dispositivo simulato in AVD, dovresti essere in grado di premere il pulsante di riproduzione nell’angolo in alto a destra della finestra:


E ecco, lo stesso app su Android:


Questo sta dimostrando il codice dell’app di esempio fornito quando crei un nuovo progetto Flutter. Nel corso di questo tutorial, gradualmente sostituiremo questo codice con il nostro.

Questo è un buon punto per fare un commit git, ora abbiamo impostato le fondamenta per lo sviluppo Flutter. Siamo ora in un punto in cui possiamo iniziare a giocare con il codice Flutter e vedere i nostri risultati su iOS e Android contemporaneamente.

Flutter utilizza Dart come linguaggio intermedio tra Android e iOS, e tutto il codice con cui interagirai risiede nella cartella lib/. Ci dovrebbe essere un file main.dart, che è dove inizieremo a giocare.

Configura e distribuisci una nuova app utilizzando Amplify

Ora che abbiamo le strumentazioni per app mobili pronte per lavorare, abbiamo bisogno di una infrastruttura backend per supportare la funzionalità dell’app.

Utilizzeremo AWS e i suoi numerosi servizi per supportare la nostra app, ma tutto ciò sarà gestito utilizzando il servizio AWS Amplify. La maggior parte di esso sarà gestita in modo trasparente per noi, e invece di preoccuparci dei servizi da utilizzare, ci concentreremo su quali caratteristiche vogliamo distribuire.

Per iniziare, all’interno della tua cartella del codice esegui quanto segue:

 
amplify init

Questo comando inizializza AWS Amplify all’interno del tuo progetto. Se non l’hai mai usato prima, ti farà una serie di domande. Per le persone successive che collaborano al progetto, eseguire questo comando configura il loro ambiente locale con la configurazione di Amplify già in atto.



Questo provisionerà alcuni primi risorse AWS per memorizzare la configurazione e lo stato del tuo app Amplify, vale a dire un bucket S3.

Il progress bar e lo stato di distribuzione sopra potrebbero sembrare familiari a qualcuno—è CloudFormation, e proprio come AWS CDK, Amplify utilizza CFN dietro le quinte per provisionare tutte le risorse richieste. Puoi aprire la console delle stack CloudFormation per vederlo in azione:


Infine, quando il CLI è completo, dovresti vedere una conferma simile alla seguente, e sarai in grado di vedere la tua nuova app distribuita nella Console Amplify:



Gestione dell’Ambiente

AWS Amplify ha il concetto di “ambienti”, che sono distribuzioni isolate del tuo applicazione e risorse. Storicamente, il concetto di ambienti doveva essere creato all’interno di qualunque ecosistema si aveva: (ad esempio, CloudFormation, CDK), utilizzando cose come convenzioni di denominazione e parametri. In Amplify, è una prima classe cittadina—puoi avere più ambienti che permettono pattern, come il provisioning di ambienti condivisi, che cambiano vengono promossi attraverso (ad esempio, Dev > QA > PreProd > Prod) così come fornendo ambienti per sviluppatore o feature-branch.

Amplify può anche configurare e fornire servizi CI/CD per te utilizzando l’hosting Amplify e integrarli nei tuoi app per fornire un ecosistema di sviluppo end-to-end. Questo configura CodeCommit, CodeBuild e CodeDeploy per gestire la gestione del controllo delle sorgenti, la costruzione e la distribuzione delle applicazioni. Questo non è trattato in questo tutorial, ma potrebbe essere utilizzato per automatizzare la costruzione, i test e la pubblicazione delle release nelle app store.

Parte Due: Aggiunta dell’Autenticazione

Di solito, dovresti imparare del servizio di autenticazione di AWS Cognito e dei servizi di supporto, come IAM, e collegarli insieme utilizzando qualcosa come CloudFormation, Terraform o CDK. In Amplify, è tanto semplice quanto fare:

 
amplify add auth

Amplify add ti permette di aggiungere vari “caratteristiche” al tuo progetto. Dietro le quinte, Amplify distribuirà e configurerà tutti i servizi necessari che ti servono utilizzando CloudFormation, così puoi concentrarti di più sulle caratteristiche delle tue app e meno sul collegamento.

Quando dico che è facile come digitare quei tre magic words sopra… non è così semplice. Amplify ti farà varie domande per capire come vuoi che le persone si autentichino e quali controlli vuoi in atto. Se scegli “Configurazione Predefinita“, Amplify configurerà l’autenticazione con valori predefiniti ragionevoli per farti partire rapidamente. Io scelgo “Configurazione Manuale” per dimostrare quanto sia configurabile Amplify.


Il setup sopra descritto consente di creare account utilizzando solo il proprio numero di telefono (non è necessario un indirizzo email) e verifica che tu sia il vero proprietario di quel numero utilizzando MFA per la verifica e ulteriori tentativi di accesso. Consiglio vivamente di utilizzare OAuth come meccanismo di autenticazione standardizzato, ma non l’ho usato qui per semplicità.

Ora, quando aggiungi funzionalità, queste non vengono istantaneamente attivate. Ecco perché i comandi sono stranamente rapidi da completare. Tutti questi comandi preparano la configurazione dell’app Amplify (e l’ambiente locale) per distribuire queste funzionalità.

Per distribuire funzionalità (o qualsiasi modifica di configurazione) è necessario eseguire un push:

 
amplify push

Nota: questo è diverso dal comando amplify publish, che compila e distribuisce sia i servizi backend che frontend. Push attiva solo le risorse backend (e questo è tutto ciò di cui avremo bisogno in questa guida introduttiva poiché costruiremo app mobili).

Quando aggiungi l’autenticazione (o qualsiasi funzionalità Amplify), Amplify aggiunge un file Dart chiamato lib/amplifyconfiguration.dart. Questo file è ignorato da Git perché contiene credenziali sensibili relative alle risorse distribuite e viene sincronizzato automaticamente con l’ambiente Amplify in cui stai lavorando. Puoi trovare maggiori informazioni su questo qui.

A questo punto, abbiamo Amplify configurato con un’app e un ambiente di sviluppo creati e Cognito configurato per l’autenticazione. È un buon momento per fare un commit git se stai seguendo, in modo da poter tornare a questo punto se necessario. Amplify dovrebbe aver già creato un file .gitignore per te, escludendo tutti i file non necessari.

Ora che abbiamo l’infrastruttura di autenticazione back-end a posto, possiamo iniziare a costruire il nostro app mobile con Flutter.

Autenticare gli Utenti nel Nostro App

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

Questo utilizza le schermate e il flusso di lavoro di autenticazione predefiniti forniti all’interno di amplify_flutter. Aggiungi le dipendenze di Amplify Flutter aggiungendo quanto segue sotto ” dependencies ” all’interno del file pubspec.yaml:

 

amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0

Se non stai utilizzando le estensioni Flutter e Dart all’interno di VSCode (o non stai utilizzando VSCode), dovrai seguire questo passo con il comando flutter pub get. Se lo sei, allora VSCode eseguirà automaticamente questa operazione quando salvi il file pubspec.yaml.

C’è un approccio di avvio rapido per l’integrazione dell’autenticazione che utilizza una libreria di interfacce utente preconfezionata, ottima per avviare rapidamente un flusso di accesso che può essere personalizzato in seguito. Lo useremo in questa esercitazione per dimostrare l’ampio set di librerie Amplify disponibili e quanto rapidamente è possibile integrarle nella tua app.

I passaggi per integrare la libreria di autenticazione OOTB sono qui.

Possiamo trasporre il widget autenticatore decorativo configurato nel codice di esempio sul codice fornito nell’esempio di avvio rapido di Flutter in questo modo:

 

class _MyAppState extends State {
  @override
  Widget build(BuildContext context) {
    return Authenticator(
        child: MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          // Questo è il tema dell'applicazione.
          //
          // Prova a eseguire l'applicazione con "flutter run". Vedrai che
          // l'applicazione ha una barra degli strumenti blu. Quindi, senza chiudere l'app, prova
          // a cambiare primarySwatch qui sotto in Colors.green e poi invoca
          // "hot reload" (premi "r" nella console dove hai eseguito "flutter run",
          // oppure salva semplicemente i tuoi cambiamenti per "hot reload" in un IDE Flutter).
          // Nota che il contatore non è tornato a zero; l'applicazione
          // non è stata riavviata.
          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');
    }
  }
}

Che cos’è un Widget? 

È il blocco costitutivo di base in Flutter utilizzato per comporre le interfacce utente e i componenti. Praticamente tutto nelle layout di Flutter sono widget—colonne, scaffolding, padding e stile, componenti complessi, ecc. L’esempio sulle documentazioni di avvio di Flutter utilizza un “Center” Widget seguito da un “Text” Widget per mostrare un pezzo di testo centrato che dice “Hello World.”

Il codice sopra decora il widget MyHomePage con un widget autenticatore, aggiunge il plugin AmplifyAuthCognito, e utilizza la configurazione generata dall’ultimo comando amplify add auth in lib/amplifyconfiguration.dart per connettersi automaticamente al tuo AWS Cognito User Pool.

Dopo aver eseguito Flutter, per dimostrare l’integrazione dell’autenticazione, mi è voluto un po’ di tempo perché il passaggio “Running pod install” si completasse per me. Siate pazienti (quasi 5 minuti).


Una volta apportate le modifiche all’autenticazione e avviato l’app, vi si presenta una schermata di accesso basilare ma funzionale.



Usando il flusso “Crea Account”, è possibile fornire il proprio numero di telefono e una password, e quindi si viene presentati con una sfida MFA per completare l’iscrizione. È possibile vedere che l’utente viene creato all’interno del Pool Utenti Cognito:


È possibile testare facilmente anche su un dispositivo Android virtuale. Non è nemmeno necessario uscire da VSCode se si hanno installati i plugin Flutter e Dart, quindi non è necessario aprire Android Studio. Basta selezionare il nome del dispositivo attivo corrente (iPhone) nell’angolo in basso a destra di VSCode, passare a un dispositivo Android virtuale che si è già creato, quindi premere “F5” per iniziare a eseguire il debug. L’esperienza è piuttosto simile a quella di iOS:



Durante il primo deploy dopo aver implementato la libreria di autenticazione, ho incontrato il seguente eccezione quando cercavo di compilare l’app:

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

Flutter è davvero utile in questa situazione, poiché subito dopo che viene scaricato il trace dello stack fornisce una raccomandazione:


Il Flutter SDK sembra già sovrascrivere questo nel nostro file build.gradle:

 

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

android {
    ...
    defaultConfig {
        // TODO: Specifica il tuo unico ID dell'applicazione (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutterapp"
        // Puoi aggiornare i seguenti valori per adattarli alle esigenze del tuo applicativo.
        // Per ulteriori informazioni, consulta: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

Mentre Flutter, come minimo, richiede l’API 16 per essere utilizzato (declarato in flutter.gradle), la libreria Amplify Auth necessita di almeno 21. Per risolvere questo, basta cambiare il minSdkVersion da “flutter.minSdkVersion” a “21.”

Una volta effettuata l’autenticazione, ti viene presentato l’app di esempio “button clicker” mostrato in precedenza. Ora è il momento di iniziare a personalizzare secondo le nostre esigenze.

Parte Tre: Caricamento di una Immagine del Profilo

In questo esempio, utilizzeremo questa funzionalità per consentire agli utenti di caricare una foto di se stessi da utilizzare come avatar all’interno dell’app.

Vuoi aggiungere funzionalità di archiviazione al tuo app? Nessun problema, basta fare:

 
amplify add storage

e Amplify provvederà alle risorse backend necessarie per l’utilizzo dell’archiviazione basata su cloud da parte del tuo app. Amplify integra facilmente Flutter con S3 per consentire agli utenti del tuo app di memorizzare oggetti. La flessibilità di S3 consente di memorizzare tutti i tipi di asset e, unitamente a Cognito e Amplify, è possibile facilmente provvedere aree private per gli utenti per memorizzare foto, video, file, ecc.

I file possono essere salvati con accesso pubblico, protetto o privato:

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

Per la nostra immagine del profilo, la creeremo con accesso protetto in modo che solo l’utente possa aggiornare e eliminare il proprio avatar, ma altri nell’app potranno visualizzarlo.

Questo è dove inizieremo a impostare lo stile e a costruire la struttura dell’app. Flutter è strettamente integrato con il sistema di design materiale, ampiamente utilizzato nello sviluppo di app mobili per garantire un aspetto e una sensazione coerenti. Offre un insieme di componenti compatibili tra piattaforme, le cui stili possono essere sovrascritti per creare un’esperienza specifica per il tuo brand.

Il modello di avvio di Flutter già scaffalatura alcuni widget utilizzando il widget MaterialApp. In precedenza abbiamo decorato questo con un widget autenticatore. Ora, espanderemo il widget figlio MyHomePage di MaterialApp per fornire un’immagine del profilo.

Combina i widget insieme in un albero, noto come “Gerarchia dei Widget“. Si inizia sempre con un widget di primo livello. Nella nostra app è il widget wrapper autenticatore che gestisce l’accesso iniziale. Scaffold è un buon widget su cui basare i layout: è comunemente usato come widget di primo livello con app material; e ha un numero di segnaposto, come un pulsante di azione fluttuante, un foglio inferiore (per scorrere ulteriori dettagli), una barra dell’app, ecc.

Per prima cosa, aggiungiamo solo un widget immagine che punta a un URL di rete. In seguito lo sostituiremo con uno che prendiamo e carichiamo su S3. Ho utilizzato le seguenti risorse per aggiungere un’immagine con segnaposto arrotondato:

  • API Flutter
  • Google Flutter

Nell’array children del widget colonna annidato, aggiungi il seguente widget 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),
              ),
            )
          ],

Ora possiamo mostrare un’immagine dalla rete:


Successivo, permetteremo all’utente di scegliere una foto profilo da un’immagine sul loro dispositivo. Un po’ di ricerca su Google ha rivelato questa libreria che semplifica la selezione delle immagini:

  • Google: “Pub Dev Packages Image Picker”

Sono sufficienti due righe di codice per invitare l’utente a selezionare una foto:

 

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

Per iOS, è necessario aggiungere la chiave NSPhotoLibraryUsageDescription al file di configurazione Xcode <project root>/ios/Runner/Info.plist per richiedere l’accesso alla libreria delle foto dell’utente; altrimenti, l’app si arresterà.

Integreremo questa funzionalità in un widget GestureDetector, che al ricevimento di un tap, suggerirà all’utente di scegliere una foto per il proprio 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");
  }

Richiama setState(), aggiornando i campi del widget all’interno della Lambda passata a Flutter, in modo che sappia di dover chiamare la funzione build(), dove lo stato aggiornato può essere utilizzato per ridisegnare il widget. Nel nostro caso, l’immagine del profilo sarà popolata, quindi creeremo un widget contenitore che visualizza l’immagine. L’operatore ?? sicuro per i valori nulli fornisce un’immagine di placeholder predefinita per quando l’utente non ha ancora selezionato una foto.

Inoltre, sarà necessario aggiungere un’immagine di placeholder per il profilo nel tuo repository e fare riferimento ad essa nel file pubspec.yml in modo che venga acquisita nel build. Puoi utilizzare l’immagine dal mio repo, aggiungendo questo al tuo pubspec.yml file:

 

# La seguente sezione è specifica per i pacchetti Flutter.
flutter:
...

  # Per aggiungere asset al tuo applicazione, aggiungi una sezione assets, come questa:
  assets:
    - assets/profile-placeholder.png


A questo punto, siamo in grado di selezionare un’immagine profilo dalla galleria delle foto del dispositivo e visualizzarla come immagine arrotondata nell’app. Tuttavia, questa immagine non viene mantenuta in alcun luogo; una volta chiusa l’app, viene persa (e nessun altro utente potrebbe vedere la tua immagine).

Quello che faremo dopo è collegare questa funzionalità a un cloud storage, AWS S3. Quando l’utente seleziona una foto dalla galleria del proprio dispositivo, la carceremo nella propria area privata in S3, quindi farò in modo che il widget dell’immagine estragga l’immagine da lì (anziché direttamente dal dispositivo) stesso:

 

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);
      });
    }
  }

Ora, quando il nostro utente seleziona un’immagine dal proprio dispositivo, la nostra app la caricherà su S3 e poi la visualizzerà sullo schermo.

Successivamente, faremo in modo che l’app scarichi l’immagine profilo dell’utente da S3 al suo avvio:

 

@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");
      });
    }
  }

In seguito, riorganizzeremo la logica dell’immagine profilo in un componente riutilizzabile. Potete visualizzare il componente finito nel mio repo GitHub che contiene tutta la logica sopra descritta. Potete quindi ripulire il componente _MyHomePageStage e inserire il vostro nuovo widget nella gerarchia in questo modo:

 

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

Per concludere sull’immagine profilo, aggiungeremo uno spinner di caricamento per fornire feedback agli utenti che qualcosa sta accadendo. Utilizzeremo un campo booleano _isLoading per tenere traccia del caricamento dell’immagine, il quale controllerà se mostrare lo spinner o l’immagine:

 

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 Quattro: Archiviazione dei Dettagli dell’Utente (Abridged)

Ottimo, ora abbiamo uno scheletro di app mobile in grado di gestire utenti, autenticazione e immagini profilo. Prossimo passo, vediamo se riusciamo a creare un’API che utilizzi le credenziali degli utenti per recuperare informazioni aggiuntive su di loro.

Normalmente direi: “Vuoi un’API? Semplice:”

 
amplify add api

Qui è dove la maggior parte dello sforzo e delle risoluzioni dei problemi si è concentrata, perché a seconda della configurazione scelta, non è completamente supportata all’interno dell’ecosistema Amplify e Flutter. L’uso del data store e del modello predefinito può anche comportare pattern di lettura inefficienti, che possono diventare rapidamente onerosi e lenti.

Amplify fornisce un’API di alto livello per interagire con i dati in AppSync, ma in questo tutorial utilizzerò GraphQL con query di basso livello poiché offre maggiore flessibilità e consente di utilizzare un Global Secondary Index in DynamoDB per evitare scansioni della tabella. Se vuoi capire come sono arrivato a questo punto e quali sono i vari problemi, dai un’occhiata a “Challenges Working with and Tuning AWS Amplify and Appsync with Flutter”.


Amplify tenta di impostare per default le domande poste durante la creazione di un’API, ma puoi sovrascrivere qualsiasi di queste scorrendo verso l’opzione che vuoi cambiare. In questa situazione, vogliamo un endpoint GraphQL (per sfruttare DataStore) e l’autorizzazione dell’API gestita dal Cognito User Pool, poiché vogliamo applicare un controllo di accesso fine-grained in modo che solo l’utente possa aggiornare o eliminare i propri dettagli (ma altri utenti possono visualizzarli).

Quando creiamo l’API, Amplify crea uno schema di tipo GraphQL ToDo di base. Aggiungeremo questo e alcune regole di autorizzazione prima di inviare le modifiche all’API.

Modificare lo schema GraphQL del modello “ToDo” per adattarlo alle nostre informazioni profilo utente.

 

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 regola privata consente agli utenti loggati di visualizzare i profili di chiunque altro. Non utilizzando “pubblico”, impediamo alle persone non loggate di visualizzare i profili. Il provider IAM impedisce agli utenti di accedere direttamente all’API GraphQL; devono utilizzare l’app e utilizzare il ruolo “non autenticato” all’interno del nostro pool di identità Cognito (cioè, disconnessi) per visualizzare i dettagli degli utenti.

La regola “owner” consente all’utente che ha creato il profilo di creare, leggere e aggiornare il proprio profilo. In questo esempio, non stiamo lasciando loro cancellare il proprio profilo però.

A questo punto, possiamo fornire la nostra infrastruttura cloud che supporta la funzionalità API:

 
amplify push

Quando si modifica il modello GraphQL esistente da ToDo a UserProfile, se in precedenza si è eseguito un amplify push e si è fornita l’infrastruttura, potrebbe essere visualizzato un errore che indica che il cambio richiesto richiederebbe la distruzione della tabella DynamoDB esistente. Amplify impedisce di farlo nel caso di dati persi dalla cancellazione della tabella ToDo esistente. Se si riceve questo errore, è necessario eseguire amplify push --allow-destructive-graphql-schema-updates.

Quando si esegue un amplify push, Amplify e CloudFormation creeranno un’API GraphQL AppSync, resolver intermediari e una tabella DynamoDB di supporto simile a questa:



Una volta definito uno schema GraphQL, possiamo utilizzare Amplify per generare il codice Dart che rappresenta il modello e il livello di repository che può lavorare con l’API:

 
amplify codegen models

A questo punto, possiamo aggiungere alcuni campi di input nella nostra pagina per popolare il nome dell’utente, la sua locazione e la sua lingua di programmazione preferita.

Ecco come appaiono le modifiche dei campi di testo nel 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,
                        )
                      ]

Quindi collegare i nostri TextFields all’API GraphQL AppSync in modo che quando l’utente preme il pulsante di azione fluttuante “Salva” , le modifiche vengano sincronizzate 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}');
    }
  }

Infine, quando i nostri utenti aprono l’app, vogliamo estrarre il profilo più recente dalla nube. Per realizzare ciò, effettuiamo una chiamata come parte dell’inizializzazione di _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 ?? "";
      });
    }
  }

Ora abbiamo un API in cui possiamo memorizzare i dati, protetto con Cognito e supportato da DynamoDB. Abbastanza interessante considerando che non ho dovuto scrivere alcuna infra-as-code.

Quindi a questo punto, abbiamo un modo per interrogare, visualizzare e aggiornare le informazioni del profilo dell’utente. Mi sembra un altro punto di salvataggio per me.

Parte Cinque: Aggiunta di un tocco di design

Infine, l’app di esempio che abbiamo esteso sembra un po’ semplice. È ora di darle un po’ di vita.

Ora, non sono un esperto dell’UI, quindi ho tratto ispirazione da dribbble.com e ho deciso di utilizzare uno sfondo vivace e un’area di scheda bianca contrastante per i dettagli del profilo.

Aggiunta di un’Immagine di Sfondo

Per prima cosa, volevo aggiungere un’immagine di sfondo per aggiungere un po’ di colore all’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. 

Il codice risultante è simile a questo:

 

@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(
            // Qui prendiamo il valore dall'oggetto MyHomePage creato dal metodo 
             // App.build e lo utilizziamo per impostare il titolo dell'appbar.
            title: Text(widget.title),
            backgroundColor: Colors.transparent,
            foregroundColor: Colors.white,
          ),
          body: Center(
          ...

Beh, sembra piuttosto carino, ma lo sfondo è un po’ scioccante rispetto agli elementi modificabili sullo schermo:


Quindi ho avvolto i campi di testo e l’immagine del profilo in un Card così, impostando un po’ di margine e spaziatura in modo che non sembri affollato:

 

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(
                      ...


Questo è un modo per farlo, anche se sospetto che ci sia un approccio più idiomatico che utilizza il sistema di design materiale. Forse uno per un altro post.

Modifica l’icona e il titolo dell’App nel Menu

Se vuoi cambiare l’icona del tuo app, devi fornire un certo numero di varianti del tuo logo, tutte a risoluzioni diverse per iOS e Android separatamente. Entrambi hanno requisiti separati anche (alcuni dei quali ignori per evitare che il tuo app venga approvato), quindi questo diventa rapidamente un lavoro noioso.

Per fortuna, c’è un pacchetto Dart che fa tutto il lavoro pesante. Dato un’immagine sorgente dell’icona del tuo app, può generare tutte le permutazioni richieste per entrambe le piattaforme.

Per questo demo app, ho appena preso un’icona di app casuale da Google Images:


Fonte: Google Images

Seguendo il readme mi ha portato a definire questo set minimo di configurazione per generare con successo le icone. Metti questo in fondo al tuo file pubspec.yaml:

 

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

Con quanto sopra in atto, esegui questo comando per generare le varianti dell’icona necessarie sia per iOS che per Android:

flutter pub run flutter_launcher_icons

Dovresti vedere una serie di file iconici generati per Android e iOS rispettivamente in android/app/src/main/res/mipmap-hdpi/ e ios/Runner/Assets.xcassets/AppIcon.appiconset/.

Il cambio del nome dell’app, a quanto ho potuto capire, purtroppo, è ancora un processo manuale. Utilizzando un articolo intitolato “Come Cambiare il Nome dell’App in Flutter—Il Modo Giusto nel 2023” su Flutter Beads come guida, ho modificato il nome dell’app nei seguenti due file per iOS e Android rispettivamente:

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">

Questo ti dà ora una graziosa icona dell’app e un titolo:


Concludendo

Quindi, questo conclude come impostare e avviare con Flutter e AWS Amplify, e si spera dimostra quanto sia rapido distribuire le risorse di supporto e il codice di scaffolding richiesto per prototipare rapidamente un’applicazione mobile multipiattaforma.

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!

Problemi Incontrati

Strumenti da riga di comando Android Mancanti

La posizione del mio gestore Android SDK è:

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

Eseguendo quanto segue sono stati installati gli strumenti da riga di comando Android: Stack Overflow “Failed to Install Android SDK Java Lang Noclassdeffounderror JavaX XML Bind A.”


flutter doctor --android-licenses

App Rimane Loggata su iOS

Durante lo sviluppo, volevo ripetere il processo di accesso all’app. Sfortunatamente (per me), l’app conservava le informazioni dell’utente tra le chiusure dell’app: chiudendo e riaprendo l’app, rimaneva collegata.

La mia esperienza precedente con lo sviluppo Android e Amplify mi aveva convinto che rimuovere l’app e rieseguire “flutter run” avrebbe eliminato lo stato utente e ricominciato da capo. Purtroppo neanche questo ha avuto l’effetto desiderato, quindi alla fine cancellavo il telefono ogni volta che necessitavo di partire con un tabula rasa:


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