Sviluppare un’applicazione Flutter – Parte 2

Questo è il secondo di una serie di articoli dove realizzeremo passo passo un’applicazione mobile multipiattaforma, Android ed iOS.

Nel precedente articolo abbiamo installato e configurato tutti gli strumenti necessari di sviluppo, imbastito il progetto iniziale e creato la prima pagina.

In questo andremo ad aggiungere il supporto multilingua e realizzeremo un provider (così chiamati in gergo) per interagire con uno strato API web.

Vi consiglio la lettura della prima parte nel caso non l’aveste già fatto.

Supporto Multilingua

Vediamo ora come rendere la nostra applicazione multilingua. Per fare ciò ci viene in soccorso un’estensione di Visual Studio Code, Flutter Intl, installiamola.

Apriamo VSCode e rechiamoci alla sezione delle estensioni aggiuntive, dal pannello laterale sinistro. Cerchiamo, scrivendo nella barra di ricerca, l’estensione Flutter Intl ed installiamola.

Terminata l’installazione ed aperto il nostro progetto su VSCode, dalla Command Palette… (Ctrl+Shift+P) cerchiamo e lanciamo il comando Flutter Intl: Initialize.

Questo comando automatizza diverse operazioni, vediamole elencate di seguito.

Abilita flutter_intl all’interno del file di configurazione di progetto pubspec.yaml:

flutter_intl:
  enabled: true

Crea una cartella chiamata l10n all’interno della directory lib (che come abbiamo visto nel precedente articolo, contiene la logica dell’applicazione).

All’interno della cartella l10n, genera il file intl_en.arb (inizialmente vuoto):

{
}

Per ogni lingua supportata dall’applicazione è necessario creare un rispettivo file .arb con il codice ISO 639-1 che identifica la lingua appunto.

Per aggiungere una nuova lingua in maniera del tutto automatizzata si può utilizzare il comando Flutter Intl: Add locale messo a disposizione dal componente aggiuntivo che abbiamo appena installato.

Questi file .arb possono essere considerati ad esempio come file .json non nidificati (non sono supportati oggetti nidificati all’interno dei file di lingua l10n su Flutter).

Compiliamo il file intl_en.arb come segue:

{
    "loginTitle": "Login",
    "loginButton": "Log In"
}

Per comodità, non potendo utilizzare oggetti nidificati, il prefisso di ogni chiave indica la pagina in cui è utilizzata quella chiave, in questo caso login.

Ogni volta che salviamo il contenuto di un file lingua, in automatico l’estensione Flutter Intl rigenererà il componente Dart l10n.dart all’interno della cartella generated (sempre all’interno della directory lib).

Questo componente autogenerato ci servirà per utilizzare le traduzioni all’interno dell’applicazione, come vedremo successivamente.

Prima di poter utilizzare le due nuove chiavi che abbiamo creato, abbiamo bisogno di un paio di passaggi manuali per completare la configurazione multilingua.

Apriamo il file pubspec.yaml e alla sezione dependencies aggiungiamo la nuova dipendenza flutter_localizations come sotto:

dependencies:
    // Altre dipendenze...
    flutter_localizations:
        sdk: flutter

Al salvataggio verrà eseguito in automatico da VSCode il comando flutter pub get per l’installazione dei nuovi pacchetti aggiunti.

Apriamo ora il file main.dart ed importiamo il nuovo componente autogenerato l10n.dart e la dipendenza appena installata flutter_localizations:

import 'package:flutter_localizations/flutter_localizations.dart';
import 'generated/l10n.dart';

Impostiamo poi, all’interno della nostra istanza MaterialApp, l’array localizationsDelegates e la proprietà supportedLocales:

