Separazione delle preoccupazioni nelle applicazioni Flutter
Engineering at ClickUp

Separazione delle preoccupazioni nelle applicazioni Flutter

Recentemente, ho dovuto implementare delle guide introduttive per i nuovi utenti di ClickUp! Si trattava di un'attività davvero importante, perché molti nuovi utenti stavano per scoprire la piattaforma grazie alla pubblicità incredibilmente divertente che abbiamo presentato in anteprima al Super Bowl! ✨

tramite ClickUp

La guida passo passo consente ai nostri numerosi nuovi utenti, che forse non conoscono ancora ClickUp, di comprendere rapidamente come utilizzare diverse funzioni dell'applicazione. Si tratta di un lavoro richiesto costante, proprio come la nuova risorsa ClickUp University che stiamo perseguendo! 🚀

Fortunatamente, l'architettura software alla base dell'applicazione mobile ClickUp Flutter mi ha permesso di implementare questa funzione abbastanza rapidamente, anche riutilizzando i widget reali dell'app! Ciò significa che la procedura guidata è dinamica, reattiva e corrisponde esattamente alle schermate reali dell'app, e continuerà ad esserlo anche quando i widget si evolveranno.

Sono anche riuscito a implementare la funzione grazie alla corretta separazione delle responsabilità.

Vediamo cosa intendo. 🤔

Separazione dei compiti

La progettazione di un'architettura software è uno degli argomenti più complessi per i team di ingegneri. Tra tutte le responsabilità, è sempre difficile anticipare le future evoluzioni del software. Ecco perché la creazione di un'architettura ben stratificata e disaccoppiata può aiutare te e i tuoi colleghi in molte cose!

Il vantaggio principale della creazione di piccoli sistemi disaccoppiati è senza dubbio la testabilità! Ed è proprio questo che mi ha aiutato a creare una demo alternativa delle schermate esistenti dell'app!

Una guida passo passo

Ora, come potremmo applicare questi principi a un'applicazione Flutter?

Condivideremo alcune tecniche che utilizziamo per sviluppare ClickUp con un semplice esempio guidato.

L'esempio è così semplice che potrebbe non chiarire tutti i vantaggi che si celano dietro, ma credimi, ti aiuterà a creare applicazioni Flutter molto più gestibili con codici complessi. 💡

L'applicazione

Ad esempio, creeremo un'applicazione che mostra la popolazione degli Stati Uniti per ogni anno.

tramite ClickUp

Qui abbiamo due schermate:

  • HomeScreen: presenta semplicemente un elenco degli anni dal 2000 ad oggi. Quando gli utenti toccano il riquadro di un anno, vengono reindirizzati alla schermata DetailScreen con un argomento di navigazione impostato sull'anno selezionato.
  • DetailScreen: ottiene l'anno dall'argomento di navigazione, chiama l'API datausa.io per quell'anno e analizza i dati JSON per estrarre il valore di popolazione associato. Se i dati sono disponibili, viene visualizzata un'etichetta con la popolazione.

Ci concentreremo sull'implementazione di DetailScreen poiché è la più interessante con la sua chiamata asincrona.

Passaggio 1. Approccio ingenuo

Approccio ingenuo Widget con stato
tramite ClickUp

L'implementazione più ovvia per la nostra app è quella che utilizza un unico StatefulWidget per l'intera logica.

Accesso all'argomento di navigazione annuale

Per accedere all'anno richiesto, leggiamo le RouteSettings dal widget ereditato ModalRoute.

Chiamata HTTP

Questo esempio richiama la funzione get dal pacchetto http per ottenere i dati dall'API datausa.io, analizza il risultato del JSON con il metodo jsonDecode dalla libreria dart:convert e mantiene Future come parte dello stato con una proprietà denominata _future.

Rendering

Per creare l'albero dei widget, utilizziamo un FutureBuilder, che si ricostruisce in base allo stato attuale della nostra chiamata asincrona _future.

Recensione

Ok, l'implementazione è breve e utilizza solo widget integrati, ma ora pensa al nostro intento iniziale: creare alternative demo (o test) per questa schermata. È molto difficile controllare il risultato della chiamata HTTP per forzare l'applicazione a eseguire il rendering in un determinato stato.

È qui che il concetto di inversione di controllo ci aiuterà. 🧐

Passaggio 2. Inversione del controllo

Inversione di controllo a Provider e api Client
tramite ClickUp

Questo principio può essere difficile da comprendere per i nuovi sviluppatori (e anche difficile da spiegare), ma l'idea generale è quella di estrarre le problematiche al di fuori dei nostri componenti, in modo che non siano responsabili della scelta del comportamento, e delegarle invece.

