Risolvere problemi comuni multipiattaforma quando si lavora con Flutter

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Quando si utilizzano framework multipiattaforma, le persone potrebbero dimenticare le sfumature di ciascuna delle piattaforme su cui vogliono che il loro codice venga eseguito. Questo articolo ha lo scopo di affrontarlo.

Ho visto molta confusione online riguardo allo sviluppo Web con Flutter e, spesso, è purtroppo per le ragioni sbagliate.

In particolare, le persone a volte lo confondono con i vecchi framework multipiattaforma mobili (e desktop) basati sul Web, che fondamentalmente erano solo pagine Web in esecuzione all'interno di browser in esecuzione all'interno di un'app wrapper.

Era davvero multipiattaforma, nel senso che le interfacce erano comunque le stesse perché avevi accesso solo alle interfacce normalmente accessibili sul Web.

Flutter non è questo, però: funziona in modo nativo su ogni piattaforma e significa che ogni app funziona esattamente come se fosse scritta in Java/Kotlin o Objective-C/Swift su Android e iOS, più o meno. Devi saperlo perché ciò implica che devi prenderti cura delle molte differenze tra queste piattaforme molto diverse.

In questo articolo, vedremo alcune di queste differenze e come superarle. Più specificamente, parleremo delle differenze di archiviazione e interfaccia utente, che sono quelle che più spesso creano confusione agli sviluppatori quando scrivono codice Flutter che vogliono essere multipiattaforma.

Altro dopo il salto! Continua a leggere sotto ↓

Esempio 1: Stoccaggio

Di recente ho scritto sul mio blog sulla necessità di un approccio diverso alla memorizzazione di JWT nelle app Web rispetto alle app mobili.

Ciò è dovuto alla diversa natura delle opzioni di archiviazione delle piattaforme e alla necessità di conoscere ciascuno dei loro strumenti di sviluppo nativi.

ragnatela

Quando scrivi un'app Web, le opzioni di archiviazione disponibili sono:

  1. scaricare/caricare file su/da disco, che richiede l'interazione dell'utente ed è quindi adatto solo per file destinati ad essere letti o creati dall'utente;
  2. l'utilizzo di cookie, che possono essere o meno accessibili da JS (a seconda che siano o meno httpOnly ) e vengono inviati automaticamente insieme alle richieste a un determinato dominio e salvati quando arrivano come parte di una risposta;
  3. utilizzando JS localStorage e sessionStorage , accessibili da qualsiasi JS sul sito Web, ma solo da JS che fa parte delle pagine di quel sito Web.

Mobile

La situazione quando si tratta di app mobili è completamente diversa. Le opzioni di archiviazione sono le seguenti:

  1. documenti dell'app locale o archiviazione della cache, accessibile da tale app;
  2. altri percorsi di archiviazione locali per file leggibili/creati dall'utente;
  3. NSUserDefaults e SharedPreferences rispettivamente su iOS e Android per l'archiviazione dei valori-chiave;
  4. Keychain su iOS e KeyStore su Android per l'archiviazione sicura, rispettivamente, di eventuali dati e chiavi crittografiche.

Se non lo sai, rovinerai le tue implementazioni perché devi sapere quale soluzione di archiviazione stai effettivamente utilizzando e quali sono i vantaggi e gli svantaggi.

Soluzioni multipiattaforma: un approccio iniziale

L'uso del pacchetto shared_preferences di Flutter utilizza localStorage sul Web, SharedPreferences su Android e NSUserDefaults su iOS. Questi hanno implicazioni completamente diverse per la tua app, soprattutto se stai archiviando informazioni sensibili come i token di sessione: localStorage può essere letto dal client, quindi è un problema se sei vulnerabile a XSS. Anche se le app mobili non sono realmente vulnerabili a XSS, SharedPreferences e NSUserDefaults non sono metodi di archiviazione sicuri perché possono essere compromessi sul lato client poiché non sono archiviazione sicura e non crittografata. Questo perché sono pensati per le preferenze dell'utente, come menzionato qui nel caso di iOS e qui nella documentazione di Android quando si parla della libreria di sicurezza che è progettata per fornire wrapper alle SharedPreferences specificamente per crittografare i dati prima di archiviarli.

