Cross-Platform Mobiele App Prototyping met Flutter en 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.

Als je mee volgt met deze tutorial, zou het je niet langer dan een uur moeten duren. Nou, het kostte me verschillende uren om te vechten met verschillende problemen, maar hopelijk heb ik ze goed genoeg gedocumenteerd zodat je ze niet zult tegenkomen.

Hier is het eindproduct. Als je de “hier is er één die ik eerder heb gemaakt versie” wilt, volg dan de stappen in de readme, je zou het binnen ongeveer vijftien minuten moeten hebben opgestart en draaiend. Hier is de GitHub link

Deze tutorial bestaat uit vijf delen:

  1. Voorbereidingen en Instellen van de Codebase
  2. Toevoegen van Authenticatie
  3. Uploaden van een Profielafbeelding
  4. Opslaan van Gebruikersgegevens
  5. Toevoegen van Enige Designkwaliteit

Aanbeveling

Flutter is een zeer volwassen platform dat nu al enkele jaren wordt gebruikt, met een bloeiende gemeenschap en veel plugins en uitbreidingen om de meeste dingen te bereiken.

Amplify is ook een sterk platform; echter, vond ik de API-functionaliteit moeilijk om mee te werken en de Flutter bibliotheken waren niet up-to-date met de laatste aankondigingen en functies in Amplify. Vooral werken met AppSync GraphQL en DataStore (voor offline datastore en synchronisatie) waren vrij broos (zoals je later zult zien).

Samen zijn deze twee een geweldige combinatie voor het versnellen van de ontwikkeling van mobiele app prototypes, maar als je het gevoel krijgt dat je Amplify aan je wil buigt, moet je niet bang zijn om het op te geven ten gunste van werken rechtstreeks met de AWS-diensten die het abstracteert.

De demo-app die ik heb gebouwd bevat gebruikersprofielinformatie—een veelvoorkomende vereiste voor veel apps. Je kunt een account aanmaken en inloggen, een profielfoto uploaden en enkele gegevens over jezelf invullen. We gaan in op de details van de full-stack—werken met Flutter en Dart voor app-code tot aan DynamoDB om je een volledig overzicht te geven van wat je moet weten.

Deel Eén: Voorkennis en Opzetten van de Codebase

Deze tutorial veronderstelt dat je de volgende zaken al op je machine hebt ingesteld:

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 en Amplify hebben sjabloongereedschappen die de initiële projectstructuur creëren. Het is belangrijk om deze in een bepaalde volgorde te doen; anders zal je mapstructuur niet overeenkomen met wat de tools verwachten, wat je later hoofdpijn zal bezorgen om recht te zetten.

Zorg ervoor dat je je codebestandstructuur eerst met Flutter aanmaakt, en vervolgens Amplify erin initialiseert.

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

Laten we eens kijken of we Flutter kunnen laten werken. Eerst, om dubbel te controleren of je het correct hebt geïnstalleerd en toegevoegd aan je PATH, kun je flutter doctor uitvoeren.

Als dit je eerste ervaring is met mobiele ontwikkeling, zullen er enkele punten zijn die aandacht vereisen. Voor mij was het:

  • Het installeren van Android Studio (en Android SDK CLI).
  • Het installeren van XCode en CocoaPods.
  • Akkoord gaan met de gebruiksvoorwaarden voor Android en iOS hulpmiddelen.

Aanmaken van Je App Codebestand

Wanneer je alle voorkennis hebt klaargezet, kun je de Flutter sjabloon aanmaken. Door dit te doen creëer je de map waarin we zullen werken, dus voer deze opdracht uit vanuit een ouder directory:

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


Je zou de hoofdmap die op dit moment is aangemaakt, misschien wel willen hernoemen als je niet wilt dat deze overeenkomt met de naam van je app. Ik heb deze veranderd van “flutterapp” naar “flutter-amplify-tutorial” (de naam van mijn git-repository).

Op dit moment heeft Flutter zevenenzeventig bestanden voor ons aangemaakt. Laten we eens kijken wat deze zijn:


De mappen waar we het meest tijd mee zullen besteden, zijn ios/android en lib/. Binnen de ios en android mappen bevinden zich projectbronnen die respectievelijk kunnen worden geopend met XCode en Android Studio. Deze projecten fungeren als de tussenkomst tussen de platformonafhankelijke Dart-code en je doelplatforms, en je kunt ze gebruiken om je app te testen op de respectievelijke platforms. Laten we dat nu proberen met iOS:

iOS Setup

 

open -a Simulator
flutter run

Op mijn Mac, met minimale XCode-setup, ging dit van niets rechtstreeks naar het draaien van een iPhone 14 Pro Max simulator met de geskelette Flutter-app, wat best wel tof is. 


Als je het volgende ziet: dan gefeliciteerd, je hebt het skelet succesvol gegenereerd.


Je kunt ook het ios/Runner.xcodeproj project binnen XCode openen, de inhoud ervan verkennen en uitvoeren tegen simulatoren en fysieke apparaten zoals je dat zou doen met elk ander XCode-project.

Android Setup

Android is een beetje minder eenvoudig, aangezien je een emulator expliciet in Android Studio moet configureren voordat je deze kunt uitvoeren. Open het android/flutterapp_android.iml project binnen Android Studio om te beginnen, en dan kun je een emulator configureren en uitvoeren om de app te testen.


Geef Android Studio een paar minuten om Gradle en alle afhankelijkheden te downloaden die nodig zijn om de app uit te voeren—u kunt de voortgang hiervan volgen in de statusbalk rechtsonder.

Wanneer Android Studio zich heeft gevestigd, als u al een gesimuleerd apparaat hebt geconfigureerd in AVD, kunt u de speelknop in de rechterbovenhoek van het venster indrukken:


En zie daar, dezelfde app op Android:


Dit demonstreert de voorbeeld-appcode die wordt geleverd bij het maken van een nieuw Flutter-project. Gedurende deze tutorial zullen we stap voor stap deze code vervangen door onze eigen.

Dit is een goed moment om een git-commit uit te voeren, nu we de basisprincipes hebben ingesteld voor Flutter-ontwikkeling. We zijn nu op het punt waarop we met de Flutter-code kunnen spelen en onze resultaten tegelijkertijd op iOS en Android kunnen zien.

Flutter gebruikt Dart als tussenliggende taal tussen Android en iOS, en al het code waarmee u zult omgaan, bevindt zich in de lib/ map. Er zou een main.dart bestand moeten zijn, waar we aan beginnen te spelen.

Configureer en implementeer een nieuwe app met behulp van Amplify

Nu we de mobiele app-hulpmiddelen klaar hebben om mee te werken, hebben we enige backend-infrastructuur nodig om de functionaliteit van de app te ondersteunen.

We zullen AWS en zijn talrijke diensten gebruiken om onze app te ondersteunen, maar dit alles zal worden beheerd met behulp van de AWS Amplify-service. Het grootste deel ervan zal voor ons transparant worden afgehandeld, en in plaats van te zorgen over welke diensten te gebruiken, zullen we ons concentreren op welke functies we willen implementeren.

Om te beginnen, voer binnen uw code-map het volgende uit:

 
amplify init

Deze opdracht initialiseert AWS Amplify binnen uw project. Als u het nog niet eerder hebt gebruikt, zal het u een aantal vragen stellen. Voor volgende mensen die samenwerken aan het project, zorgt het uitvoeren van deze opdracht ervoor dat hun lokale omgeving wordt ingesteld met de Amplify-configuratie al in plaats.



Dit zal enkele initiële AWS-resources inrichten om de configuratie en staat van uw Amplify-app op te slaan, met name een S3-bucket.

De bovenstaande implementatievoortgangsbalk en status zullen voor sommigen bekend kunnen zijn—het is CloudFormation, en net als AWS CDK gebruikt Amplify CFN achter de schermen om alle vereiste resources in te richten. U kunt de CloudFormation stacks console openen om het in actie te zien:


Tenslotte, wanneer de CLI is voltooid, zou u een bevestiging moeten zien die lijkt op onderstaande, en u zult in staat zijn om uw zojuist geïmplementeerde App in de Amplify Console te zien:



Milieu Beheer

