Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 33 di 37
  • livello intermedio
Indice lezioni

Gestione dei file: lettura e scrittura su file di testo

Durante lo sviluppo di un’app è cruciale poter gestire dati da file di testo, CSV e database locali. Flutter offre anche tale opportunità.
Durante lo sviluppo di un’app è cruciale poter gestire dati da file di testo, CSV e database locali. Flutter offre anche tale opportunità.
Link copiato negli appunti

Uno degli aspetti importanti durante lo sviluppo di un’applicazione è senza dubbio la possibilità di salvare e caricare le informazioni da una sorgente dati come file di testo, CSV e database locali.

Flutter, come tutte le piattaforme cross-platform e native, offre tale opportunità.

In questa lezione focalizzeremo la nostra attenzione su come leggere e scrivere su un file di testo accessibile solo ed esclusivamente dall’applicazione.

La classe File e il path_provider plugin

In Flutter, per poter effettuare la lettura e la scrittura di file è necessario utilizzare:

Plugin/Classe Descrizione
path_provider un apposito plugin realizzato per individuare percorsi e posizioni sul filesystem del dispositivo
File una classe nativa della libreria dart:io utilizzata per gestire un apposito path su cui effettuare operazioni come creazione di file, lettura e scrittura
Future<T> Un oggetto che rappresenta un valore o un errore in potenza e che si ottiene al termine di una callback. In questo caso lo utilizzeremo come tipo di ritorno di un metodo asincrono

Esempio pratico

Come sempre, creiamo un nuovo progetto come descritto nella lezione 6.

Procediamo adesso step by step per creare un’applicazione che ci permetta di scrivere su file e di leggere da questo.

Installazione del plugin path_provider

Installiamo il plugin path_provider.

Apriamo il file pubspec.yaml e inseriamo sotto la voce dependencies il nome del plugin come segue

dependencies:
  path_provider: ^1.6.5

Eseguiamo dal nostro terminale il comando

flutter pub get

per installare il plugin.

Definizione della struttura dell’applicazione

Come mostrato nella lezione 32, definiamo la corretta struttura del nostro progetto e creiamo la seguente struttura di cartelle e file .dart come mostrato di seguito.

lib/
│── screens/
│   │── screen1
│   │      │── components
│   │      │      │── body.dart
│   │      │── screen1.dart
│── theme/
│   │── style.dart
│── services/
│   │── utilities.dart
│── routes.dart

Definizione dell’UI e dei file associati

Creata la struttura, modifichiamo i file dart relativi alla UI e partiamo dal file style.dart, che può essere definito come segue:

ThemeData appTheme() {
  return ThemeData(
      fontFamily: 'Roboto',
      primaryColor: Colors.deepOrangeAccent,
      accentColor: Colors.green,
      hintColor: Colors.white,
      buttonColor: Colors.greenAccent
  );
}

La schermata screen1 sarà costituita da:

  • un TextField per scrivere il testo da salvare;
  • un RaisedButton per salvare il testo su file
  • un RaisedButton caricare il testo da file;
  • un Text per mostrare il testo caricato.

Definiamo quindi la struttura principale della schermata in screen1.dart

class Screen1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Screen1'),
      ),
      body: Body(),
    );
  }
}

e nel file body.dart invece definiamo il contenuto della nostra pagina. In particolare:

  • creiamo un nuovo StatefulWidget che chiameremo Body;
  • definiamo lo stato del nostro stateful widget che:
    • gestisca l’interfaccia utente sopra descritta;
    • crei un TextEditingController per estrapolare il testo dal TextField;
    • gestisca lo stato.

Vediamo una possibile implementazione di seguito.

class Body extends StatefulWidget {
  Body({Key key}) : super(key: key);
  @override
  _BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> {
  String _fileContent = "";
  final myController = TextEditingController();
  @override
  void dispose() {
    myController.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
          Container(
            width: 300.0,
            child: TextField(
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: 'Add your text here',
                  hintStyle: TextStyle(color: Colors.grey),
                ),
                maxLines: 5,
                controller: myController),
          ),
          Divider(),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              RaisedButton(
                onPressed: () {
                  // ...
                },
                child: Text('Save Data'),
              ),
              RaisedButton(
                  onPressed: () {
                     // ...
                  },
                  child: Text('Read Data'))
            ],
          ),
          Divider(),
          Text(_fileContent)
        ]));
  }
}

In questo scenario, la variabile _fileContent conterrà il testo scritto dall’utente e sarà gestita in modo opportuno dal nostro stato, come vedremo nella sezione seguente.

Aggiungiamo quindi la corretta mappatura delle rotte nel file routes.dart.

final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
  "/": (BuildContext context) => Screen1(),
};

Aggiorniamo il codice della nostra main.dart come segue.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Lesson 33',
      theme: appTheme(),
      initialRoute: '/',
      routes: routes,
    );
  }
}

