In this moment our app is not really complete: we cannot access the settings screen, and we have to way to delete a file once it’s been created. And there’s also something more we can do: we can leverage JSON for our settings, as this may prevent errors, is commonly used, especially in other databases, and it’s generally considered a best practice.
So let’s begin by polishing our app a little bit: first we need a way to reach the settings screen. I think a good way to do that is to add an action button at the top of the files list screen. And for this we can leverage the actions property of the AppBar.
So back to the files_list_screen.dart file in the appBar of the state class, let’s add the actions: this takes a List of widgets, even if we only need one button here.
So our only widget is an IconButton, which is basically an icon that can be pressed to perform an action. This has an icon parameter, and in this case we can add a const Icon that takes the settings icon, and an onPressed callback. Here we’ll call the navigator.push method, passing the current context, and a MaterialPageRoute, of type dynamic, and in its builder, that again takes the context, we’ll return the SettingsScreen.
actions: <Widget>[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push<dynamic>(
context,
MaterialPageRoute<dynamic>(
builder: (context) => const SettingsScreen()),
);
},
),
],
Let’s also add a then callback here, so that we can update the UI based on the settings that have changed. In the function let’s just call our “getSettings” method.
Before trying this, let’s make sure that in the settings screen we are calling SharedPreferences, and not Secure Storage.
So in the imports, instead of secureStoragehelper, let’s import SPhelper.dart
Then in the loadSettings method, let’s await SPHelper.getInstance. As getters in sharedpreferences are not asynchronous, we can remove the await keyword from the getters.
In the saveSettings method we just need to await SPHelper.getINstance.
OK, let’s try this out. So, if you press the settings icon at the top right of the screen, you get to the settings screen. Here we could change a setting, for example making the date not visible, and then if we get back to the previous screen you can see this gets updated. Good.
Now. We also want to delete a single file when we don’t need it any more. I believe there are two moments when users expect to be able to do that: in the list, by swiping the file away, and in the file screen, where we could add another action button in the App bar.
Let’s begin with the file screen. In the AppBar, let’s add the actions, and in the List let’s place an IconButton, whose icon is the delete icon from the icons set.
In the onpressed function, let’s call deleteFile, and in the then callback let’s call navigator.pop to return to the screen containing the list of files. Now in a real world app you would probably want to confirm with the user if they really want to delete the file, but this won’t be necessary in this case.
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_deleteFile().then((value) {
Navigator.pop(context);
});
},
)
]
As we are here, I think it would also useful to show users a SnackBar when something changes (like after saving or deliting a file). So let’s create a new function, called _showMessage. Inside the function, first let’s check whether we hava a statusMessage to show. If statusmessage is not empty, let’s create an instance of a SnackBar, whose content is the _statusMessage state variable. Next, let’s call ScaffoldMessenger.of(context).showSnackBar(snackBar), and finally let’s also set statusMessage to be empty again.
void _showMessage() {
if (_statusMessage != '') {
final snackBar = SnackBar(content: Text(_statusMessage));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
_statusMessage = '';
}
}
Now let’s call _showMessage after each update to the _statusMessage: in the readFile method, in the writeFile method, and in deleteFile. OK, let’s run the app and see if this works. From the list let’s select a file, and the message immediately appears. Now let’s make a change, and again, the message is there. Now let’s delete this file, and the snackbar appears at the bottom of the screen. Excellent, now this app is a bit more “responsibe” to our user.
Let’s also allow deleting files from the listview containing the files. So in the FileListScreen, using the code actions, let’s wrap the ListTile widget into a Dismissible widget.
We don’t actually care about the direction of the swipe, as we want to delete the file when users swipe left or right. So in the ondismissed method, let’s retrieve an instance of FileHelper, calling it fileHelper, and then from the instance let’s call deleteFile.
This requires the file name we want to delete. As this parsing of the full path of the file repeats itself, let0s create a variable in the builder. Let’s call it fileName, and it takes the last part of the file path, and we can copy it from there.
final fileName = file.path.split('/').last;
So back to the onDismissed function, let’s pass fileName to the deleteFile method, and in the then callback, (here we don’t need any data, so let’s just specify dynamic underscore), let’s call the setState method to update the UI.
We also need to add a key to the dismissible, and for this we can create a new key from the file name:
return Dismissible(
key: Key(fileName),
onDismissed: (direction) {
final fileHelper = FileHelper();
fileHelper.deleteFile(fileName).then((dynamic _) {
setState(() {});
});
},
OK, let’s try this out: if we swipe one of the files we have created, left or right, the file will gracefully disappear from the screen.
That’s it! We app is almost complete. There’s only one more feature we could leverage. THis will not change the way our users interact with the app, but it will change the way WE store date. Let’s introduce the JSON format next!