AWS Amplify kent het concept van “omgevingen”, die geïsoleerde implementaties van uw toepassing en resources zijn. Historisch gezien moest het concept van omgevingen worden gecreëerd binnen welk ecosysteem dan ook: (bijv. CloudFormation, CDK), met behulp van zaken zoals naamgevingsconventies en parameters. In Amplify is het een eersteklas burger—u kunt meerdere omgevingen hebben die patronen toestaan, zoals het inrichten van gedeelde omgevingen, waar wijzigingen worden gepromoveerd door (bijv. Dev > QA > PreProd > Prod) en om omgevingen per ontwikkelaar of feature-branch te bieden.

Amplify kan ook CI/CD-services voor u configureren en inrichten met behulp van de Amplify-hosting-add en ze integreren in uw apps om een end-to-end ontwikkelingsecosysteem te bieden. Dit zorgt voor CodeCommit, CodeBuild en CodeDeploy om broncodebeheer, bouwen en implementatie van toepassingen te verzorgen. Dit wordt niet in deze tutorial behandeld, maar kan worden gebruikt om het automatiseren van het bouwen, testen en publiceren van releases naar app stores.

Deel Twee: Authenticatie toevoegen

Meestal moet je leren over AWS’ authenticatiedienst Cognito en ondersteunende diensten, zoals IAM, alles aan elkaar plakken met iets als CloudFormation, Terraform of CDK. In Amplify is het zo simpel als:

 
amplify add auth

Amplify toevoegen stelt je in staat om verschillende “functies” aan je project toe te voegen. Achter de schermen zal Amplify alle vereiste services die je nodig hebt implementeren en configureren met behulp van CloudFormation, zodat je meer aandacht kunt besteden aan de functies van je app en minder aan de bedrading.

Als ik zeg dat het zo eenvoudig is als het typen van die drie magische woorden hierboven… het is niet helemaal zo eenvoudig. Amplify zal je verschillende vragen stellen om te begrijpen hoe je wilt dat mensen zich aanmelden en welke controles je wilt in plaats. Als je kiest voor “Standaardconfiguratie“, zal Amplify authenticatie instellen met redelijke standaardinstellingen om je snel aan de slag te helpen. Ik ga kiezen voor “Handmatige configuratie” om te laten zien hoe configureerbaar Amplify is.


De bovenstaande setup stelt je in staat om accounts te creëren met alleen je mobiele telefoonnummer (geen e-mailadres nodig) en verifieert dat je de werkelijke eigenaar bent van dat nummer met behulp van MFA voor verificatie en verdere aanmeldpogingen. Ik raad ten zeerste aan om OAuth te gebruiken als gestandaardiseerde authenticatiemechanisme, maar ik heb het hier niet gebruikt voor eenvoud.

Wanneer je functies toevoegt, worden deze niet onmiddellijk geprovisioneerd. Daarom zijn de opdrachten zo vreemd snel om te voltooien. Al deze opdrachten voorbereiden de configuratie van je Amplify App (en lokale omgeving) om deze functies te implementeren.

Om functies (of eventuele configuratiewijzigingen) te implementeren, moet je een push doen:

 
amplify push

Opmerking: dit verschilt van de amplify publish opdracht, die zowel back-end als front-end services bouwt en implementeert. Push beoogt alleen back-endbronnen te provisio­neren (en dat is alles wat we in deze tutorial nodig zullen hebben omdat we mobiele apps gaan bouwen).

Wanneer je auth (of elke Amplify-functie) toevoegt, voegt Amplify een dart-bestand toe genaamd lib/amplifyconfiguration.dart. Dit git wordt genegeerd omdat het gevoelige referenties bevat met betrekking tot je geïmplementeerde bronnen en wordt automatisch gesynchroniseerd met de Amplify-omgeving waarin je werkt. Meer informatie hierover kunt u hier vinden.

Op dit moment hebben we Amplify ingesteld met een app en een ontwikkelomgeving die is aangemaakt en Cognito is geconfigureerd voor authenticatie. Het is een goed moment om een git-commit uit te voeren als je meekomt, zodat je terug kunt gaan naar dit punt indien nodig. Amplify zou al een .gitignore bestand voor je hebben gemaakt, waarbij alle onnodige bestanden worden uitgesloten.

Nu we de achtergrondauthenticatie-infrastructuur hebben, kunnen we beginnen met het bouwen van onze mobiele app met Flutter.