Ora che la nostra interfaccia è pronta possiamo passare allo sviluppo del servizio di lettura e scrittura su file.

Definizione della logica per leggere e scrivere un file

Creiamo all’interno della cartella services il file utilities.dart in cui definire la classe FileUtility che sarà responsabile delle lettura e scrittura su file.

Il package path_provider fornisce la possibilità di accedere a dati che possono trovarsi in:

  • una cartella temporanea in cache, detta temporary directory;
  • una cartella a cui può accedere solo l’applicazione e che viene cancellata una volta cancellata l’app. Questo tipo di cartella è detta documents directory e corrisponde a NSDocumentDirectory per iOS e ad AppData per Android.

In questo esempio, vogliamo scrivere il nostro file all’interno della documents directory del dispositivo e per farlo dobbiamo definire il seguente metodo.

Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();
    return directory.path;
  }

In particolare, possiamo notare che gli accessi a file e cartelle devono avvenire in modo asincrono utilizzando quindi la keyword async per il metodo e la keyword await per aspettare il risultato del metodo getApplicationDocumentsDirectory() il cui valore di ritorno rappresenta il percorso alla cartella dell’app.

Definito il percorso in cui salvare il file andiamo a creare tramite la classe File del package dart:io una referenza al percorso su disco in cui si troverà il file.

Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/mytext.txt');
  }

Anche in questo caso, il metodo è asincrono e come valore di ritorno viene restituito un nuovo oggetto File che punta al documento mytext.txt in cui scrivere e da cui leggere il testo fornito dall’utente.

Definiamo adesso il metodo per scrivere su file in modo asincrono. Il metodo dovrà:

  • accettare in input una stringa rappresentante il testo;
  • invocare il metodo _localFile per ottenere il riferimento al file su cui scrivere;
  • invocare il metodo writeAsString della classe File per scrivere la stringa sul file mytext.txt definito in partenza.

Vediamo quindi il codice.

Future<File> writeTextFile(String myText) async {
  final file = await _localFile;
  return file.writeAsString('$myText');
}

Analogamente, per leggere il file in modo asincrono dovremo:

  • invocare il metodo _localFile per ottenere il riferimento al file da cui leggere;
  • invocare il metodo readAsString per leggere il contenuto del file di interesse;
  • restituire il contenuto del file, che in questo caso è di tipo String.

Future<String> readTextFile() async {
    try {
      final file = await _localFile;
      String contents = await file.readAsString();
      return contents;
    } catch (e) {
      return "No Data";
    }
  }

In caso di errore verrà restituito dal metodo il valore No Data.

Ora che abbiamo tutto l’occorrente per leggere e scrivere in uno specifico percorso su file non resta che integrare i metodi all’interno dei nostri bottoni definiti nella classe Body.

Aggiornamento della UI con la logica dei servizi

All’interno del Widget Body definiamo i due metodi per leggere e scrivere su file utilizzando i metodi offerti dall’oggetto fileUtils come segue.

Future<File> _updateStateAndWriteText() {
    return widget.fileUtils.writeTextFile(myController.text);
  }
  void _readTextAndUpdateState() {
    widget.fileUtils.readTextFile().then((String value) {
      setState(() {
        _fileContent = value;
      });
    });
  }
  Future<File> _updateStateAndWriteText() {
    return widget.fileUtils.writeTextFile(myController.text);
  }

In particolare, possiamo notare che la classe readTextFile aggiornerà lo stato del widget Body impostando la variabile _fileContente al valore di ritorno del metodo asincrono readTextFile tramite il metodo then.

Aggiungiamo il metodo initState in modo da caricare tramite il metodo _readTextAndUpdateState() l’eventuale testo già salvato sul file locale.

@override
  void initState() {
    super.initState();
    _readTextAndUpdateState();
  }

Aggiorniamo quindi i RaisedButton con i metodi appena creati.

RaisedButton(
        onPressed: () {
          _updateStateAndWriteText();
        },
        child: Text('Save Data'),
      ),
      RaisedButton(
        onPressed: () {
          _readTextAndUpdateState();
        },
        child: Text('Read Data')
      ),

Modifichiamo infine il metodo override della classe Screen1 per passare come parametro un nuovo oggetto FileUtils al costruttore del widget Body, come segue:

body: Body(fileUtils: FileUtils()),

Eseguiamo l’applicazione per vedere il risultato delle modifiche.

Figura 176a. Esempio di scrittura e lettura su file per Android (click per ingrandire)Esempio di scrittura e lettura su file per Android
Figura 176b. Esempio di scrittura e lettura su file per iOS (click per ingrandire)Esempio di scrittura e lettura su file per iOS

Il codice di questa lezione è disponibile su Github.

Ti consigliamo anche