In una situazione più comune, consiste semplicemente nel creare astrazioni e inserire implementazioni nei nostri componenti in modo che la loro implementazione possa essere modificata in seguito, se necessario.

Ma non preoccuparti, tutto ti sarà più chiaro dopo il prossimo esempio! 👀

Creazione di un oggetto client API

Per controllare la chiamata HTTP alla nostra API, abbiamo isolato la nostra implementazione in una classe DataUsaApiClient dedicata. Abbiamo anche creato una classe Measure per rendere i dati più facili da manipolare e mantenere.

Fornire un client API

Per il nostro esempio, utilizziamo il noto pacchetto provider per inserire un'istanza DataUsaApiClient alla radice del nostro albero.

Utilizzo del client API

Il provider consente a qualsiasi widget discendente (come il nostro DetailScreen) di leggere il DataUsaApiClient superiore più vicino nell'albero. Possiamo quindi utilizzare il suo metodo getMeasure per avviare il nostro Future, al posto dell'effettiva implementazione HTTP.

Client API demo

Ora possiamo trarne vantaggio!

Nel caso non lo sapessi: tutte le classi in Dart definiscono implicitamente anche un'interfaccia associata. Questo ci permette di fornire un'implementazione alternativa di DataUsaApiClient che restituisce sempre la stessa istanza dalle chiamate al metodo getMeasure.

Questo metodo

Questo metodo

Visualizzazione di una pagina demo

Ora abbiamo tutte le chiavi per visualizzare un'istanza demo della DetailPage!

Sostituiamo semplicemente l'istanza DataUsaApiClient attualmente fornita racchiudendo il nostro DetailScreen in un provider che crea invece un'istanza DemoDataUsaApiClient!

E questo è tutto: il nostro DetailScreen legge invece questa istanza demo e utilizza i nostri dati demoMeasure invece di una chiamata HTTP.

Recensione

Questo è un ottimo esempio di inversione di controllo. Il nostro widget DetailScreen non è più responsabile della logica di acquisizione dei dati, ma la delega invece a un oggetto client dedicato. E ora siamo in grado di creare istanze demo della schermata o di implementare test dei widget per la nostra schermata! Fantastico! 👏

Ma possiamo fare ancora meglio!

Poiché non siamo in grado di simulare uno stato di caricamento, ad esempio, non abbiamo il pieno controllo di alcun cambiamento di stato a livello di widget.

Passaggio 3. Gestione dello stato

Gestione delle istruzioni nelle applicazioni Flutter
tramite ClickUp

Questo è un argomento molto dibattuto in Flutter!

Sono sicuro che hai già letto lunghe threads di persone che cercano di scegliere la migliore soluzione di gestione dello stato per Flutter. E, per essere chiari, non è quello che faremo in questo articolo. A nostro avviso, basta separare la logica di business dalla logica visiva e il gioco è fatto! La creazione di questi livelli è davvero importante per la manutenibilità. Il nostro esempio è semplice, ma nelle applicazioni reali la logica può diventare rapidamente complessa e tale separazione rende molto più facile trovare i tuoi algoritmi di logica pura. Questo argomento viene spesso riecheggiato come gestione dello stato.