Authenticatie van gebruikers in onze app

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

Dit gebruikt de standaard authenticatie-UI-schermen en -werkstroom die zijn ingebouwd in amplify_flutter. Voeg Amplify Flutter-afhankelijkheden toe door het volgende onder “dependencies” in het pubspec.yaml bestand toe te voegen:

 

amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0

Als je niet gebruikmaakt van de Flutter en Dart-extensies binnen VSCode (of gebruikmaakt van VSCode), moet je hierna een flutter pub get opdracht volgen. Als dat wel het geval is, zal VSCode deze automatisch uitvoeren wanneer je het pubspec.yaml bestand opslaat.

Er is een snelstartbenadering voor het integreren van authenticatie die gebruikmaakt van een voorgefabriceerde Authenticator UI-bibliotheek, geweldig voor het snel een aanmeldingsstroom opzetten die later kan worden aangepast. We zullen dat in deze tutorial gebruiken om de uitgebreide set Amplify-bibliotheken te demonstreren en hoe snel je ze in je app kunt integreren.

Stappen voor het integreren van de standaard Authentication-bibliotheek zijn hier.

We kunnen de authenticator-widget die is geconfigureerd in het voorbeeldcode overbrengen op de code die wordt geboden in het Flutter quickstart-voorbeeld zoals volgt:

 

class _MyAppState extends State {
  @override
  Widget build(BuildContext context) {
    return Authenticator(
        child: MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          // Dit is het thema van uw applicatie.
          //
          // Probeer uw applicatie uit te voeren met "flutter run". Je zult zien dat
          // de applicatie een blauwe balk heeft. Probeer vervolgens zonder de app te sluiten
          // de primarySwatch hieronder te veranderen in Colors.green en vervolgens aan te roepen
          // "hot reload" (druk op "r" in de console waar je "flutter run" hebt uitgevoerd,
          // of gewoon uw wijzigingen opslaan voor "hot reload" in een Flutter IDE).
          // Merk op dat de teller niet terug is gezet naar nul; de applicatie
          // wordt niet opnieuw opgestart.
          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');
    }
  }
}

Wat Is een Widget? 

Het is het basisbouwsteen in Flutter die wordt gebruikt om UI-layouts en componenten te creëren. Vrijwel alles in Flutter-layouts zijn widgets—kolommen, bekisting, padding en stijlen, complexe componenten, enz. Het voorbeeld in de Flutter-documentatie voor beginners gebruikt een “Center” Widget gevolgd door een “Text” Widget om een centraal uitgelijnd stuk tekst weer te geven dat zegt “Hello World.”

De bovenstaande code decoreert de MyHomePage widget met een authenticator-widget, voegt de AmplifyAuthCognito plugin toe en gebruikt de configuratie die de vorige amplify add auth commando heeft gegenereerd in lib/amplifyconfiguration.dart om automatisch verbinding te maken met uw AWS Cognito User Pool.

Na het uitvoeren van Flutter, om de authenticatie-integratie te demonstreren, duurde het een tijdje voordat de stap “Running pod install” bij mij was voltooid. Wees geduldig (bijna 5 minuten).


Zodra die authenticatiewijzigingen zijn aangebracht en de app opstart, wordt je begroet met een eenvoudige maar functionele aanmeldingsscherm.



Met behulp van de “Create Account” flow kun je je telefoonnummer en wachtwoord opgeven en word je vervolgens geconfronteerd met een MFA-uitdaging om de registratie te voltooien. Je kunt dan zien dat de gebruiker is gemaakt binnen de Cognito User Pool:


Je kunt dit ook gemakkelijk genoeg testen op een virtueel Android-apparaat. Je hoeft niet eens VSCode te verlaten als je de Flutter en Dart-plugins hebt geïnstalleerd, dus het openen van Android Studio is niet nodig. Selecteer gewoon de naam van het huidige actieve apparaat (iPhone) in de rechterbenedenhoek van VSCode, schakel over naar een virtueel Android-apparaat dat je al hebt aangemaakt en druk vervolgens op “F5” om debuggen te starten. Het ervaring is vrij vergelijkbaar met iOS:



Toen ik voor het eerst implementeerde na het toevoegen van de authenticatielibrary, ontdekte ik de volgende uitzondering bij het proberen de app te bouwen:

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