Archiviazione sicura su dispositivi mobili

Le uniche soluzioni di archiviazione sicura sui dispositivi mobili sono Keychain e KeyStore su iOS e Android, mentre sul Web non è disponibile un'archiviazione sicura .

Tuttavia, il Keychain e KeyStore sono di natura molto diversa: il Keychain è una soluzione di archiviazione delle credenziali generica, mentre il KeyStore viene utilizzato per archiviare (e può generare) chiavi crittografiche, chiavi simmetriche o chiavi pubbliche/private.

Ciò significa che se, ad esempio, devi memorizzare un token di sessione, su iOS puoi lasciare che sia il sistema operativo a gestire la parte di crittografia e inviare semplicemente il tuo token al Keychain , mentre su Android è un po' più un'esperienza manuale perché è necessario per generare (non hard-code, non va bene) una chiave, usala per crittografare il token, archiviare il token crittografato in SharedPreferences e archiviare la chiave nel KeyStore .

Esistono diversi approcci a questo, come la maggior parte delle cose nella sicurezza, ma il più semplice è probabilmente utilizzare la crittografia simmetrica, poiché non è necessaria la crittografia a chiave pubblica poiché l'app crittografa e decrittografa il token.

Ovviamente, non è necessario scrivere codice specifico della piattaforma mobile che faccia tutto ciò, poiché esiste un plug-in Flutter che fa tutto ciò, ad esempio.

La mancanza di archiviazione sicura sul Web

Questo è stato, in realtà, il motivo che mi ha costretto a scrivere questo post. Ho scritto sull'utilizzo di quel pacchetto per archiviare JWT su app mobili e le persone volevano la versione Web di quello ma, come ho detto, non esiste uno spazio di archiviazione sicuro sul Web . Non esiste.

Significa che il tuo JWT deve essere allo scoperto?

No, per niente. Puoi usare httpOnly cookies, vero? Quelli non sono accessibili da JS e vengono inviati solo al tuo server. Il problema è che vengono sempre inviati al tuo server, anche se uno dei tuoi utenti fa clic su un URL di richiesta GET sul sito Web di qualcun altro e quella richiesta GET ha effetti collaterali che a te o al tuo utente non piaceranno. In realtà funziona anche per altri tipi di richiesta, è solo più complicato. Si chiama Cross-Site Request Forgery e tu non lo vuoi. È tra le minacce alla sicurezza web menzionate nei documenti MDN di Mozilla, dove puoi trovare una spiegazione più completa.

Ci sono metodi di prevenzione. Il più comune è avere due token, in realtà: uno di questi arriva al client come cookie httpOnly , l'altro come parte della risposta. Quest'ultimo deve essere memorizzato in localStorage e non nei cookie perché non vogliamo che venga inviato automaticamente al server.

Risolvere entrambi

Cosa succede se hai sia un'app mobile che un'app Web?

Questo può essere affrontato in due modi:

  1. Utilizza lo stesso endpoint di back-end, ma ottieni e invia manualmente i cookie utilizzando le intestazioni HTTP relative ai cookie;
  2. Creare un endpoint back-end non Web separato che generi un token diverso da uno dei token utilizzati dall'app Web e quindi consentire l'autorizzazione JWT regolare se il client è in grado di fornire il token solo per dispositivi mobili.

Esecuzione di codice diverso su piattaforme diverse

Ora, vediamo come possiamo eseguire codice diverso su piattaforme diverse per poter compensare le differenze.

Creazione di un plug-in Flutter

Soprattutto per risolvere il problema dell'archiviazione, un modo per farlo è con un pacchetto di plug-in: i plug-in forniscono un'interfaccia Dart comune e possono eseguire codice diverso su piattaforme diverse, incluso il codice Kotlin/Java o Swift/Objective-C specifico della piattaforma nativa . Lo sviluppo di pacchetti e plugin è piuttosto complesso, ma è spiegato in molti punti del Web e altrove (ad esempio nei libri di Flutter), inclusa la documentazione ufficiale di Flutter.

Per le piattaforme mobili, ad esempio, esiste già un plug-in di archiviazione sicura, ed è flutter_secure_storage , per il quale puoi trovare un esempio di utilizzo qui, ma che non funziona sul Web, ad esempio.

