Nuovo progetto in Flutter: come strutturarlo

Alessandro Oddo

Introduzione


Da pochi mesi è stata rilasciata la versione 2.0 di Flutter e con sé ha portato diverse novità, sopratutto ha reso stabile la piattaforma Web e introdotto definitivamente il null-safety all'interno dei nuovi progetti.

L'introduzione del null-safety ha creato diverse problematiche sui progetti esistenti ma ha migliorato le prestazioni e costretto noi programmatori a seguire questa logica che porta inevitabilmente a commettere meno errori. 


In questo articolo voglio parlare di come strutturare un nuovo progetto, Flutter non impone una struttura, ma con il tempo in azienda, abbiamo creato una nostra convenzione che ci aiuta nello sviluppo. 

                Provider

                La logica proposta segue le direttive date da BLoC (riconducibile perlopiù ad un più noto Model-View-ViewModel) e prevede che abbiate un minimo di conoscenza della libreria provider, combinata con bloc o state_notifier, per poter comprende a pieno alcuni passaggi.

                Organizzazione delle cartelle

                Un progetto nasce solamente con il file main.dart all'interno della cartella lib.

                Il mio consiglio è di prevedere e creare sin da subito le cartelle utili per separare le varie componenti del progetto. All'interno della cartella lib consiglio di creare le seguenti cartelle:

                - api: dove inserire i file riguardanti la comunicazione remota (tipicamente chiamate REST).
                - providers: dove inserite le classi che funzioneranno da provider generici per l'app
                - models: dove inserire le classi contenenti i modelli del progetto.
                - pages: dove inserire le schermate principali, ogni schermata avrà una propria sottocartella
                - utils: dove inserire classi contenenti per lo più metodi statici
                - widgets: dove inserire widgets (per lo più Stateless) utilizzati da più pagine

                Nei seguenti capitoli spiego un po' più nel dettaglio cosa si prevede di inserire nelle singole cartelle.

                                        Cartella api

                                        Per gestire la comunicazione remota consiglio di usare Retrofit in combinazione a Dio, se non le conoscete seguite i link per avere maggiori informazioni.

                                        Nella cartella api si inserirà l'implementazione delle chiamate seguendo la guida proposta da Retrofit stesso, se non usate Retrofit inserite qui l'implementazione delle chiamate secondo le direttive della libreria utilizzata.

                                        Cartella providers

                                        Nella cartella vanno definiti singolarmente i provider che saranno comuni a tutta l'applicazioni. Un esempio di provider qui presenti saranno:

                                        PreferenceProvider.dart : in questo file inserire le SharedPreferences, ovvero quelle impostazioni che vengono salvate in memoria.

                                        ApiProvider.dart: definite le chiamate nella cartella api, l'ApiProvider conterrà l'istanza del client, l'implementazione delle chiamate e l'eventuale gestione degli errori specifici.

                                        Anche l'autenticazione con la gestione dei permessi potrebbe essere gestito tramite un proprio provider, allo stesso modo anche servizi specifici come Bonjour o WebSocket.

                                        I Provider qui definiti dovranno essere aggiunti a monte del nostro progetto utilizzando un MultiProvider. Un esempio è dato dal codice sottostante:


                                         
                                        return MultiProvider(
                                        providers: [
                                        Provider<PreferencesProvider>(create: (_) => PreferencesProvider()),
                                        Provider<ApiProvider>(create: (_) => ApiProvider()),
                                        ...
                                        ],
                                        child: MyApp());
                                         

                                        Cartella Models

                                        Su questa cartella non c'è molto da dire, le singole classi dei modelli vengono create su file separati.

                                        L'unico consiglio che sento di darvi è di creare, nel caso usiate un ORM all'interno del vostro progetto, una sotto-cartella dove inserire le classi che definiscono il vostro database interno all'app.

                                        Cartella pages

                                        Qui vanno ad essere inserite le singole schermate dell'applicazione.

                                        Il codice di seguito proposto sfrutta la libreria freezed, che permette di creare una classe le cui variabili una volta definite risultano final. L'instanza di una classe freezed non può essere modificata se non creandone una copia.

                                        Questa libreria se accoppiata alla libreria state_notifier assicura che lo state sia immodificabile se non con la creazione di una nuova istanza.

                                        La logica può sembrare inizialmente complessa ma dopo qualche utilizzo la sua gestione risulta molto comoda.

                                        Per ogni pagina deve essere creata una sotto cartella che conterrà tre file (più uno generato da freezed):

                                        • Lo state: una classe che conterrà la lista degli stati possibili della pagina


                                        import 'package:freezed_annotation/freezed_annotation.dart';
                                        part 'MyClassPageState.freezed.dart';

                                        @freezed
                                        class MyClassPageState with _$MyClassPageState {
                                        const factory MyClassPageState() =
                                        MyClassPageStateData;

                                        const factory MyClassPageState.inProgress() = MyClassPageStateInProgress;

                                        const factory MyClassPageState.onError(String message) = MyClassPageStateOnError;

                                        const factory MyClassPageState.completed() = MyClassPageStateCompleted;
                                        }


                                        • Il VM: è la classe che gestisce lo stato della vista, viene definito uno stato iniziale e l'interazione dell'utente con la vista può scatenare eventi che richiamano il VM per aggiornarne lo stato
                                         
                                        import 'package:state_notifier/state_notifier.dart';
                                        import 'MyClassPageState.dart';

                                        class MyClassVM extends StateNotifier<MyClassPageState> with LocatorMixin {

                                        MyClassVM():super(const MyClassPageStateData());

                                        void doRemoteRequest(){
                                        try{
                                        state = MyClassPageState.inProgress();
                                        ... //qui effettuo la mia richiesta remota
                                        state = MyClassPageState.completed();//notifico il completamento
                                        }
                                        catch(err){
                                        ...
                                        state = MyClassPageState.onError(err.toString()); //notifico l'errore
                                        }
                                        }
                                        }

                                        • la page: la classe che gestisce la vista, questa avrà a monte definito uno StateNotifierProvider che andrà ad inizializzare il VM. Tramite il metodo watch è possibile dire che elementi andrà a visualizzare la vista.
                                         

                                        import 'package:flutter/material.dart';
                                        import 'package:flutter_state_notifier/flutter_state_notifier.dart';
                                        import 'package:provider/provider.dart';
                                        import 'MyClassPageState.dart';
                                        import 'MyClassVM.dart';

                                        class MyClassPage extends StatelessWidget {
                                        @override
                                        Widget build(BuildContext context) {
                                        return StateNotifierProvider<MyClassVM, MyClassPageState>(
                                        create: (_) => MyClassVM(),
                                        builder: (context, child) {
                                        return context.watch<MyClassPageState>().when(
                                        ()=>Container(
                                        child: Center(
                                        child: TextButton(
                                        onPressed:()=> context.read<MyClassVM>().doRemoteRequest,
                                        child: Text("Press Here")
                                        )
                                        )
                                        ),
                                        inProgress: () => Container(
                                        child: Center(
                                        child: CircularProgressIndicator(),
                                        ),
                                        ),
                                        onError: (message) => Container(
                                        child: Center(
                                        child: Text(message),
                                        )),
                                        completed: () => Container(
                                        child: Center(
                                        child: Text("COMPLETED"),
                                        )
                                        ));
                                        });
                                        }
                                        }

                                        Da notare che questa vista ha il vantaggio di estendere uno StatelessWidget. Ogni volta che il VM cambierà il suo state la vista si aggiornerà di conseguenza rappresentando il nuovo stato corrente.

                                        Il mio consiglio è di partire sempre definendo gli stati della vista.

                                        Stiamo facendo una schermata di login? Allora prevediamo uno stato iniziale di inserimento dati, uno di invio della richiesta, uno di errore ed eventualmente uno per gestire il successo della richiesta (potrebbe non essere necessario e portare direttamente alla schermata successiva).

                                        Altro esempio: dobbiamo a creare una home page con un menu a tab? Creiamo un singolo stato in cui prevediamo una variabile numerica che cambia in base al tab selezionato.

                                        Definiti gli stati sarà più facile scrivere il VM e la page.

                                        Cartella utils

                                        Questa cartella conterrà le classi di utility specifiche per il progetto.

                                        Ad esempio: se abbiamo necessità di definirci dei metodi statici per convertire le date fornite da un sistema remoto e visualizzarle, o di definirci dei controlli sulla validità dei campi inseriti, questo è il posto giusto dove inserirli.

                                        Cartella widgets

                                        La cartella widgets, conterrà tutti i widgets che sono utilizzati da più pagine.

                                        Consiglio generale sull'utilizzo dei Widget è quello di estendere la classe StatelessWidget per creare un nuovo Widget ogni volta che gli elementi risultano troppo annidati, questo per evitare l'effetto "matrioska" che è tipico delle viste costruite in Flutter.

                                        Conclusioni

                                        La suddivisione delle cartelle sopra descritta permette di avere facilmente sott'occhio l'intero progetto, questo aspetto non è da sottovalutare e partire subito con un progetto ben strutturato velocizza molto lo sviluppo.

                                        Queste convenzioni non sono ufficiali e sono legate al nostro metodo di sviluppo corrente, essendo Flutter una piattaforma ancora in evoluzione nulla toglie che quanto scritto risulti obsoleto in futuro.