Flutter is in deze situatie echt behulpzaam, want direct na het dumpen van deze stack trace biedt het een aanbeveling:


Het Flutter SDK lijkt dit al te negeren in ons build.gradle bestand:

 

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

android {
    ...
    defaultConfig {
        // TODO: Specificeer uw eigen unieke Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutterapp"
        // U kunt de volgende waarden aanpassen aan de behoeften van uw applicatie.
        // Voor meer informatie, zie: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

Hoewel Flutter, als minimum, API 16 vereist om te worden gebruikt (aangekondigd in flutter.gradle), heeft de Amplify Auth bibliotheek ten minste 21 nodig. Om dit te corrigeren, verander gewoon de minSdkVersion van “flutter.minSdkVersion” naar “21.”

Zodra u zich heeft aangemeld, wordt u geconfronteerd met de voorbeeld “knop klikker” app die eerder is getoond. Nu is het tijd om aan te passen aan onze behoeften.

Deel Drie: Uploaden van een Profiel Foto

In dit voorbeeld zullen we deze mogelijkheid gebruiken om gebruikers toe te staan een foto van zichzelf te uploaden die wordt gebruikt als hun avatar binnen de app.

Wil je opslagfuncties aan je app toevoegen? Geen probleem, doe gewoon:

 
amplify add storage

en Amplify zal de achterendiensten die nodig zijn voor je app in de cloud inrichten. Amplify integreert gemakkelijk Flutter met S3 om gebruikers van je app toe te staan objecten op te slaan. De flexibiliteit van S3 stelt je in staat allerlei soorten activa op te slaan, en in combinatie met Cognito en Amplify kun je gemakkelijk privégebieden voor gebruikers inrichten om foto’s, video’s, bestanden, enz. op te slaan.

Bestanden kunnen worden opgeslagen met openbaar, beschermd of privé toegang:

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

Voor ons profielafbeelding zullen we het maken met beschermde toegang, zodat alleen de gebruiker hun avatar kan bijwerken en verwijderen, maar anderen in de app kunnen het bekijken.

Dit is waar we de stijl en structuur van onze app gaan ontwerpen en bouwen. Flutter is nauw geïntegreerd met het material design-systeem, dat veel wordt gebruikt bij de ontwikkeling van mobiele apps om een consistente uitstraling te bieden. Het biedt een set kruisbesturingssysteem-compatibele componenten, waarvan de stijlen kunnen worden overschreven om een ervaring te creëren die specifiek is voor uw merk.

Het Flutter-sjabloon voor het aan de slag gaan bouwt al enkele widgets op met behulp van de MaterialApp-widget. We hebben dit eerder versierd met een authenticator-widget. Nu gaan we de MyHomePage-kindwidget van MaterialApp uitbreiden om een profielfoto te bieden.

Je combineert widgets samen in een boom, bekend als de “Widget Hiërarchie“. Je begint altijd met een top-level widget. In onze app is de authenticator-wrapper-widget die de eerste aanmelding afhandelt. Scaffold is een goede widget om je lay-outs op te baseren: het wordt vaak gebruikt als top-level widget bij material apps; en het heeft een aantal plaatsaanduidingen, zoals een zwevende actieknop, een onderste vellen (voor het omhoog vegen van aanvullende details), een app-balk, enz.

Laten we in eerste instantie gewoon een afbeeldingswidget toevoegen die naar een netwerk-URL wijst. We zullen dit later vervangen door een die we nemen en uploaden naar S3. Ik heb de volgende bronnen gebruikt om een afbeelding toe te voegen met een afgeronde placeholder:

  • API Flutter
  • Google Flutter

Voeg in de kinderen array van de geneste kolomwidget de volgende containerwidget toe:

 

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),
              ),
            )
          ],

We kunnen nu een afbeelding weergeven vanaf het web:


Vervolgens laten we de gebruiker een profielfoto kiezen uit een afbeelding op hun apparaat. Een klein beetje googlen onthulde deze bibliotheek die de details van het selecteren van afbeeldingen abstracteert:

  • Google: “Pub Dev Packages Image Picker”

Twee regels code is alles wat het kost om de gebruiker te vragen een afbeelding te selecteren:

 

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

