Saving Data in Flutter

Jan 31 2024 · Dart 3, Flutter 3.10, Visual Studio Code

Part 3: Reading & Writing Files

14. Creating the File Screen

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 13. Creating the File List Screen (Part 2) Next episode: 15. Polishing the App

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 14. Creating the File Screen

Now that we have the screen that contains the list of files in the documents directory of our app, we need a screen to read and write the file content. We will call this screen when users want to create a new file, and when they want to edit an existing one.

So, in the lib folder of our project, let’s create a new file, and call it file_screen.dart. As usual, let’s import material.dart at the top of the file. Then let’s leverage the stful shortcut to create a stateful widget and call it FileScreen.

This screen will always be called by the files_screen, so it will receive the file name that will be shown to the user, or not, if this is a new file. So, at the top of the filescreen class, let’s add a final nullable string, that takes the file name. Then let’s add this to the FileScreen constructor, before the super.key parameter.

final String? fileName; 
const FileScreen({this.fileName, super.key}); 

At the top of the state class, let’s create two TextEditingControllers: one for the file name, and one for its content. Let’s also retrieve the FileHelper object. We’ll also need a string that will contain a message we want to give our users about the status of their actions, like “File saved”, “file updated” and so on. We can call this _statusMessage and set it to an empty string.

final TextEditingController _fileNameController = TextEditingController(); 
final TextEditingController _contentController = TextEditingController(); 
final FileHelper _fileHelper = FileHelper(); 
String _statusMessage = ''; 

Now we can add the methods that will read and write the file. Let’s begin with method to write a file: it returns a Future, of void, and we can call this _writeFile, marking it as async.

Inside the method, let’s await the_filehelper writeFile method, passing the name of the file, that will be contained in the _fileNameController, at its text property, and the content of the file, which will be contained in the _contentController.text property.

Once this is done, let0s call the setState method to update the _statusMessage string: let’s set it to “File successfully saved”.

The method to read file content is very similar. Let’s create a method that returns a Future of void, and call it _readFile. It’s async as well. In the method, let’s retrieve the content of the file, calling _fileHelper.readFile, and passing the content of fileNamecontroller.text.

final content = await _fileHelper.readFile(_fileNameController.text); 

Once we have the content, let’s put it in the text property of _contentController. Then let’s call setstate, and in the method let0s update the _statusMessage to “File content retrieved”.

Now let’s add one last method, to delete a file. It returns a Future of void, we can call it _deleteFile, and as usual it as async. In the method, let’s await the _fileHelper deleteFile method, passing the name of the file, available in the text property of _ fileNameController. Let’s clear the contentController calling its own clear method, then, in the setState method, let’s update the _statusMessage with “FIle deleted”.

 Future<void> _deleteFile() async { 
    await _fileHelper.deleteFile(_fileNameController.text); 
    _contentController.clear(); 
    setState(() { 
      _statusMessage = 'File deleted.'; 
    }); 
  } 

As we are using controllers in this screen, let’s also override the dispose method. Here let’s call fileNameController.dispose and _contentController.dispose. This is something very easy to forget, but it’s considered a best practice to always dispose of controllers in Flutter, to avoid memory leaks.

void dispose() { 
  _fileNameController.dispose(); 
  _contentController.dispose(); 
  super.dispose(); 
} 

Now, in the initState method, let’s check whether a file name was passed to this screen: so if widget.filename is not null, then let’s place the file name into the _fileNameController text property. Then let’s call the readFile method, that will also update the content of the file.

@override 
void initState() { 
  super.initState(); 
  if (widget.fileName != null) { 
    _fileNameController.text = widget.fileName!; 
    _readFile(); 
  } 
} 

Right, let’s get to the UI: in the build method, we can return a Scaffold. In its appBar, we’ll place an AppBar, whose title is a Text with the name of the file, or a the “New file” string:

title: Text(widget.fileName ?? 'New File'), 

In the body of the Scaffold, let’s return a SingleCHildScrollView: this is a widget that makes sure its child can scroll when its content takes more than the available space in the screen. It has a padding to create some space between its border and its content, and here let’s specify a const EdgeInsets.all(16),

padding: const EdgeInsets.all(16), 

As for its child, it’s a Column. Let’s set the crossAxisAlignment to CrossAxisAlignment.start,

crossAxisAlignment: CrossAxisAlignment.start, 

And for its children, first let’s add a TextField. This will contain the file name, so let’s set its controller to _fileNameController. Let’s also add a decoration, that takes an InputDecoration with a LabelText of “File name”. This makes sure users know what should be written in this text field:

TextField( 
  controller: _fileNameController, 
  decoration: const InputDecoration(labelText: 'File Name'), 
), 

Let’s repeat for the content. Here the controller is _contentController, and the inputDecoration takes “Content”. In this case let’s also set the maxLines to 12, as this is a multiline text field.

Right, now we need a button to update the file: I think saving the file content is the main action for this screen, so let’s add a floatingActionButton to the Scaffold that when pressed will write the new content to the file.

In its onPressed method we’ll pass _writeFile. The child, quite predictably, will be a save icon, from the Icons set.

All right, why don’t we try this? To do that, we need to connect this second screen to the FilesListScreen.

Here, let’s add a method that navigates to FileScreen, passing the file name when it’s there: We can call this _openFileScreen, and it will take an optional String, called fileName. For this we’ll just use the Navigator.push method passing the context, and a MaterialPageRoute of dynamic, whose builder takes the context and calls FileScreen, passing the fileName. Then, when this returns, we just call setState to update the list.

void _openFileScreen({String? fileName}) { 
  Navigator.push<dynamic>( 
    context, 
    MaterialPageRoute<dynamic>( 
      builder: (context) => FileScreen(fileName: fileName), 
    ), 
  ).then((dynamic _) => setState(() {})); 
}

There are two times we need to call this method: when users add a new file, and for that we can add a floatingActionButton. And also when they want to read a single file, and this happens when they click on an item on the list.

So, let’s also add a FloatingActionButton to the Scaffold, and there in the onpressed just pass _openFileScreen. No parameters are needed here as it’s a new file.
For the child, let’s set the add Icon.

floatingActionButton: FloatingActionButton( 
  onPressed: _openFileScreen, 
  child: const Icon(Icons.add) 
), 

The second time we need to call _openFIleScreen is when users tap, or click, on one of the Items in the ListView. So in the onTap of the ListTile, let’s add a method, that calls openFIleScreen, this time passing the filename. In this case it will take last part of the file: so it’s file.path.split(’/’).last).

onTap: () { 
  _openFileScreen(fileName: file.path.split('/').last); 
} 

Right, we are ready to try this! Let’s run the app.

So press the floatingActionButton. Let’s find a name, let’s say cookies. Here I’ll paste a recipe that I’ve copied, but feel free to write anything you like here. Then let’s save, and get back to the previous screen. And as you can see, it works!

And if you click on the file name, you will get back to the text you saved! We are not finished yet, but the hard work is done!

OK, let’s polish the app a little next!