File Manager in Flutter - Full Tutorial
File Manager is an interesting app where we can manage the files and folders stored on our device and we can also alter them (rename or delete). In this tutorial, we are going to build the same app, which will help us to explore the storage of our device, explore the different files, open them natively and also delete as well as rename the files.
At the end of the tutorial, the result is as follows:
e
Create a new Project and Install Required Dependencies
First and foremost, create a new project.
flutter create file_manager_app
Then install the following plugins:
permission_handler: It will be used for asking to read and write the storage.
open_filex: It will be used to open the files natively.
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
permission_handler: ^11.3.1
open_filex: ^4.4.0
To access the storage, we need permission to manage external storage. To do that, open the AndroidManifest.xml and add the following lines inside the manifest declaration.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Inside the application tag, add the following for legacy support -->
<application
android:requestLegacyExternalStorage="true"
That's it! We are now ready to build our File Manager app.
Logic Implementation
This part deals with the features and the logic implementation. UI will be covered later in this article.
Fetch the List of Files from the Directory
Dart provides the FileSystem classes, objects, and functions with the help of the Dart io library. Import it as follows:
import 'dart:io';
Create a StateFul widget as follows:
import 'package:flutter/material.dart';
class ShowFilesScreen extends StatefulWidget {
const ShowFilesScreen({super.key});
@override
State<ShowFilesScreen> createState() => _ShowFilesScreenState();
}
class _ShowFilesScreenState extends State<ShowFilesScreen> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
We need to fetch the files from some directory. So for this tutorial, we will specify the root directory as follows:
Directory directory = Directory('/storage/emulated/0');
Remember that this is for android root storage. IOS doesn't allow access to modify outside the sandbox. Hence make necessary changes accordingly. Also for SD card, you can scan the filesystem or check for SD card.
Now we are going to fetch all the files and folders from the given directory. To do that, we use the listSync() method provided by the Directory class.
directory.listSync()
It returns a list of FileSystemEntity items. So we will create a list to store it and then fetch the list.
Directory directory = Directory('/storage/emulated/0');
List<FileSystemEntity> files = [];
void getFiles() {
files = directory.listSync();
}
But we also need the permission to have read and write access, and we are going to request the same using the permission_handler package.
Directory directory = Directory('/storage/emulated/0');
List<FileSystemEntity> files = [];
bool loading = true;
Future<void> getFiles() async {
setState(() {
loading = true;
});
PermissionStatus status = await Permission.manageExternalStorage.request();
if (status.isGranted == false) {}
if (await directory.exists() == false) {
return;
}
files = directory.listSync();
setState(() {
loading = false;
});
}
We added a loading variable because the action may be blocking and hence we need to show loading bar to user.
We use the directory.exists() method so that we don't run into an error if some directory doesn't exist.
We will be fetching files after all the dependencies have been loaded as follows:
@override
void didChangeDependencies() {
super.didChangeDependencies();
getFiles();
}
Sorting the Files
This is a rather easy task, we write a function to sort the FileSystemEntity objects based on their path into ascending or descending. Hence for this purpose, we create an enum at the top of file before the declaration of the class.
enum SortOrder { ascending, descending }
Also, we declare a variable inside our StatefulWidget:
SortOrder sortOrder = SortOrder.ascending;
Now we create a method, sortFileSystemItems() and use the sort method using the custom sort function as follows:
void sortFileSystemItems() {
if (sortOrder == SortOrder.ascending) {
files.sort((a, b) {
return a.path.compareTo(b.path);
});
} else {
files.sort((a, b) {
return b.path.compareTo(a.path);
});
}
setState(() {});
}
We call it every time we call the getFiles() method:
Future<void> getFiles() async {
// ...
sortFileSystemItems();
// ...
}
Renaming the files
Renaming files is one of the important features of a File Manager app which we have implemented here. Although we just implemented the renaming of files only, a similar can also be applied for the folders ( you can try it as an exercise!).
The rename method is executed as follows:
file.renameSync(newPath);
A new path is required to be specified. Here is an example:
Old Path: /storage/emulated/0/Documents/myfile.txt
File new name: myresume.txt
New Path: /storage/emulated/0/Documents/myresume.txt
So the implementation contains an AlertDialog with a TextField from where we store the name in the TextEditingController. We first substring the folder path before the filename and then merge it with the new name and the file extension is preserved.
void renameFile(FileSystemEntity file) async {
final TextEditingController controller = TextEditingController(
text: file.path.split('/').last.split('.').first,
);
bool edit = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Rename File'),
content: TextField(
controller: controller,
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context, false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: const Text('Rename'),
),
],
);
},
);
if (edit == false) {
return;
}
String newFileName =
'${controller.text}.${file.path.split('/').last.split('.').last}';
String newPath =
'${file.path.split('/').sublist(0, file.path.split('/').length - 1).join('/')}/$newFileName';
file.renameSync(newPath);
getFiles();
}
Deleting a File
For deleting a file, we use the deleteSync method:
void deleteFile(FileSystemEntity file) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete File'),
content: const Text('Are you sure you want to delete?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
file.deleteSync();
getFiles();
Navigator.pop(context);
},
child: const Text('Delete'),
),
],
);
},
);
}
Handling Back Press
Since we show all the files on a single screen, we have implemented a simple logic using PopScope. The idea is it can never pop so that while pressing it, the user doesn't accidentally exit the app. Then, at each pop call, we go to the parent directory until we reach the root directory which is /storage/emulated/0.
int selectedFileIndex = -1;
void onBackPress() {
if (selectedFileIndex != -1) {
setState(() {
selectedFileIndex = -1;
});
return;
}
if (directory.path != '/storage/emulated/0') {
directory = directory.parent;
getFiles();
}
}
The selectedFileIndex is the file selected. Our UI implementation will implement that when a list tile is long pressed, that file is selected and this is set. So if selected, the back press means that the user wants to remove it from selection. At that time, we don't change the directory, otherwise the results would be unusual.
UI Implementation
This part will contain step-by-step UI implementation.
Showing the Directory Name in AppBar
First, we deal with the PopScope.
PopScope(
canPop: false,
onPopInvoked: (didPop) {
onBackPress();
},
child: Scaffold(
appBar: AppBar(
title:
leading:
),
),
);
During file renaming, it is conventional to show the selected file name. So using the selectedFileIndex, we show either "File Manager" or the selected file name.
title: Text(
selectedFileIndex == -1
? 'File Manager'
: files[selectedFileIndex].path.split('/').last,
),
There should be a leading icon as well, so we create a back arrow icon which will also serve the functionality of the back press.
leading: directory.path == '/storage/emulated/0'
? null
: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
onBackPress();
}),
),
If it is the root directory, then the icon is not required.
Showing Files and Folders
First, create a ListView.builder and then display all the Files and folders. We use different icon for them:
ListView.builder(
itemCount: files.length,
itemBuilder: (context, index) {
return ListTile(
selected: selectedFileIndex == index,
selectedTileColor: Colors.purple.shade100,
title: Text(files[index].path.split('/').last),
leading: Icon(files[index] is File
? Icons.file_open
: Icons.folder),
);
},
),
It should contain two functionalities:
On Tap: If the FileSystemEntity is
Folder: Open the open by setting the directory path to the folder.
File: Open the file using the OpenFilex plugin that we installed.
On Long press: We implement the select functionality here only if it is a file. You can change it to a folder as well.
Implementation:
ListView.builder(
itemCount: files.length,
itemBuilder: (context, index) {
return ListTile(
selected: selectedFileIndex == index,
selectedTileColor: Colors.purple.shade100,
title: Text(files[index].path.split('/').last),
leading: Icon(files[index] is File
? Icons.file_open
: Icons.folder),
onTap: () {
if (files[index] is Directory) {
directory = files[index] as Directory;
getFiles();
} else if (files[index] is File) {
OpenFilex.open(files[index].path);
}
},
onLongPress: () {
setState(() {
selectedFileIndex = index;
});
},
);
},
),
While loading the files, we intend to show the loading bar. Hence we wrap the List View in the Visibility widget.
Visibility(
visible: !loading,
replacement: const LinearProgressIndicator(),
child: Expanded(
child: ListView.builder(
...
),
),
),
Rename and Delete - Bottom Panel
Now we are going to implement the bottom panel to rename and delete a file. So whenever is file is selected, we want the bottom sheet to appear. So Visibility widget will be used here.
Next, we show a BottomAppBar with two IconButtons that is Rename and Delete Buttons as follows:
Scaffold(
bottomSheet: Visibility(
visible: selectedFileIndex != -1,
child: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
if (files[selectedFileIndex] is File) {
deleteFile(files[selectedFileIndex]);
}
},
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
if (files[selectedFileIndex] is File) {
renameFile(files[selectedFileIndex]);
}
},
),
],
),
),
),
)
Files Sorting DropDown
For this purpose, we create a Row with two widgets, first is TextWidget showing the Folder path, and the second is DropDown as follows:
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(directory.path),
// dropdown for sorting
DropdownButton<String>(
value: sortOrder == SortOrder.ascending
? 'Ascending'
: 'Descending',
onChanged: (String? newValue) {
setState(() {
sortOrder = newValue == 'Ascending'
? SortOrder.ascending
: SortOrder.descending;
});
sortFileSystemItems();
},
items: <String>['Ascending', 'Descending']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
)
],
),
),
We want it to be above our ListView, so we wrap both of them inside a Column widget.
Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
...
),
),
Visibility(
visible: !loading,
replacement: const LinearProgressIndicator(),
child: Expanded(
child: ListView.builder(
...
),
),
),
],
),
Hence our full code is complete.
Output
Here is the full output.
Full Code:
Subscribe to my newsletter
Read articles from Manav Sarkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by