Voor iOS moet je de NSPhotoLibraryUsageDescription sleutel toevoegen aan het <project root>/ios/Runner/Info.plist Xcode configuratiebestand om toestemming te vragen om de gebruikersfoto’s te bekijken; anders zal de app crashen.

We zullen dit aansluiten op een GestureDetector widget, die bij ontvangst van een tik, de gebruiker zal vragen om een afbeelding te kiezen voor hun profielfoto:

 

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

Roep setState() aan, waarbij de widgetvelden binnen de Lambda doorgegeven aan Flutter worden bijgewerkt, zodat deze weet dat de build() functie moet worden aangeroepen, waar de bijgewerkte staat kan worden gebruikt om de widget opnieuw te tekenen. In ons geval zal de profielfoto worden ingevuld, dus we zullen een container widget maken die de afbeelding weergeeft. De null-bewuste ?? operator biedt een standaard profielplaatsaanwijzer voor wanneer de gebruiker nog geen afbeelding heeft geselecteerd.

Je moet ook een profielplaatsaanwijzer-afbeelding toevoegen aan je repository en deze verwijzen in je pubspec.yml bestand, zodat deze wordt opgenomen in de build. Je kunt de afbeelding uit mijn repo gebruiken, terwijl je dit toevoegt aan je pubspec.yml bestand:

 

# De volgende sectie is specifiek voor Flutter pakketten.
flutter:
...

  # Om middelen aan je toepassing toe te voegen, voeg een middelen sectie toe, zoals dit:
  assets:
    - assets/profile-placeholder.png


Op dit moment kunnen we een profielfoto selecteren uit de fotogalerie van het apparaat en deze in de app weergeven als een afgeronde foto. Deze foto wordt echter nergens bewaard—zodra de app wordt afgesloten, is de foto verloren (en kunnen ook geen andere gebruikers uw foto zien).

Wat we hierna gaan doen is deze koppeling aanbrengen met enkele cloudopslag—AWS S3. Wanneer de gebruiker een foto selecteert uit de galerie van zijn apparaat, uploaden we deze naar zijn privégebied in S3, en laten we de afbeeldingswidget de afbeelding daar ophalen (in plaats van direct vanaf het apparaat) zelf:

 

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

Nu, wanneer onze gebruiker een foto selecteert van zijn apparaat, zal onze app deze uploaden naar S3 en vervolgens op het scherm weergeven.

Vervolgens zullen we de app laten downloaden van de profielfoto van de gebruiker van S3 wanneer deze opstart:

 

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

Daarna zullen we de logica voor de profielfoto herstructureren in een eigen hergebruikbare component. U kunt het afgewerkte component bekijken in mijn GitHub repo dat alle bovenstaande logica herbergt. U kunt vervolgens de _MyHomePageStage component opruimen en uw nieuwe widget zo in de hiërarchie plaatsen:

 

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

Om af te ronden over de profielfoto, voegen we een laadspinner toe om feedback te geven aan gebruikers dat er iets gebeurt. We gebruiken een _isLoading boolean veld om bij te houden wanneer de foto aan het laden is, wat bepaalt of de spinner of een foto wordt getoond:

 

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

Deel Vier: Opslaan van Gebruikersgegevens (ingekort)

Goed, we hebben nu een mobiele app skelet gereed dat gebruikers, authenticatie en profielfoto’s omvat. Volgende stap, laten we kijken of we een API kunnen maken die gebruikersreferenties gebruikt om aanvullende informatie over hen op te halen.

Normaal gesproken zou ik zeggen: “je wilt een API? Simpel:”

 
amplify add api

Dit is waar het grootste deel van de inspanningen en probleemoplossing plaatsvond, omdat afhankelijk van de gekozen configuratie, het niet volledig wordt ondersteund binnen het Amplify en Flutter ecosysteem. Het gebruik van de standaard data store en model kan ook resulteren in inefficiënte leespatronen, wat snel duur en traag kan worden.

Amplify biedt een hoog niveau API voor interactie met gegevens in AppSync, maar in deze tutorial gebruik ik GraphQL met lagere niveau query’s omdat het meer flexibiliteit biedt en het gebruik van een Global Secondary Index in DynamoDB toestaat om tabelscans te vermijden. Wil je begrijpen hoe ik hier gekomen ben en wat de verschillende valkuilen zijn, bekijk dan “Challenges Working with and Tuning AWS Amplify and Appsync with Flutter”.