In questo esempio, utilizziamo un ValueNotifier di base insieme a un Provider. Ma avremmo potuto anche utilizzare flutter_bloc o riverpod (o un'altra soluzione), e avrebbe funzionato altrettanto bene. I principi rimangono gli stessi e, purché si separino gli stati e la logica, è persino possibile trasferire il codice da una delle altre soluzioni.

Questa separazione ci aiuta anche a controllare qualsiasi stato dei nostri widget, in modo da poterlo simulare in ogni modo possibile!

Creazione di uno stato dedicato

Invece di affidarci all'AsyncSnapshot del framework, ora rappresentiamo lo stato della nostra schermata come un oggetto DetailState.

È anche importante implementare i metodi hashCode e operatore == per rendere il nostro oggetto comparabile in base al valore. Questo ci permette di rilevare se due istanze devono essere considerate diverse.

💡 I pacchetti equatable o freezed sono ottime opzioni per aiutarti a implementare i metodi hashCode e operatore ==!

💡 I pacchetti equatable o freezed sono ottime opzioni per aiutarti a implementare i metodi hashCode e operatore ==!

Il nostro stato è sempre associato a un anno, ma abbiamo anche quattro stati distinti possibili per quanto riguarda ciò che vogliamo mostrare all'utente:

  • NotLoadedDetailState: l'aggiornamento dei dati non è ancora iniziato
  • LoadingDetailState: i dati sono attualmente in fase di caricamento
  • LoadedDetailState: l'esito positivo del caricamento dei dati con una misura associata.
  • NoDataDetailState: i dati sono stati caricati, ma non ci sono dati disponibili.
  • UnknownErrorDetailState: l'operazione non è riuscita a causa di un errore sconosciuto.

Questi stati sono più chiari di un AsyncSnapshot, poiché rappresentano realmente i nostri casi d'uso. E, ancora una volta, questo rende il nostro codice più facile da mantenere!

💡 Consigliamo vivamente i tipi Union del pacchetto freezed per rappresentare i tuoi stati logici! Aggiunge molte utilità come i metodi copyWith o map.

💡 Consigliamo vivamente i tipi Union del pacchetto freezed per rappresentare i tuoi stati logici! Aggiunge molte utilità come i metodi copyWith o map.

Inserire la logica in un Notifier

Ora che abbiamo una rappresentazione del nostro stato, dobbiamo memorizzarne un'istanza da qualche parte: questo è lo scopo di DetailNotifier. Manterrà l'istanza corrente di DetailState nel suo valore e fornirà metodi per aggiornare lo stato.

Forniamo uno stato iniziale NotLoadedDetailState e un metodo di aggiornamento per caricare i dati dall'API e aggiornare il valore corrente.

Fornire uno stato per la vista

Per istanziare e osservare lo stato della nostra schermata, ci affidiamo anche al provider e al suo ChangeNotifierProvider. Questo tipo di provider cerca automaticamente qualsiasi ChangeListener creato e triggererà una ricostruzione dal Consumer ogni volta che viene notificato un cambiamento (quando il valore del nostro notificatore è diverso da quello precedente).

Recensione

Ottimo! L'architettura della nostra applicazione sta iniziando a prendere forma. Tutto è separato in livelli ben definiti, con funzioni specifiche! 🤗

Manca ancora un elemento per la testabilità: vogliamo definire l'attuale DetailState per controllare lo stato del nostro DetailScreen associato.

Passaggio 4. Widget di layout dedicato visivo

Widget di layout visivi dedicati nelle applicazioni Flutter
tramite ClickUp

Nell'ultimo passaggio, abbiamo dato un po' troppa responsabilità al nostro widget DetailScreen: era responsabile dell'istanziazione di DetailNotifier. E come abbiamo visto in precedenza, cerchiamo di evitare qualsiasi responsabilità logica nel livello di visualizzazione!

Possiamo risolvere facilmente questo problema creando un altro livello per il nostro widget dello schermo: divideremo il nostro widget DetailScreen in due:

  • DetailScreen è responsabile dell'impostazione delle varie dipendenze della nostra schermata dallo stato attuale dell'applicazione (navigazione, notifiche, stato, servizi, ...).
  • DetailLayout converte semplicemente un DetailState in un albero dedicato di widget.

Combinando le due cose, saremo in grado di creare semplicemente istanze demo/test DetailLayout, ma avremo DetailScreen per il caso d'uso reale nella nostra applicazione.

Layout dedicato

Per ottenere una migliore separazione delle responsabilità, abbiamo spostato tutto ciò che si trovava sotto il widget Consumer in un widget DetailLayout dedicato. Questo nuovo widget consuma solo dati e non è responsabile di alcuna istanziazione. Si limita a convertire lo stato di lettura in un albero di widget specifico.

Il ModalRoute. di chiamata e l'istanza ChangeNotifierProvider rimangono nel DetailScreen, e questo widget restituisce semplicemente il DetailLayout con un albero di dipendenze preconfigurato!

Questo piccolo miglioramento è specifico per l'utilizzo del provider, ma noterai che abbiamo anche aggiunto un ProxyProvider in modo che qualsiasi widget discendente possa utilizzare direttamente un DetailState. Ciò renderà più facile simulare i dati.

Estrazione di widget come classi dedicate

Non esitare mai a estrarre un albero di widget in una classe dedicata! Migliorerà le prestazioni e renderà il codice più gestibile.

Nel nostro esempio abbiamo creato un widget di layout visivo per ciascuno dei tipi di stato associati:

Istanze dimostrative

Ora abbiamo il pieno controllo su ciò che possiamo simulare e visualizzare sul nostro schermo!

Dobbiamo solo avvolgere un DetailLayout con un provider per simulare lo stato del layout.

Conclusione

Creare un'architettura software gestibile non è sicuramente facile! Anticipare scenari futuri può richiedere un grande lavoro richiesto, ma spero che la mia breve condivisione di consigli ti sarà utile in futuro!

Gli esempi possono sembrare semplici, forse addirittura eccessivi, ma man mano che la complessità della tua app aumenta, questi standard ti saranno di grande aiuto! 💪

Divertiti con Flutter e segui il blog per leggere altri articoli tecnici come questo! Restate sintonizzati!