return MaterialApp(
      localizationsDelegates: const [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: const LoginPage(),
    );

Ora siamo pronti ad utilizzare le chiavi di traduzione.

Apriamo il file login.dart ed all’interno dell’oggetto AppBar cambiamo il titolo stringa “Login” con la nuova chiave di traduzione:

return Scaffold(
  appBar: AppBar(
    title: Text(S.current.loginTitle),
  ),
  ...

importiamo il componente autogenerato l10n.dart:

import '../generated/l10n.dart';

Facciamo la stessa cosa con l’etichetta del pulsante ElevatedButton di accesso:

ElevatedButton(
  onPressed: () {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    _formKey.currentState!.save();

    print(_email);
    print(_password);
  },
  child: SizedBox(
    width: 200,
    height: 50,
    child: Center(
      child: Text(
        S.current.loginButton,
        textScaleFactor: 1.5,
      ),
    ),
  ),
)

Selezionamo il nostro dispositivo Android (o l’emulatore) per il deploy e successivamente avviamo l’applicazione, menu Run, Start Debugging o più semplicemente il tasto F5 sulla tastiera.

Il pulsante ed il titolo della pagina ora utilizzano le chiavi di traduzione

Integrazione API Web

Iniziamo ora a vedere un metodo per poter integrare l’applicazione con uno strato API web di esempio.

Nota: per implementare uno strato API web di test in modo rapido è possibile utilizzare lo strumento online https://beeceptor.com/.

Consideriamo quindi di dover comunicare con un API che espone i seguenti metodi:

POST …/api/auth

Input (Oggetto JSON)
{
    "email": "demo@cloudsurfers.it",
    "password": "passworddemo"
}
Output (Oggetto JSON)
{
    "token": "TOKEN_DEMO"
}

GET …/api/items

{
    ...
    "token": "TOKEN_DEMO",
    ...
}
Output (Oggetto JSON)
[
    {
        "id": 0,
        "name": "Item 1",
        "description": "Blue item"
    },
    {
        "id": 1,
        "name": "Item 2",
        "description": "Green item"
    }
]

La chiamata items è una chiamata autenticata, necessita quindi del token di accesso all’interno dell’header della richiesta.

I modelli

Per interagire con le API abbiamo bisogno di implementare tre nuovi oggetti all’interno dell’applicazione: AuthRequest, AuthResponse ed ItemResponse. Questi tre oggetti dovranno: essere generati partendo da un file .json, oppure generare a loro volta un file .json (per il modello AuthRequest per esempio).

Creiamo quindi una nuova cartella models all’interno della cartella libs.

All’interno di models creiamo tre file vuoti: auth_request.dart, auth_response.dart ed item_response.dart.

Apriamo il file auth_request.dart e compiliamolo come segue:

class AuthRequest {
  String? email;
  String? password;

  AuthRequest({
    this.email,
    this.password,
  });

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = <String, dynamic>{};
    data["email"] = email;
    data["password"] = email;
    return data;
  }
}

come vediamo il metodo toJson() trasforma l’oggetto AuthRequest in oggetto JSON.

Compiliamo ora il file auth_response.dart:

class AuthResponse {
  String? token;

  AuthResponse({
    this.token,
  });

  AuthResponse.fromJson(Map<String, dynamic> json) {
    token = json["token"];
  }
}

a differenza del metodo prima, qui, il metodo fromJson(json) trasforma un oggetto JSON assegnando le proprietà dell’oggetto AuthResponse.

Terminiamo infine con il file item_response.dart molto simile al precedente:

class ItemResponse {
  int? id;
  String? name;
  String? description;

  ItemResponse({
    this.id,
    this.name,
    this.description,
  });

  ItemResponse.fromJson(Map<String, dynamic> json) {
    id = json["id"];
    name = json["name"];
    description = json["description"];
  }
}

I provider

Per una gestione pulita ed organizzata delle logiche dell’applicazione, che possono essere ad esempio: la gestione dell’autenticazione, la gestione di un database locale, ecc…, in Flutter, si usano i provider (Services per qui proviene dal mondo Angular).

I provider non sono altro che classi di oggetti che si occupano di eseguire determinate operazioni che possono essere invocate all’interno di tutta l’applicazione, ad esempio all’interno di tutte le pagine.

All’interno del nostro progetto, al momento, abbiamo bisogno di due provider:

  • auth_provider, si occupa della memorizzazione del token di autenticazione dopo che viene effettuato l’accesso tramite API web;
  • api_provider, conterrà i metodi per eseguire chiamate verso lo strato API web esterno.

Iniziamo quindi con lo sviluppo.

Creaiamo all’interno della cartella lib una cartella providers, ed, al suo interno, un file auth_provider.dart:

class AuthProvider {
  String? _token;

  setToken(String? token) {
    _token = token;
  }

  String? get token {
    return _token;
  }
}

Prima di proseguire con la creazione del provider per l’interazione con le API, abbiamo bisogno di installare nel progetto la dipendenza dio, un client HTTP per progetti Dart.

All’interno del file pubspec.yaml aggiungiamo quindi la dipendenza dio e salviamo successivamente il file per eseguire in automatico l’installazione del pacchetto:

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

  dio: ^4.0.6

  flutter_localizations:
        sdk: flutter

Creiamo ora, nella cartella providers, il file api_provider.dart:

import 'package:demo_application/models/auth_request.dart';
import 'package:demo_application/models/auth_response.dart';
import 'package:demo_application/models/item_response.dart';
import 'package:dio/dio.dart';

class ApiProvider {
  final Dio _dio = Dio();
  final String _url = 'http://api.demo/';

  ApiProvider();

  Future<AuthResponse?> auth(AuthRequest request) async {
    try {
      Response response = await _dio.post('$_url/api/auth', data: request);
      return AuthResponse.fromJson(response.data);
    } catch (error) {
      print(error);
      return null;
    }
  }

  Future<List<ItemResponse>?> items() async {
    try {
      Response response = await _dio.get('$_url/api/items');
      return List<ItemResponse>.from(
          response.data?.map((data) => ItemResponse.fromJson(data)));
    } catch (error) {
      print(error);
      return null;
    }
  }
}

Vediamo le operazioni più nel dettaglio:

final Dio _dio = Dio();
final String _url = 'http://api.demo';

All’interno della classe viene dichiarato ed istanziato il client HTTP dio utilizzando il pacchetto installato in precedenza.
Viene anche dichiarata ed assegnata la variabile stringa che contiene l’endpoint dello strato API web.

...
Future<AuthResponse?> auth(AuthRequest request) async {
    try {
      Response response = await _dio.post('$_url/api/auth', data: request);
      return AuthResponse.fromJson(response.data);
    } catch (error) {
      print(error);
      return null;
    }
}
...

Il metodo auth, così come il metodo items, è un metodo asincrono e ritorna l’oggetto AuthResponse contenente il token di autenticazione se la chiamata HTTP ottiene uno status 200. Oppure ritorna un oggetto NULL nel caso di errore.

Per poter utilizzare i due nuovi provider che abbiamo creato, non ci resta che instanziarli e richiamare i metodi implementati.

E invece no…

Così facendo, se dovessimo utilizzare i provider su più pagine della nostra applicazione, dovremo istanzarli più volte, causando inefficienze e possibili errori di concorrenza all’interno dell’applicazione.

Vediamo quindi come fare…

Utilizziamo get_it

get_it è un pacchetto Dart che permette di istanziare un’oggetto una sola volta nel processo di avvio dell’applicazione e richiamare successivamente la stessa istanza ovunque vogliamo.

Come al solito come procedura d’installazione di nuovi pacchetti, aggiungiamo la dipendenza get_it all’interno del file di progetto pubspec.yaml e salviamo:

dio: ^4.0.6
get_it: ^7.2.0

Apriamo il file main.dart ed importiamo il nuovo pacchetto:

import 'package:get_it/get_it.dart';

All’interno del metodo main() registriamo i due provider creati:

void main() {
  GetIt.I.registerSingleton<AuthProvider>(AuthProvider());
  GetIt.I.registerSingleton<ApiProvider>(ApiProvider());

  runApp(const MyApp());
}

I due provider ora verranno istanziati all’avvio e saranno disponibili in tutta l’applicazione richiamandoli con una chiamata simile a questa: GetIt.I<ApiProvider>()....

Pagina di login

Ora che i provider sono implementati, torniamo alla pagina di login login.dart e più precisamente all’interno dell’evento onPressed dell’ElevatedButton che abbiamo realizzato nel precedente articolo.

Implementiamo la chiamata API di autenticazione:

...
onPressed: () {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    _formKey.currentState!.save();

    print(_email);
    print(_password);

    AuthRequest request = AuthRequest(
      email: _email,
      password: _password,
    );

    GetIt.I<ApiProvider>().auth(request).then((response) {
      print(response?.token);
    }).onError((error, stackTrace) {
      print(error);
    });
},
...

Eseguiamo l’applicazione con F5

come potete vedere, TOKEN_DEMO viene stampato correttamente come voluto all’interno della DEBUG CONSOLE.

Chiamate autenticate

A differenza della chiamata auth di autenticazione, che non richiede appunto autenticazione per essere eseguita per ovvi motivi, la chiamata items necessita all’interno dell’header della richiesta, il token di accesso.

Per fare questo possiamo utilizzare lo strumento Interceptor del client HTTP dio.

Prima però, una volta ottenuto il token di autenticazione, dobbiamo salvarlo all’interno dell’AuthProvider.

Sempre all’interno della pagina di login login.dart, evento onPressed dell’ElevatedButton, aggiungiamo il salvataggio del token, se ricevuto, all’interno dell’AuthProvider:

...
GetIt.I<ApiProvider>().auth(request).then((response) {
  print(response?.token);

  if (response != null && response.token != null) {
    GetIt.I<AuthProvider>().setToken(response.token);
  }
}).onError((error, stackTrace) {
  print(error);
});
...

Apriamo il file api_provider.dart ed all’interno della classe ApiProvider aggiungiamo tre nuovi metodi: requestInterceptor, responseInterceptor ed errorInterceptor:

...
dynamic requestInterceptor(RequestOptions options, RequestInterceptorHandler handler) async {
    String? token = GetIt.I<AuthProvider>().token;
    if (token != null) {
      options.headers.addAll({
        'token': token,
      });
    }
    handler.next(options);
}

dynamic responseInterceptor(Response response, ResponseInterceptorHandler handler) async {
    handler.next(response);
}

dynamic errorInterceptor(DioError error, ErrorInterceptorHandler handler) async {
    handler.next(error);
}
...

All’interno del metodo requestInterceptor viene iniettato il token, se presente, nell’header della richiesta HTTP intercettata.

I tre metodi appena creati, successivamente, vanno aggiunti all’istanza del client HTTP dio.

Creiamo quindi un nuovo metodo addnterceptors:

...
addInterceptors() {
    _dio.interceptors.add(InterceptorsWrapper(
        onRequest: (options, handler) => requestInterceptor(options, handler),
        onResponse: (response, handler) =>
            responseInterceptor(response, handler),
        onError: (dioError, handler) => errorInterceptor(dioError, handler)));
}
...

e richiamiamolo all’interno del costruttore ApiProvider:

...
ApiProvider() {
    addInterceptors();
}
...

Ora, una volta che abbiamo ottenuto il token, questo verrà inserito in tutte le chiamate HTTP effettuate tramite il client.

Per verificare se l’interceptor funziona correttamente, aggiungiamo un pulsante nuovo sotto al pulsante Login (all’interno della pagina di Login) che chiamiamo Elenco Oggetti. Facciamo si, inoltre, che il nuovo pulsante sia visibile solo se siamo autenticati, quindi il token è salvato in memoria.

Per fare questo, all’interno del file login.dart, aggiungiamo un nuovo metodo di tipo Getter, chiamato authenticated:

...
get authenticated {
    return GetIt.I<AuthProvider>().token != null;
}
...

Aggiungiamo anche un metodo itemsButton che ritorni un Widget contente il nuovo pulsante:

...
Widget itemsButton() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: () {
            GetIt.I<ApiProvider>().items().then((response) {
              print(response);
            }).onError((error, stackTrace) {
              print(error);
            });
          },
          child: SizedBox(
            width: 200,
            height: 50,
            child: Center(
              child: Text(
                S.current.loginItemsButton,
                textScaleFactor: 1.5,
              ),
            ),
          ),
        )
      ],
    );
}
...