Amplify probeert standaardvragen te stellen bij het maken van een API, maar je kunt elk van deze overschrijven door naar boven te scrollen naar de optie die je wilt wijzigen. In dit scenario willen we een GraphQL eindpunt (om DataStore te benutten) en de API autorisatie te worden behandeld door de Cognito User Pool, zoals we fijne toegangscontrole willen toepassen, zodat alleen de gebruiker hun eigen details kan bijwerken of verwijderen (maar andere gebruikers deze kunnen bekijken).

Wanneer we de API maken, creëert Amplify een basale ToDo GraphQL schema type. We zullen dit aanpassen en enkele autorisatieregels toevoegen voordat we de API wijzigingen pushen.

Pas het “ToDo” sjabloon GraphQL schema aan om ons gebruikersprofiel informatie.

 

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
}

Een privéregel staat ingelogde gebruikers toe om de profielen van iedereen anders te bekijken. Door niet te werken met publiek, voorkomen we dat mensen die niet zijn ingelogd profielen kunnen bekijken. De IAM-provider voorkomt dat gebruikers direct toegang krijgen tot de GraphQL API—ze moeten de app gebruiken en de rol “onbevoegd” binnen onze Cognito-identiteitspool gebruiken (d.w.z. uitgelogd) om gebruikersgegevens te bekijken.

De “eigenaar” regel staat de gebruiker die het profiel heeft gemaakt toe om hun eigen profiel te maken, te lezen en aan te passen. In dit voorbeeld laten we ze echter niet toe om hun eigen profiel te verwijderen.

Op dit moment kunnen we onze cloudinfrastructuur inrichten die de API-functie ondersteunt:

 
amplify push

Wanneer u het bestaande GraphQL-model van ToDo naar UserProfile verandert, als u eerder een amplify push heeft uitgevoerd en de infrastructuur heeft ingericht, krijgt u mogelijk een foutmelding dat de gewenste wijziging het vernietigen van de bestaande DynamoDB-tabel vereist. Amplify voorkomt dat u dit doet in het geval van verloren gegevens door de bestaande ToDo-tabel te verwijderen. Als u deze fout krijgt, moet u amplify push --allow-destructive-graphql-schema-updates uitvoeren.

Wanneer u een amplify push uitvoert, zullen Amplify en CloudFormation een AppSync GraphQL API, tussenliggende oplossers en een ondersteunende DynamoDB-tabel opzetten die op deze manier eruit ziet:



Nadat we een GraphQL-schema hebben gedefinieerd, kunnen we Amplify gebruiken om de Dart-code te genereren die de model- en repositorylaag vertegenwoordigt die kan werken met de API:

 
amplify codegen models

Op dit moment kunnen we enkele invoervelden toevoegen aan onze pagina om de naam, locatie en favoriete programmeertaal van de gebruiker te vullen.

Zo ziet de wijziging van het tekstveld eruit in onze _MyHomePageState component:

 

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,
                        )
                      ]

Vervolgens verbinden we onze TextFields met de AppSync GraphQL API, zodat wanneer de gebruiker op een “Save” zwevende actieknop drukt, de wijzigingen worden gesynchroniseerd met 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}');
    }
  }

Ten slotte, wanneer onze gebruikers de app openen, willen we de meest recente profielgegevens uit de cloud ophalen. Om dit te bereiken, maken we een oproep als onderdeel van de initialisatie van onze _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 ?? "";
      });
    }
  }

We hebben nu een API waarin we gegevens kunnen opslaan, beveiligd met Cognito en gedekt door DynamoDB. Best tof, aangezien ik geen infra-as-code heb hoeven schrijven.

Dus op dit moment hebben we een manier om een gebruikersprofielgegevens op te vragen, weer te geven en bij te werken. Voelt voor mij als een nieuw savepunt.

Deel Vijf: Wat Designverfijning Toevoegen

Tot slot ziet de voorbeeld-app die we hebben uitgebreid er een beetje saai uit. Tijd om het wat levendiger te maken.

