saf 1.0.4 copy "saf: ^1.0.4" to clipboard
saf: ^1.0.4 copied to clipboard

PlatformAndroid

Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:saf/saf.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SAF File Explorer',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const FileExplorerPage(),
    );
  }
}

class FileExplorerPage extends StatefulWidget {
  const FileExplorerPage({super.key});

  @override
  State<FileExplorerPage> createState() => _FileExplorerPageState();
}

class _FileExplorerPageState extends State<FileExplorerPage> {
  List<String> _filePaths = [];
  String? _selectedFolderPath;
  bool _isLoading = false;
  String? _errorMessage;

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

  Future<void> _requestStoragePermission() async {
    await Permission.storage.request();
  }

  Future<void> _selectFolderAndGetFiles() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
      _filePaths.clear();
    });

    try {
      // Use SAF to get dynamic directory permission (user chooses folder)
      bool? isGranted = await Saf.getDynamicDirectoryPermission();

      if (isGranted == true) {
        // Get the list of persisted permission directories
        List<String>? directories =
            await Saf.getPersistedPermissionDirectories();

        if (directories != null && directories.isNotEmpty) {
          // Use the most recently granted directory
          String selectedDirectory = directories.last;

          setState(() {
            _selectedFolderPath = selectedDirectory;
          });

          // Get all files recursively from the selected directory
          print("DEBUG: Calling Saf.getFilesPathFor with directory: $selectedDirectory");
          List<String>? filePaths = await Saf.getFilesPathFor(
            selectedDirectory,
            fileType: "any", // Get all file types
          );

          print("DEBUG: Received filePaths: $filePaths");
          print("DEBUG: FilePaths length: ${filePaths?.length ?? 0}");

          if (filePaths != null) {
            setState(() {
              _filePaths = filePaths;
              _isLoading = false;
            });
            print("DEBUG: Updated UI with ${filePaths.length} files");
          } else {
            setState(() {
              _errorMessage = "No files found in the selected directory";
              _isLoading = false;
            });
            print("DEBUG: No files found, showing error message");
          }
        } else {
          setState(() {
            _errorMessage = "No directory permissions found";
            _isLoading = false;
          });
        }
      } else {
        setState(() {
          _errorMessage = "Permission denied or folder selection cancelled";
          _isLoading = false;
        });
      }
    } catch (e) {
      setState(() {
        _errorMessage = "Error: ${e.toString()}";
        _isLoading = false;
      });
    }
  }

  Future<void> _clearPermissions() async {
    await Saf.releasePersistedPermissions();
    setState(() {
      _filePaths.clear();
      _selectedFolderPath = null;
      _errorMessage = null;
    });
  }

  Widget _buildFileItem(String filePath) {
    String fileName = filePath.split('/').last;
    String fileExtension = fileName.contains('.')
        ? fileName.split('.').last.toLowerCase()
        : '';

    IconData iconData;
    Color iconColor;

    // Choose icon based on file extension
    switch (fileExtension) {
      case 'mp3':
      case 'wav':
      case 'flac':
      case 'm4a':
        iconData = Icons.audiotrack;
        iconColor = Colors.blue;
        break;
      case 'mp4':
      case 'avi':
      case 'mkv':
      case 'mov':
        iconData = Icons.video_file;
        iconColor = Colors.red;
        break;
      case 'jpg':
      case 'jpeg':
      case 'png':
      case 'gif':
        iconData = Icons.image;
        iconColor = Colors.green;
        break;
      case 'pdf':
        iconData = Icons.picture_as_pdf;
        iconColor = Colors.red[700]!;
        break;
      case 'txt':
      case 'doc':
      case 'docx':
        iconData = Icons.description;
        iconColor = Colors.blue[700]!;
        break;
      default:
        iconData = Icons.insert_drive_file;
        iconColor = Colors.grey;
    }

    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      elevation: 2,
      child: ListTile(
        leading: Icon(iconData, color: iconColor, size: 32),
        title: Text(
          fileName,
          style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
        ),
        subtitle: Text(
          filePath,
          style: const TextStyle(fontSize: 12, color: Colors.grey),
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
        dense: true,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SAF File Explorer'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        elevation: 2,
      ),
      body: Column(
        children: [
          // Header section with folder info and controls
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.grey[100],
              border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (_selectedFolderPath != null) ...[
                  const Text(
                    'Selected Folder:',
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    _selectedFolderPath!,
                    style: const TextStyle(fontSize: 14, color: Colors.blue),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Found ${_filePaths.length} file(s)',
                    style: const TextStyle(
                      fontWeight: FontWeight.w500,
                      color: Colors.green,
                    ),
                  ),
                ] else ...[
                  const Text(
                    'No folder selected',
                    style: TextStyle(fontSize: 16, color: Colors.grey),
                  ),
                ],
                const SizedBox(height: 12),
                Row(
                  children: [
                    ElevatedButton.icon(
                      onPressed: _isLoading ? null : _selectFolderAndGetFiles,
                      icon: _isLoading
                          ? const SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(strokeWidth: 2),
                            )
                          : const Icon(Icons.folder_open),
                      label: Text(_isLoading ? 'Loading...' : 'Choose Folder'),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.deepPurple,
                        foregroundColor: Colors.white,
                      ),
                    ),
                    const SizedBox(width: 12),
                    if (_selectedFolderPath != null)
                      ElevatedButton.icon(
                        onPressed: _clearPermissions,
                        icon: const Icon(Icons.clear),
                        label: const Text('Clear'),
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.orange,
                          foregroundColor: Colors.white,
                        ),
                      ),
                  ],
                ),
              ],
            ),
          ),

          // File list section
          Expanded(
            child: _isLoading
                ? const Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        CircularProgressIndicator(),
                        SizedBox(height: 16),
                        Text('Scanning folder recursively...'),
                      ],
                    ),
                  )
                : _errorMessage != null
                ? Center(
                    child: Padding(
                      padding: const EdgeInsets.all(20),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          const Icon(
                            Icons.error_outline,
                            size: 64,
                            color: Colors.red,
                          ),
                          const SizedBox(height: 16),
                          Text(
                            _errorMessage!,
                            textAlign: TextAlign.center,
                            style: const TextStyle(
                              fontSize: 16,
                              color: Colors.red,
                            ),
                          ),
                        ],
                      ),
                    ),
                  )
                : _filePaths.isEmpty
                ? const Center(
                    child: Padding(
                      padding: EdgeInsets.all(20),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(
                            Icons.folder_outlined,
                            size: 64,
                            color: Colors.grey,
                          ),
                          SizedBox(height: 16),
                          Text(
                            'Choose a folder to see all files recursively',
                            textAlign: TextAlign.center,
                            style: TextStyle(fontSize: 16, color: Colors.grey),
                          ),
                        ],
                      ),
                    ),
                  )
                : ListView.builder(
                    itemCount: _filePaths.length,
                    itemBuilder: (context, index) {
                      return _buildFileItem(_filePaths[index]);
                    },
                  ),
          ),
        ],
      ),
    );
  }
}
67
likes
150
points
218
downloads

Publisher

verified publisherivehement.com

Weekly Downloads

Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

MIT (license)

Dependencies

flutter, permission_handler

More

Packages that depend on saf

Packages that implement saf