login.dart<\/strong> troviamo:<\/p>\n\n\n\n\/\/ Call this when drawing component graphics\n@override\nWidget build(BuildContext context) {\n return loginUi(this);\n}\n<\/pre>\n\n\n\nSi pu\u00f2 notare come non vi sia della grafica, ma al suo posto, viene chiamata la classe loginUi <\/strong>passando la classe LoginState<\/strong> di tipo State<Login> <\/strong>mediante la chiave this<\/strong>. Ora vediamo login.ui.dart<\/strong>:<\/p>\n\n\n\nUiBuilder loginUi = (state) => Container(\n child: Stack(\n children: [\n\t...\n ]\n )\n);\n<\/pre>\n\n\n\nIl file adibito alla \u201cgrafica\u201d \u00e8 dichiarato come UiBuilder<LoginState> loginUi = (state) => Container(child: \u2026 <\/strong>dove UiBuilder<\/strong> \u00e8 un \u201chelper\u201d definito ad hoc (che vedremo a breve) e con tipo \u201cforzato\u201d a LoginState che passa la variabile state<\/strong> al metodo. Mediante questo approccio \u00e8 possibile definire la grafica separatamente dalla logica, garantendo inoltre accesso a tutte le variabili<\/strong> dichiarate in login.dart<\/strong> all\u2019interno della classe LoginState<\/strong>! A tal proposito abbiamo definito anche un package core<\/strong> che contiene l\u2019implementazione di UiBuilder:<\/p>\n\n\n\n <\/figure>\n\n\n\nOra vediamo ui-builder.dart<\/strong>:<\/p>\n\n\n\nimport 'package:flutter\/material.dart';\ntypedef UiBuilder = Widget Function(T context);\n<\/pre>\n\n\n\nNulla pi\u00f9 che la definizione di un nuovo \u2018type\u2019<\/strong> (UiBuilder) facente riferimento ad una funzione che ritorna un oggetto Widget<\/strong> (per la grafica) con un context<\/strong> che dipende dal tipo generico T<\/strong>, in questo modo abbiamo un metodo pulito per separare grafica e logica! Per completezza diamo uno sguardo al file login_layout.dart<\/strong>:<\/p>\n\n\n\nimport 'package:flutter\/material.dart';\n\nclass LoginLayout extends StatefulWidget {\n\n final Widget child;\n\n LoginLayout({ this.child, });\n\n }\n\nclass LoginLayoutState extends State {\n @override\n Widget build(BuildContext context) {\n return Container(\n\u2026\n appBar: null,\n body: \n...\n this.widget.child\n...\n );\n }\n}\n<\/pre>\n\n\n\nIn questo file i 2 elementi pi\u00f9 importanti sono il costruttore<\/strong> LoginLayout({ this<\/strong>.child<\/strong>, }); dove si richiede una variabile final <\/strong>Widget child<\/strong>; (la classe Login <\/strong>in questi esempi), \u00e8 quindi possibile iniettare la variabile nella propriet\u00e0 body<\/strong> del layout grazie a this<\/strong>.widget<\/strong>.child <\/strong>per includere la grafica del widget. Infine, per poter mostrare questo componente all\u2019interno di questo layout usiamo LoginLayout(child: Login()).<\/p>\n\n\n\nStruttura di Progetto<\/h2>\n\n\n\n Abbiamo deciso di sviluppare una semplice applicazione in cui un utente si pu\u00f2 registrare per ottenere accesso ad uno spazio personale su S3 per caricare e scaricare file. Per questo motivo abbiamo definito 2 domini<\/strong>: Autenticazione e Gestione di S3. <\/p>\n\n\n\nIl primo contiene i componenti di login <\/strong>e signup<\/strong>, mentre il secondo dominio \u00e8 adibito a gestire le azioni di upload<\/strong>, download<\/strong>, delete<\/strong> e list <\/strong>sul bucket dell’applicazione.<\/p>\n\n\n\nCreare i widget di Login e Signup widgets e connetterli con Cognito<\/h3>\n\n\n\n Come primo passo, abbiamo deciso di sviluppare le feature di login e signup per permettere agli utenti di registrarsi e avere dunque accesso all\u2019uso dell\u2019applicazione. Come prerequisito, abbiamo seguito la guida di Amplify per analytics prima, e quella per l\u2019autorizzazione dopo, come gi\u00e0 descritto precedentemente. <\/p>\n\n\n\n
Incominciamo con il lanciare il seguente comando nella cartella principale di progetto:<\/p>\n\n\n\n
amplify add analytics<\/pre>\n\n\n\nUtilizziamo i parametri di default. Una volta completato il comando, passiamo ad abilitare le funzionalit\u00e0 di login e signup con:<\/p>\n\n\n\n
amplify add auth<\/pre>\n\n\n\nEssendo il progetto una semplice POC, abbiamo deciso di lasciare i parametri di default come indicato di seguito:<\/p>\n\n\n\n
? Do you want to use the default authentication and security configuration?\n `Default configuration`\n? How do you want users to be able to sign in?\n `Username`\n? Do you want to configure advanced settings?\n `No, I am done.`\n<\/pre>\n\n\n\nQuindi abbiamo salvato la configurazione online utilizzando l\u2019utente creato durante la fase preliminare con:<\/p>\n\n\n\n
amplify push<\/pre>\n\n\n\nIn questa fase sarebbe buona cosa lanciare un flutter clean<\/strong> e ricaricare<\/strong> l\u2019applicazione sul cellulare per essere sicuri che la configurazione venga aggiornata correttamente sul dispositivo.<\/p>\n\n\n\nAvere la configurazione caricata online significa aver creato un bucket<\/strong> S3 con l\u2019applicazione Amplify<\/strong>, una user pool, <\/strong>e una identity pool<\/strong> su AWS Cognito. Tutte queste risorse possono essere lasciate cos\u00ec come sono.<\/p>\n\n\n\nOra concentrandoci sulla parte di codice, mostreremo le parti pi\u00f9 interessanti, poich\u00e8 l\u2019intero progetto \u00e8 disponibile sul nostro repository Github.<\/p>\n\n\n\n
Login<\/strong><\/h4>\n\n\n\nInnanzitutto utilizziamo questa riga di codice per identificare il form di login:<\/p>\n\n\n\n
final formKey = GlobalKey();<\/pre>\n\n\n\nQuesto passo \u00e8 necessario per permettere la validazione dei campi del form con un\u2019altra semplice riga di codice:<\/p>\n\n\n\n
\/\/ Validate the form with this line...\nif (formKey.currentState.validate()) {<\/pre>\n\n\n\nPer accedere ai valori dei campi del form si usano oggetti di tipo TextEditingController<\/strong> come i seguenti:<\/p>\n\n\n\nfinal usernameController = TextEditingController();\nfinal passwordController = TextEditingController();\n<\/pre>\n\n\n\nPer effettuare il login dell\u2019utente abbiamo invocato i metodi base di Amplify come da documentazione:<\/p>\n\n\n\n
SignInResult res = await Amplify.Auth.signIn(\n username: usernameController.text.trim(),\n password: passwordController.text.trim(),\n);\n<\/pre>\n\n\n\nE per passare alle schermate di signup<\/strong> e di s3<\/strong> si pu\u00f2 usare:<\/p>\n\n\n\nNavigator.push(context, MaterialPageRoute(builder: (context) => LoginLayout(child: Signup())));\nNavigator.push(context, MaterialPageRoute(builder: (context) => S3ViewerLayout(child: S3Viewer())));\n<\/pre>\n\n\n\nCome gi\u00e0 descritto in precedenza abbiamo passato il layout<\/strong> corretto al MaterialPageRoute<\/strong> specificando il componente<\/strong> desiderato. Inoltre per notificare<\/strong> l\u2019interfaccia <\/strong>che lo stato dell\u2019applicazione \u00e8 cambiato, abbiamo utilizzato il metodo di Flutter setState:<\/strong><\/p>\n\n\n\nsetState(() {\n loggingIn = false;\n});\n<\/pre>\n\n\n\nPer la parte di UI, diamo un’occhiata al file login.ui.dart<\/strong>; la variabile formKey<\/strong> viene assegnata al form per identificarlo in modo univoco:<\/p>\n\n\n\nchild: Form(\n key: state.formKey,\n<\/pre>\n\n\n\nPer standardizzare la grafica all\u2019interno dell\u2019applicazione sono stati creati due componenti condivisi: roundedTextFormField<\/strong> e roundedRectButton;<\/strong> entrambi definiti all\u2019interno del package shared<\/strong>. Giusto per curiosit\u00e0 diamo uno sguardo anche a questi:<\/p>\n\n\n\nimport 'package:flutter\/material.dart';\n\nWidget roundedTextFormField(TextEditingController controller, String hintText, Color mainColor, Color backColor, Function validation, obscured) {\n return Padding(\n padding: EdgeInsets.only(bottom: 10, left: 50, right: 50),\n child: TextFormField(\n obscureText: obscured,\n controller: controller,\n validator: (value) => validation(value),\n style: TextStyle(color: mainColor),\n decoration: new InputDecoration(\n border: new OutlineInputBorder(borderRadius: BorderRadius.circular(100.0),),\n filled: true,\n hintStyle: new TextStyle(color: mainColor.withOpacity(0.5)),\n hintText: hintText,\n fillColor: backColor\n ),\n )\n );\n}\n<\/pre>\n\n\n\nIn Dart <\/strong>si ha un comportamento simile a Javascript nel senso che \u00e8 possibile definire un widget <\/strong>e importarlo nel file dove sia necessario, proprio come nel caso presentato con roundedTextFormField<\/strong>. <\/p>\n\n\n\nSignup<\/strong><\/h4>\n\n\n\nPer quanto riguarda il processo di Signup, sono stati utilizzati i parametri di default di Amplify durante il wizard di configurazione: email<\/strong>, username<\/strong> (che verranno sfruttati per identificare lo spazio personale nel bucket S3) e una password valida<\/strong>. Dopo il processo di registrazione, avviene un processo<\/strong> di<\/strong> conferma<\/strong>. Una mail viene inviata all\u2019utente contenente un codice di conferma da inserire nel programma per completare la registrazione.<\/p>\n\n\n\nDiamo un’occhiata anche al file signup.dart. <\/strong>Ci sono due metodi principali: signupNewUser<\/strong> e confirmNewUser<\/strong>; l\u2019approccio \u00e8 simile a quello visto per il login:<\/p>\n\n\n\nif (formKey.currentState.validate()) {<\/pre>\n\n\n\nAbbiamo definito una variabile formKey<\/strong>, collegata poi al form della pagina, quindi si \u00e8 usato Amplify per la registrazione:<\/p>\n\n\n\n\/\/ Create a map attributes dictionary for holding extra information for the user\nMap userAttributes = {\n 'email': emailController.text.trim(),\n \/\/ additional attributes as needed: we set email because is a common way\n \/\/ to define a unique value to use for S3 folders\n};\n\n\/\/ Signup using Amplify with Cognito\nSignUpResult res = await Amplify.Auth.signUp(\n username: usernameController.text.trim(),\n password: passwordController.text.trim(),\n options: CognitoSignUpOptions(\n userAttributes: userAttributes\n )\n);\n<\/pre>\n\n\n\nAncora una volta si pu\u00f2 cambiare lo stato dell\u2019applicazione con setState<\/strong>, alterando cos\u00ec la visibilit\u00e0 di diversi componenti all\u2019interno del file signup.ui.dart <\/strong>mediante il Visibility Widget<\/strong>:<\/p>\n\n\n\nVisibility(\n visible: state.registering,\n\u2026\n<\/pre>\n\n\n\nPer mantenere il codice pi\u00f9 snello abbiamo usato setState<\/strong> anche in signup.dart<\/strong> per definire quale Widget rendere visibile: signup<\/strong> o confirm<\/strong>.<\/p>\n\n\n\n@override\nWidget build(BuildContext context) {\n return this.isSignUpComplete ? confirmUi(this) : signupUi(this);\n}\n<\/pre>\n\n\n\nIn pratica la variabile isSignUpComplete<\/strong> viene utilizzata per decidere quale widget mostrare (entrambi codificati nel file signup.ui.dart<\/strong>).<\/p>\n\n\n\nPer completare la registrazione utilizziamo:<\/p>\n\n\n\n
SignUpResult res = await Amplify.Auth.confirmSignUp(\n username: usernameController.text.trim(),\n confirmationCode: confirmController.text.trim()\n);\n<\/pre>\n\n\n\nInfine l\u2019utente viene inoltrato di nuovo alla pagina di login.<\/p>\n\n\n\n
Creare il widget di gestione di S3 e connetterlo con il Bucket<\/h3>\n\n\n\n Il manager di S3 \u00e8 un widget con quattro funzioni principali: <\/p>\n\n\n\n
Listare il file online<\/li> Caricare un nuovo file<\/li> Scaricare un file online<\/li> Cancellare un file online<\/li><\/ul>\n\n\n\nDi seguito vedremo come \u00e8 stato sviluppato il tutto. <\/p>\n\n\n\n
\/\/ Storage Item list\nList items = [];\n\n\/\/ Check if the app is uploading something\nbool isUploading = false;\n\n\/\/ Check if a file is being removed\nbool isRemoving = false;\n\n\/\/ Check if a file is being downloaded\nbool isDownloading = false;\n\n\/\/ Check if the app is retrieving the list of files\nbool isListing = false;\n<\/pre>\n\n\n\nCominciamo col definire un array contenente gli elementi da mostrare, e quattro variabili booleane per gestire i cambi di stato dell’applicazione relativi alle suddette quattro azioni: in questo modo \u00e8 semplice utilizzare setState<\/strong> per bloccare eventuali interazioni dell\u2019utente sui pulsanti durante l\u2019esecuzione di task lunghi. Durante l\u2019avvio del componente viene lanciato il metodo listFiles()<\/strong>, che necessita di conoscere il nostro utente autorizzato:<\/p>\n\n\n\nAuthUser user = await Core.getUser();\n<\/pre>\n\n\n\nE per listare tutti i file usiamo il metodo standard di Amplify senza opzioni:<\/p>\n\n\n\n
ListResult res = await Amplify.Storage.list();\n<\/pre>\n\n\n\nPrima di elencare i file, questi vengono filtrati per utente di modo da evitare di mostrare file di altri:<\/p>\n\n\n\n
items = res.items.where((e) => e.key.split('\/').first.contains(user.username)).toList();\n<\/pre>\n\n\n\nPer caricare un file, questo va recuperato dal cellulare e il modo pi\u00f9 semplice per farlo \u00e8 usare la libreria File Picker<\/strong> che gestisce da sola anche i permessi Android:<\/p>\n\n\n\nimport 'package:file_picker\/file_picker.dart';\n<\/pre>\n\n\n\nFile picker<\/strong> permette di recuperare un file e, grazie all\u2019utente autorizzato, \u00e8 possibile creare una chiave univoca per l\u2019upload:<\/p>\n\n\n\n\/\/ We put this outside of try to avoid logging user cancel\nFile file = await FilePicker.getFile();\nAuthUser user = await Core.getUser();\n\ntry {\n if(file.existsSync()) {\n\n setState(() {\n isUploading = true;\n });\n\n final key = user.username + '\/' + file.path.split('\/').last;\n\n \/\/ Upload the file\n UploadFileResult result = await Amplify.Storage.uploadFile(\n key: key,\n local: file\n );\n\n...\n<\/pre>\n\n\n\nPer rimuovere un file utilizziamo il metodo di Amplify passando la chiave identificativa:<\/p>\n\n\n\n
RemoveResult res = await Amplify.Storage.remove(\n key: item.key,\n);\n<\/pre>\n\n\n\nInfine per effettuare il download di un file sono necessari due elementi; verificare di nuovo i permessi dell\u2019utente perch\u00e9 le nuove versioni di android lo richiedono a runtime:<\/p>\n\n\n\n
import 'package:permission_handler\/permission_handler.dart';\n\nFuture checkPermission() async {\n final status = await Permission.storage.status;\n if (status != PermissionStatus.granted) {\n final result = await Permission.storage.request();\n if (result == PermissionStatus.granted) {\n return true;\n }\n } else {\n return true;\n }\n return false;\n}\n<\/pre>\n\n\n\nE chiaramente scaricare il file:<\/p>\n\n\n\n
var dir = await DownloadsPathProvider.downloadsDirectory;\nvar url = await Amplify.Storage.getUrl(key: item.key, options: GetUrlOptions(expires: 3600));\n\nawait checkPermission();\n\nfinal taskId = await FlutterDownloader.enqueue(\n url: url.url,\n fileName: item.key.split('\/').last,\n savedDir: dir.path,\n showNotification: true, \/\/ show download progress in status bar (for Android)\n openFileFromNotification: true, \/\/ click on notification to open downloaded file (for Android)\n);\n<\/pre>\n\n\n\nUna nota a parte: abbiamo riscontrato alcune difficolt\u00e0 a scaricare i file con il metodo apposito di download di Amplify. Siamo riusciti invece a completare l\u2019operazione con una combinazione dei metodi getUrl<\/strong> di Amplify e di FlutterDownloader.enqueue<\/strong>.<\/p>\n\n\n\nPer la parte di UI si pu\u00f2 vedere il file s3viewer.ui.dart<\/strong>; nulla di nuovo.<\/p>\n\n\n\nCome costruire il file di Core contenente le utility<\/h3>\n\n\n\n Il componente di core contiene alcuni metodi statici <\/strong>che servono da utility per tutta l’applicazione ma anche i metodi di validazione per i form. Diamo un\u2019occhiata ai seguenti esempi:<\/p>\n\n\n\n\/\/ Static method to get the current logged user\nstatic Future getUser() async {\n return Amplify.Auth.getCurrentUser();\n}\n<\/pre>\n\n\n\nQuello sopra per ottenere un utente di Amplify e di seguito un semplice validatore per le email:<\/p>\n\n\n\n
static emailValidator(value) {\n if (value.isEmpty) {\n return 'Please fill the field';\n }\n if (!RegExp(r\"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-\/=?^_`{|}~]+@[a-zA-Z0-9]+\\.[a-zA-Z]+\").hasMatch(value)) {\n return 'Please insert a valid email';\n }\n return null;\n}\n<\/pre>\n\n\n\nQuest’ultimo richiede una stringa e ritorna un messaggio di errore oppure null<\/strong> se l’input \u00e8 valido; quest\u2019ultima pu\u00f2 essere passata ad un TextFormField <\/strong>come propriet\u00e0 validator <\/strong>(si pu\u00f2 fare riferimento alla soluzione su github per completezza).<\/p>\n\n\n\nTestare l’applicazione su un dispositivo reale<\/h3>\n\n\n\n Per sviluppare l\u2019applicazione su un dispositivo reale, Flutter permette di compilare e testare il codice su un cellulare con una funzionalit\u00e0 di hot reload<\/strong>. Ci sono due modi per fare questo: 1) Premere play<\/strong> nella parte in alto a destra di Android Studio come in figura:<\/p>\n\n\n\n <\/figure>\n\n\n\n2) Scrivere flutter devices<\/strong> nel terminale e prendere nota del device id<\/strong>. Quindi, sempre nel terminale, scrivere flutter run -d <DEVICE_ID> -t .\/lib\/main.dart<\/strong>, assicurandosi di essere nella cartella di progetto.<\/p>\n\n\n\nIn modalit\u00e0 di test un flag di<\/strong> debug apparir\u00e0 automaticamente nella parte a destra della appBar<\/strong>, e premendo r <\/strong> nel terminale \u00e8 possibile forzare l\u2019hot reload.<\/p>\n\n\n\nPer completare i preliminari necessari alla build bisogna eseguiamo questi due step:<\/p>\n\n\n\n
1) Nella directory android<\/strong> accediamo a app\/src\/build.gradle <\/strong>e verifichiamo di avere questi valori per l\u2019 SDK:<\/p>\n\n\n\nminSdkVersion 23\ntargetSdkVersion 29\ncompiledSdkVersion 29\n<\/pre>\n\n\n\nQuesto passaggio serve ad evitare problemi di build con le librerie utilizzate per la demo.<\/p>\n\n\n\n
2) Sempre nella directory android <\/strong>apriamo app\/src\/main\/AndroidManifest.xml<\/strong> e aggiungiamo le seguenti permission:<\/p>\n\n\n\n<uses-permission android:name=\"android.permission.INTERNET\" \/>\n<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" \/>\n<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" \/>\n<\/pre>\n\n\n\n