D'altra parte, per una semplice archiviazione chiave-valore che funziona anche sul Web, esiste un pacchetto di plug-in proprietario sviluppato da Google multipiattaforma chiamato shared_preferences , che ha un componente specifico per il Web chiamato shared_preferences_web che utilizza NSUserDefaults , SharedPreferences o localStorage a seconda della piattaforma.

TargetPlatform su Flutter

Dopo aver importato package:flutter/foundation.dart , puoi confrontare Theme.of(context).platform con i valori:

  • TargetPlatform.android
  • TargetPlatform.iOS
  • TargetPlatform.linux
  • TargetPlatform.windows
  • TargetPlatform.macOS
  • TargetPlatform.fuchsia

e scrivi le tue funzioni in modo che, per ogni piattaforma che vuoi supportare, facciano la cosa appropriata. Ciò sarà particolarmente utile per il prossimo esempio di differenza di piattaforma, ovvero le differenze nel modo in cui i widget vengono visualizzati su piattaforme diverse.

Per quel caso d'uso, in particolare, esiste anche un plugin flutter_platform_widgets ragionevolmente popolare, che semplifica lo sviluppo di widget platform-aware.

Esempio 2: differenze nella modalità di visualizzazione dello stesso widget

Non puoi semplicemente scrivere codice multipiattaforma e fingere che un browser, un telefono, un computer e uno smartwatch siano la stessa cosa, a meno che tu non voglia che la tua app per Android e iOS sia una visualizzazione Web e che la tua app desktop sia creata con Electron . Ci sono molte ragioni per non farlo, e non è lo scopo di questo pezzo convincerti a utilizzare framework come Flutter invece che mantengono la tua app nativa, con tutti i vantaggi in termini di prestazioni ed esperienza utente che ne derivano, pur consentendoti di scrivere codice che sarà lo stesso per tutte le piattaforme per la maggior parte del tempo.

Ciò richiede cura e attenzione, tuttavia, e almeno una conoscenza di base delle piattaforme che desideri supportare, delle loro effettive API native e tutto il resto. Gli utenti React Native devono prestare ancora più attenzione a questo perché quel framework utilizza i widget del sistema operativo integrati, quindi in realtà è necessario prestare ancora più attenzione all'aspetto dell'app testandola ampiamente su entrambe le piattaforme, senza poter passare da una all'altra iOS e widget al volo come è possibile con Flutter.

Cosa cambia senza la tua richiesta

Ci sono alcuni aspetti dell'interfaccia utente della tua app che vengono modificati automaticamente quando cambi piattaforma. Questa sezione menziona anche cosa cambia tra Flutter e React Native a questo riguardo.

Tra Android e iOS (Flutter)

Flutter è in grado di eseguire il rendering di widget Material su iOS (e widget Cupertino (simile a iOS) su Android), ma ciò che NON fa è mostrare esattamente la stessa cosa su Android e iOS: i temi dei materiali si adattano in particolare alle convenzioni di ciascuna piattaforma .

Ad esempio, le animazioni e le transizioni di navigazione e i caratteri predefiniti sono diversi, ma non influiscono molto sulla tua app.

