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 chiameremoBody
; - definiamo lo stato del nostro stateful widget che:
- gestisca l’interfaccia utente sopra descritta;
- crei un
TextEditingController
per estrapolare il testo dalTextField
; - 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 adAppData
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 classeFile
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.
Il codice di questa lezione è disponibile su Github.