Nu ben ik geen UI-expert, dus heb ik wat inspiratie gehaald van dribbble.com en besloten op een luidruchtige achtergrond en contrasterend witte kaartgebied voor de profieldetails.

Achtergrondafbeelding Toevoegen

Eerst wilde ik een achtergrondafbeelding toevoegen om wat kleur in de app te brengen.

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. 

Het resulterende codefragment ziet er zo uit:

 

@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(
            // Hier nemen we de waarde van het MyHomePage-object dat is aangemaakt door 
             // de App.build-methode en gebruiken deze om de titel van onze appbar in te stellen. 
            title: Text(widget.title),
            backgroundColor: Colors.transparent,
            foregroundColor: Colors.white,
          ),
          body: Center(
          ...

Nou, dat ziet er best mooi uit, maar de achtergrond is een beetje prikkelend tegenover de bewerkbare elementen op het scherm:


Dus wikkelde ik de tekstvelden en het profielfoto in een Card zoals hieronder, met wat marge en padding zodat het niet er geperst uitziet:

 

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


Dit is een manier om het te doen, hoewel ik vermoed dat er een meer idiomatische aanpak is die gebruik maakt van het materiaal ontwerp systeem. Misschien één voor een ander bericht.

Wijzig de App-pictogram en Titel in Menu

Als je het pictogram van je app wilt wijzigen, moet je een aantal varianten van je logo aanbieden, allemaal op verschillende resoluties voor iOS en Android afzonderlijk. Beide hebben ook aparte vereisten (waarvan sommige je zult negeren om te voorkomen dat je app wordt goedgekeurd), dus dit wordt al snel een vervelende klus.

Gelukkig is er een Dart-pakket dat al het zware werk doet. Gegeven een bronafbeelding van je app-pictogram, kan het alle permutaties genereren die nodig zijn voor beide platforms.

Voor deze demo-app heb ik gewoon een willekeurig app-pictogram van Google Images gepakt:


Bron: Google Images

Volgend de readme leidde me tot het definiëren van deze minimale configuratie om pictogrammen succesvol te genereren. Plaats dit onderaan je pubspec.yaml bestand:

 

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

Met de bovenstaande in plaats, voer deze opdracht uit om de icon varianten te genereren die nodig zijn voor zowel iOS als Android:

flutter pub run flutter_launcher_icons

Je zou een reeks icon-bestanden moeten zien gegenereerd voor Android en iOS respectievelijk in android/app/src/main/res/mipmap-hdpi/ en ios/Runner/Assets.xcassets/AppIcon.appiconset/.

Het wijzigen van de app-naam, helaas, blijft nog steeds een manuele procedure. Volgens een artikel genaamd “How to Change App Name in Flutter—The Right Way in 2023” op Flutter Beads, heb ik de app-naam veranderd in de volgende twee bestanden voor iOS en Android:

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

Dit geeft je nu een mooi klein app-icoon en titel:


Afsluiten

Dus, dat besluit hoe je aan de slag kunt gaan met Flutter en AWS Amplify, en hopelijk laat zien hoe snel het is om de ondersteunende resources en skeletcode te implementeren die nodig is om snel een mobiele cross-platform applicatie te prototyperen.

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!

Problemen waargenomen

Ontbrekende Android Command-Line tools

De locatie van mijn Android SDK Manager is:

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

Het volgende installeerde de Android command-line tools: Stack Overflow “Failed to Install Android SDK Java Lang Noclassdeffounderror JavaX XML Bind A.”


flutter doctor --android-licenses

App Blijft Ingelogd op iOS

Terwijl ik aan het ontwikkelen was, wilde ik het proces van inloggen in de app herhalen. Helaas (voor mij) behield de app gebruikersinformatie tussen app-sluitingen – het sluiten en opnieuw openen van de app hield het ingelogd.

Mijn eerdere ervaring met Android-ontwikkeling en Amplify overtuigde mij ervan dat het verwijderen van de app en het opnieuw uitvoeren van “flutter run” de gebruikersstatus zou verwijderen en opnieuw zou beginnen. Helaas had zelfs dit niet het gewenste effect, dus eindigde ik ertoe om de telefoon elke keer te wissen wanneer ik met een schone lei wilde beginnen:


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