Ciò che può influenzare alcune delle tue scelte quando si tratta di estetica o UX è il fatto che cambiano anche alcuni elementi statici. Nello specifico alcune icone cambiano tra le due piattaforme, i titoli della barra delle app sono al centro su iOS e a sinistra su Android (a sinistra dello spazio disponibile nel caso sia presente il pulsante Indietro o il pulsante per aprire un Drawer (spiegato qui nelle linee guida Material Design e noto anche come menu hamburger). Ecco come appare un'app Material con un cassetto su Android:

immagine di un'app Android che mostra dove appare il titolo della barra dell'app nelle app Flutter Android Material
App materiale in esecuzione su Android: il titolo dell'AppBar si trova nella parte sinistra dello spazio disponibile. (Grande anteprima)

E come appare la stessa, semplicissima app Material su iOS:

immagine di un'app iOS che mostra dove appare il titolo della barra dell'app nelle app Flutter iOS Material
App materiale in esecuzione su iOS: il titolo AppBar è nel mezzo. (Grande anteprima)

Tra cellulare e Web e con notch dello schermo (flutter)

Sul Web c'è una situazione un po' diversa, come accennato anche in questo articolo Smashing sul Responsive Web Development con Flutter: in particolare, oltre a dover ottimizzare per schermi più grandi e tenere conto del modo in cui le persone si aspettano di navigare nel tuo sito - che è l'obiettivo principale di quell'articolo - devi preoccuparti del fatto che a volte i widget vengono posizionati al di fuori della finestra del browser. Inoltre, alcuni telefoni hanno delle tacche nella parte superiore dello schermo o altri impedimenti alla corretta visualizzazione dell'app a causa di una sorta di ostruzione.

Entrambi questi problemi possono essere evitati avvolgendo i tuoi widget in un widget SafeArea , che è un particolare tipo di widget di riempimento che assicura che i tuoi widget cadano in un luogo in cui possono essere effettivamente visualizzati senza che nulla ostacoli la capacità degli utenti di vederli, sia esso un vincolo hardware o software.

In Reagire Nativo

React Native richiede molta più attenzione e una conoscenza molto più approfondita di ciascuna piattaforma, oltre a richiedere di eseguire almeno il Simulatore iOS e l'emulatore Android per poter testare la tua app su entrambe le piattaforme: non è lo stesso e converte i suoi elementi dell'interfaccia utente JavaScript in widget specifici della piattaforma. In altre parole, le tue app React Native assomiglieranno sempre a iOS - con gli elementi dell'interfaccia utente di Cupertino come vengono talvolta chiamati - e le tue app Android sembreranno sempre normali app Android Material Design perché utilizza i widget della piattaforma.

La differenza qui è che Flutter esegue il rendering dei suoi widget con il proprio motore di rendering di basso livello, il che significa che puoi testare entrambe le versioni dell'app su un'unica piattaforma.

Come aggirare quel problema

A meno che tu non stia cercando qualcosa di molto specifico, la tua app dovrebbe avere un aspetto diverso su piattaforme diverse, altrimenti alcuni dei tuoi utenti saranno insoddisfatti.

Proprio come non dovresti semplicemente spedire un'app mobile sul web (come ho scritto nel suddetto post Smashing), non dovresti spedire un'app piena di widget Cupertino agli utenti Android, ad esempio, perché sarà fonte di confusione per la maggior parte. D'altra parte, avere la possibilità di eseguire effettivamente un'app che ha widget destinati a un'altra piattaforma ti consente di testare l'app e mostrarla alle persone in entrambe le versioni senza dover necessariamente utilizzare due dispositivi per questo.

L'altro lato: usare i widget sbagliati per le giuste ragioni

Ma ciò significa anche che puoi eseguire la maggior parte del tuo sviluppo Flutter su una workstation Linux o Windows senza sacrificare l'esperienza dei tuoi utenti iOS, quindi puoi semplicemente creare l'app per l'altra piattaforma e non devi preoccuparti di testarla a fondo.

Prossimi passi

I framework multipiattaforma sono fantastici, ma trasferiscono la responsabilità a te, lo sviluppatore, di capire come funziona ciascuna piattaforma e come assicurarti che la tua app si adatti e sia piacevole da usare per i tuoi utenti. Altre piccole cose da considerare potrebbero essere, ad esempio, l'utilizzo di descrizioni diverse per ciò che potrebbe essere essenzialmente la stessa cosa se ci sono convenzioni diverse su piattaforme diverse.

È fantastico non dover creare le due (o più) app separatamente utilizzando lingue diverse, ma devi comunque tenere a mente che, in sostanza, stai creando più di un'app e ciò richiede di pensare a ciascuna delle app che stai creando .

Ulteriori risorse

  • Il sito Web Flutter Gallery e l'app Android, che mostra l'uso dei widget Flutter tipici di diverse piattaforme e il loro agnosticismo della piattaforma
  • Documentazione API Flutter su TargetPlatform
  • Documentazione Flutter sulla creazione di pacchetti e plugin
  • Documentazione Flutter sugli adattamenti della piattaforma
  • Documentazione MDN sui cookie