L’evento onPressed: () {} del pulsante contiene la chiamata API autenticata items.

Infine non ci resta che aggiungere, sotto al pulsante Login, il nuovo metodo itemsButton(), condizionando la visibilità in base alla variabile (metodo di tipo Getter) authenticated:

...
Visibility(
  visible: authenticated,
  child: itemsButton(),
)
...

Il Widget Visibility ci permette di mostrare o meno un Widget figlio.

Provando ad eseguire l’applicazione ora, noteremo che nonostante si effettui l’accesso, e questo vada a buon fine, non venga mostrato il nuovo pulsante che abbiamo creato.

Questo succede poiché authenticated viene eseguito solo la prima volta, durante la creazione della pagina.

Per farlo rieseguire ottenendo il valore aggiornato della variabile token, dobbiamo utilizzare il metodo setState((){}):

...
setState(() {
  GetIt.I<AuthProvider>().setToken(response.token);
});
...

In questo modo verranno eseguite le operazioni presenti all’interno del metodo setState((){}) e verranno aggiornati gli stati delle variabili che hanno subito una modifica, nel nostro caso la variabile token di AuthProvider richiamata per l’appunto all’interno del metodo authenticated.

Eseguiamo l’applicazione con F5 ed effettuiamo l’accesso:

Come si può vedere, sempre che l’accesso vada a buon fine, comparirà il nuovo pulsante Elenco Oggetti.

Facendo uso ora dello strumento Dart DevTools visto nel primo articolo di questa serie, verifichiamo che, cliccando sul pulsante Elenco Oggetti, la chiamata API items venga effettuata con TOKEN_DEMO all’interno dell’header.

Apriamo i Dart DevTools all’interno del Browser Web.

Spostiamoci nella scheda Network e premiamo, sull’applicazione, il pulsante Elenco Oggetti:

All’interno dell’header della richiesta GET items, è presente TOKEN_DEMO come token. La chiamata autenticata quindi verrà eseguita correttamente.

Prossimamente…

Nel prossimo articolo vedremo come creare ed interagire con una base dati locale SQLite ed un accenno alla navigazione tra le pagine.

GitHub

Il progetto Flutter realizzato in questo articolo è disponibile su GitHub.

Fonti

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.