Da quando Flutter For Web è stato ufficialmente rilasciato nella sua versione “Technical Preview”, ho sempre desiderato di fare un giro di prova. Allo stesso tempo, in un mio progetto attuale ho avuto la necessità di dover sviluppare un veloce webtool helper in grado di dirmi se un endpoint accetta (o meno) richieste cross-origin da chiunque.
Per cui, l’occasione era troppo ghiotta per lasciarmela sfuggire!
Ecco gli step necessari:
- Installare Flutter For Web Tecnical Preview
- Effettuare l’Install/Upgrade delle estensioni per la propria IDE (io utilizzo VS Code)
- Creare un nuovo progetto
- Configurare il nuovo progetto
- Scrivere la business logic (il divertimento inizia qui!)
- Andare in produzione
Al momento della stesura di questo articolo, si sarà in grado di rilasciare codice in produzione che funzioni su tutti i browser Chromium-based, Firefox e Safari. Non ho testato con Edge. Eventuali feedback a riguardo saranno i benvenuti.
1. Installare flutter for web technical preview
Per iniziare, dobbiamo assicurarci di avere una versione di flutter maggiore o uguale a v1.5.4. Al momento io utilizzo v1.5.4-hotfix.2, sul canale master.
Eseguiamo il seguente comando per effettuare l’upgrade:
flutter upgrade
Per assicurarci di avere una versione corretta di Flutter, eseguiamo Flutter Doctor:
flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, v1.5.4-hotfix.2, on Mac OS X 10.14.4 18E226, locale it-IT) [✓] Android toolchain — develop for Android devices (Android SDK version 28.0.3) [✓] iOS toolchain — develop for iOS devices (Xcode 10.2.1) [✓] Android Studio (version 3.4) [✓] VS Code (version 1.34.0) [!] Connected device ! No devices available
Successivamente, abbiamo bisogno di aggiugere la directory .pub-cache/bin (relativa al nostro percorso di installazione dell’sdk di Flutter) al nostro PATH. Sulla mia macchina, ho inserito la seguente riga all’interno del mio file .zshrc (per quelli che se lo stanno domandando: sì, sono un fiero utilizzatore di Zsh!)
PATH=$PATH:/Users/mike/flutter/.pub-cache/bin
Dopo aver effettuato l’update del nostro path, assicuriamoci di chiudere e riaprire la console/il prompt dei comandi al fine di rendere le modifiche effettive
Siamo pronti per attivare ed utilizzare webdev, la controparte di Flutter For Web del comando flutter! Attiviamolo con:
flutter packages pub global activate webdev
Infine, testiamo il nuovo pargoletto:
~> webdev A tool to develop Dart web projects. Usage: webdev <command> [arguments] Global options: -h, --help Print this usage information. --version Prints the version of webdev. Available commands: build Run builders to build a package. help Display help information for webdev. serve Run a local web development server and a file system watcher that rebuilds on changes. Run "webdev help <command>" for more information about a command. ~>
2. Effettuare l’Install/Upgrade delle estensioni per la propria IDE
Dovremmo assicurarci di utilizzare l’ultima versione delle estensioni per la propria IDE. Personalmente, amo Visual Studio Code, lo uso praticamente per tutto, quindi nel mio caso, mi assicuro di utilizzare almeno la versione v3.0 di Flutter Plugin per VSCode.
3. Creare un nuovo progetto
I motori sono caldi! Possiamo ora creare un nuovo progetto Flutter For Web, direttamente da VSCode, premendo Cmd+Shift+P (oppure Ctrl+Shift+P su Windows) e selezionando Flutter: New Web Project dal menu.
Seguendo le istruzioni, possiamo scegliere il nome e la location del nostro progetto.
4. Configurare il nuovo progetto
Sebbene la struttura di un progetto Flutter For Web ricalchi quella di un progetto Flutter Mobile, ci sono alcune piccole differenze. Comunque, la struttura può essere riassunta così:
- la cartella web contiene la struttura html del nostro progetto (index.html ecc.)
- la cartella lib contiene il file main.dart e il nostro codice principale, come su mobile
- il file analysis_options.yaml definisce le opzioni di configurazione del linter
- il file pubspec.yaml definisce, come su mobile, le dipendenze da utilizzare
Perciò, come su mobile, si spenderà la maggior parte del tempo nella cartella lib.
In questo progetto, utilizzeremo un font customizzato (TitilliumWeb, un Google Font). Per aggiungere font customizzati in un progetto Flutter For Web, si deve:
- creare la directory assets sotto la cartella web
- mettere i propri font dentro la suddetta cartella, ad esempio sotto assets/fonts/
- dichiarare i fonts nel file assets/FontManifest.json
Il mio FontManifest.json appare come di seguito:
[ { “family”: “Titillium Web”, “fonts”: [ { “asset”: “fonts/TitilliumWeb-Regular.ttf” } ] } ]
Utilizzeremo per il nostro progetto anche il package HTTP che includeremo come dipendenza dentro il file pubspec.yaml
La maggior parte dei package standard non funzionerà ancora con Flutter For Web. Questo package sembra funzionare. Il mio suggerimento è quello di utilizzare le librerie native di Dart fino a quando Flutter For Web non verrà rilasciato come Stable. Si può consultare la Dart API Reference per maggiori informazioni.
Adesso siamo davvero pronti a sporcarci le mani con il codice. Iniziamo!
5. Scrivere la business logic
Il gioco è semplice, ma dimostra come sia possibile implementare la nostra business logic, proprio come se fossimo su un progetto Flutter Mobile.
Leggeremo un URL da una TextField e controlleremo se il server che è (eventualmente) in listen su quell’URL accetta (o meno) richieste cross-site su chiamata XMLHttpRequest, da chiunque.
Questa è un’implementazione basilare e piuttosto incompleta della nostra business logic: dovremmo effettuare dei controlli più complessi piuttosto che un semplice controllo sullo statusCode della response. Ma, per questo esempio (e per le mie necessità), assumeremo che se il server ritorna uno statusCode nella sua response, allora significa che sarà in grado di accettare richieste cross-origin da chiunque.
Non utilizzeremo pattern per la gestione dello state (come BLoC et similia), per questo esempio metteremo l’intera “bistecca” (chiedo scusa, ma è ora di cena!) in lib/main.dart, ma si è liberi di sperimentare ed arricchire il codice, come esercizio.
Prima di tutto, abbiamo bisogno effettuare alcuni import. Utilizzeremo la libreria dart:html al fine di usufruire del motore di sintesi del browser, come ciliegina sulla torta, che ci comunicherà a voce il risultato del nostro check
import ‘package:flutter_web/material.dart’; import ‘package:http/http.dart’ as http; import ‘dart:html’;
Fatto ciò, ordinaria amministrazione come su mobile: richiamiamo la funzione main() che a sua volta chiama la funzione runApp(), alla quale passiamo uno StatelessWidget (un’instanza della classe MyApp) che a finalmente instanzia un Widget di tipo MaterialApp. Setteremo anche il titolo ed il tema della nostra Web App, inoltre imposteremo il font di default, che sarà TitilliumWeb.
MyHomePage estende la classe StatefulWidget, quindi dobbiamo curarci di dichiarare ed inizializzare il nostro state.
Utilizziamo una property di tipo TextEditingController per catturare ciò che l’utente scrive dentro la TextField, inoltre dichiariamo alcune variabili booleane che saranno utilizzate per validare il nostro input e per mostrare un progress indicator fintanto che c’è attività di comunicazione tramite la rete. Infine, dichiariamo anche una property di tipo String che servirà a visualizzare (e far pronunciare dal browser) il risultato del check all’utente. Effettuando l’override della funzione initState() siamo in grado di inizializzare lo state.
class MyHomePage extends StatefulWidget { @override State<MyHomePage> createState() { return _MyHomePage(); } } class _MyHomePage extends State<MyHomePage> { final _myController = TextEditingController(); bool _canVerify; bool _isBusy; String _message; @override void initState() { _canVerify = false; _isBusy = false; _message = ''; super.initState(); }
Non dobbiamo dimenticarci di fare pulizia: quano non abbiamo più bisogno del nostro TextEditingController, sarà il momento giusto per sbarazzarci di esso.
@override void dispose() { _myController.dispose(); super.dispose(); }
Passiamo ora a definire la nostra funzione di validazione dell’input, che sarà passata ed invocata dalla property onChanged. Analizziamo la nostra stringa di input e la confrontiamo con una RegExp, per verificare che la stringa sia un URL valido. Se è così, abiliteremo il pulsante Verify, altrimenti il pulsante rimarrà disabilitato.
_handleOnChanged(String value) { const urlPattern = r"(https?|http)://([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:,.;]*)?"; final match = RegExp(urlPattern, caseSensitive: false).firstMatch(value); setState(() { _canVerify = match != null; }); }
È tempo di scrivere il codice relativo alla funzione che effettua la request e controlla se il server remoto (qualora vi sia un server in ascolto) accetti richieste CORS. Utilizzeremo il client HTTP per effettuare una richiesta di tipo get. Se otterremo uno statusCode nella response, il risultato del check avrà esito Positivo. Altrimenti, se non otterremo uno statusCode oppure si verificherà un errore durante la request, il risultato del check sarà Negativo.
Adotteremo un approccio di tipo then/catchError per effettuare lo spacchettamento dell’oggetto Future<Response>. Preferisco utilizzare questo approccio piuttosto che async/await, ma ovviamente: a voi la scelta.
Settando _isBusy a true permetterà alla funzione build() di MyHomePage di disegnare un indicatore di progresso circolare finché il server non ritornerà una Response (o, sfortunatamente, un errore).
A questo punto, al ritorno della request, sia nel caso di una response o di un errore, settiamo subito _isBusy a false (poiché non c’è più comunicazione attraverso la rete) e creiamo un’istanza della classe SpeechSynthesisUtterance, la sua speed rate e la lingua. Utilizzeremo dopo quest’istanza, per comunicare all’utente verbalmente, attraverso il motore di sintesi del browser, l’esito del check. Questo dovrebbe funzionare sulla maggior parte dei browsers attualmente. Questa classe è fornita dalla libreria dart:html.
Analizziamo il body della nostra response: se è ‘Not Found’, molto probabilmente abbiamo inserito un URL/Endpoint non valido. Quindi, settiamo di conseguenza la property _message.
Altrimenti, se response.statusCode è diverso da null, signore e signori: Abbiamo un vincitore! Molto probabilmente il server remoto accetta request CORS da chiunque.
Infine, se lo statusCode è null, molto probabilmente il server rifiuta la nostra request.
Alla fine del processo, non dimentichiamoci di invocare setState(): abbiamo modificato lo state negli step precedenti, quindi dobbiamo rebuildare MyHomePage() in funzione di esso.
Si possono wrappare i cambiamenti di stato in una chiamata a setState(), oppure possiamo chiamare setState() alla fine del processo. In questo caso, chiamare setState() fuori il blocco if/else risulta molto più elegante che effettuare chiamate multiple a setState() dentro il blocco if/else. Ma, ricordate: se decidete di utilizzare questo approccio, prestate attenzione al *quando* richiamare setState(), altrimenti potreste ottenere risultati inaspettati!
Di nuovo, settiamo _message. Infine, facciamo pronunciare la nostra Utterance che abbiamo creato prima, al browser. Il testo della Utterance sarà quello contenuto nella variabile _message.
Se, malauguratamente, dovessimo ottenere un errore come risultato della request, molto probabilmente il server non accetta request CORS da chiunque (per quanto detto prima, questa è una business logic davvero minimale e non completa, ma per ora ci va bene così).
_verifyEndpoint() { final URL = _myController.text; setState(() { _isBusy = true; }); http.get(URL).then((response) { final u = SpeechSynthesisUtterance(); u.lang = 'en-US'; u.rate = 1.0; _isBusy = false; print(response.body); if (response.body == 'Not Found') { _message = "Sorry: can't resolve this endpoint"; } else { _message = response.statusCode != null ? 'This server seems to allow cross origin requests' : 'This server seems to deny cross origin requests'; } setState(() {}); u.text = _message; window.speechSynthesis.speak(u); }).catchError((error) { final u = SpeechSynthesisUtterance(); u.lang = 'en-US'; u.rate = 1.0; setState(() { _isBusy = false; _message = "This server seems to deny cross origin requests"; }); u.text = _message; window.speechSynthesis.speak(u); }); }
Bene, adesso abbiamo a disposizione tutti i pezzi del puzzle che ci servono per assemblare MyHomePage(). Questo è il metodo build(), dove utilizziamo un layout piuttosto standard con delle colonne, centrate. La cosa interessante è il Colore che scegliamo per il messaggio di feedback: se il risultato del check è OK, esso sarà Verde. Altrimenti, sarà Rosso.
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('CORS Detector - Made in Flutter For Web'), centerTitle: true, ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: const EdgeInsets.all(20.0), child: Text( '', style: TextStyle(fontSize: 24.0), ), ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Expanded( child: Padding( padding: const EdgeInsets.all(20.0), child: TextField( onChanged: (value) => _handleOnChanged(value), controller: _myController, autofocus: true, decoration: const InputDecoration( helperText: "Enter a valid http(s) URL", hintText: "Ex.: https://server.to.test", helperStyle: TextStyle( fontSize: 18.0, ), hintStyle: TextStyle( fontStyle: FontStyle.italic, color: Colors.white24, ), ), ), ), ), Padding( padding: const EdgeInsets.all(20.0), child: !_isBusy ? RaisedButton( color: Theme.of(context).accentColor, textColor: Colors.black, child: Text('VERIFY'), onPressed: _canVerify ? () => _verifyEndpoint() : null, ) : CircularProgressIndicator(), ) ], ), ), Flexible( child: Text( _message, style: TextStyle( fontSize: 22.0, color: _message.contains('allow') ? Colors.green : Colors.red, ), ), ), ], ), ), ); }
A questo punto, possiamo lanciare il web server di sviluppo, eseguendo questo comando nella root folder del nostro progetto:
webdev serve
Alla fine del processo, il server di sviluppo sarà in listen su http://localhost:8080. Apriamo una finestra del browser e verifichiamo:
6. Andare in produzione
Quando siamo pronti per andare in produzione, possiamo semplicemente dare il seguente comando all’interno della root folder del nostro progetto:
webdev build
Flutter creerà la directory build, che conterrà il codice minifizzato e pronto per essere servito da qualsiasi web server. Possiamo utilizzare ad esempio il protocollo FTP per connetterci al nostro hosting provider preferito ed effettuare l’upload dei files nella document root del nostro web server.
Ed il gioco è fatto!
Conclusioni
Sono davvero soddisfatto da questa Technical Preview di Flutter For Web. È stato davvero semplice e divertente sviluppare una semplice (ma ricca di concetti cardine) web app come quella di questo articolo. Personalmente, penso che Flutter For Web sia davvero promettente. Beh, sicuramente è ancora lontano dal poter essere definito Ready for production, ma.. ehy: è solamente una Technical Preview! E devo dire che i ragazzi di Big G stanno facendo un ottimo lavoro.
Il codice della Web App CORS Detector è disponibile per il download sul mio spazio Github.