file_picker_next 0.1.1
file_picker_next: ^0.1.1 copied to clipboard
A Flutter plugin for picking files that complies with Google Play's Photo and Video Permissions policy. Uses Android Photo Picker and Storage Access Framework instead of requesting READ_MEDIA permissions.
import 'package:flutter/material.dart';
import 'package:file_picker_next/file_picker_next.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'File Picker Next Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<PickedFileInfo> _files = [];
bool _loading = false;
Future<void> _pickFiles(
FilePickerType type, {
List<String>? extensions,
}) async {
setState(() => _loading = true);
try {
final files = await FilePickerNext.pickFiles(
type: type,
allowMultiple: true,
allowedExtensions: extensions,
);
if (files != null) {
setState(() => _files = files);
}
} finally {
setState(() => _loading = false);
}
}
Future<void> _pickSingleImage() async {
setState(() => _loading = true);
try {
final files = await FilePickerNext.pickFiles(
type: FilePickerType.image,
allowMultiple: false,
);
if (files != null) {
setState(() => _files = files);
}
} finally {
setState(() => _loading = false);
}
}
Future<void> _showMediaSourceDialog({bool isVideo = false}) async {
final file = await FilePickerNext.showMediaSourceDialog(
context,
isVideo: isVideo,
);
if (file != null) {
setState(() => _files = [file]);
}
}
Future<void> _saveFirstFileToDownloads() async {
if (_files.isEmpty) {
_showSnackBar('No file to save');
return;
}
setState(() => _loading = true);
try {
final file = _files.first;
final bytes = file.bytes ?? await file.file.readAsBytes();
final path = await FilePickerNext.saveToDownloads(
fileName: file.name,
bytes: bytes,
mimeType: file.mimeType,
);
if (path != null) {
_showSnackBar('Saved to: $path');
} else {
_showSnackBar('Failed to save file');
}
} finally {
setState(() => _loading = false);
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
void _clearFiles() {
setState(() => _files.clear());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('File Picker Next Demo'),
actions: [
if (_files.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: _clearFiles,
tooltip: 'Clear files',
),
],
),
body: Column(
children: [
// Picker Options
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Pick Files:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _loading ? null : _pickSingleImage,
icon: const Icon(Icons.image),
label: const Text('Single Image'),
),
ElevatedButton.icon(
onPressed: _loading
? null
: () => _pickFiles(FilePickerType.image),
icon: const Icon(Icons.photo_library),
label: const Text('Multiple Images'),
),
ElevatedButton.icon(
onPressed: _loading
? null
: () => _pickFiles(FilePickerType.video),
icon: const Icon(Icons.video_library),
label: const Text('Videos'),
),
ElevatedButton.icon(
onPressed: _loading
? null
: () => _pickFiles(
FilePickerType.custom,
extensions: ['pdf', 'doc', 'docx'],
),
icon: const Icon(Icons.description),
label: const Text('Documents'),
),
ElevatedButton.icon(
onPressed: _loading
? null
: () => _pickFiles(FilePickerType.any),
icon: const Icon(Icons.folder),
label: const Text('Any File'),
),
],
),
const SizedBox(height: 16),
const Text(
'Camera/Gallery:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _loading
? null
: () => _showMediaSourceDialog(isVideo: false),
icon: const Icon(Icons.camera_alt),
label: const Text('Take Photo'),
),
ElevatedButton.icon(
onPressed: _loading
? null
: () => _showMediaSourceDialog(isVideo: true),
icon: const Icon(Icons.videocam),
label: const Text('Record Video'),
),
],
),
if (_files.isNotEmpty) ...[
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _loading ? null : _saveFirstFileToDownloads,
icon: const Icon(Icons.save),
label: const Text('Save First File to Downloads'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
],
],
),
),
const Divider(),
// File List
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: _files.isEmpty
? const Center(
child: Text(
'No files picked yet.\nTry picking some files!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _files.length,
itemBuilder: (context, index) {
final file = _files[index];
return Card(
child: ListTile(
leading: _buildFileIcon(file),
title: Text(
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${_formatBytes(file.size)}\n${file.path}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
isThreeLine: true,
trailing: IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () => _showFileDetails(file),
),
),
);
},
),
),
],
),
);
}
Widget _buildFileIcon(PickedFileInfo file) {
if (file.isImage) {
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.file(
file.file,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.image, size: 50);
},
),
);
} else if (file.isVideo) {
return const Icon(Icons.video_file, size: 50, color: Colors.blue);
} else if (file.isDocument) {
return const Icon(Icons.description, size: 50, color: Colors.orange);
} else {
return const Icon(Icons.insert_drive_file, size: 50, color: Colors.grey);
}
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
void _showFileDetails(PickedFileInfo file) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('File Details'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_detailRow('Name', file.name),
_detailRow('Extension', file.extension),
_detailRow('Size', _formatBytes(file.size)),
_detailRow('MIME Type', file.mimeType),
_detailRow('Path', file.path),
_detailRow('Is Image', file.isImage.toString()),
_detailRow('Is Video', file.isVideo.toString()),
_detailRow('Is Document', file.isDocument.toString()),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
const SizedBox(height: 2),
Text(value, style: const TextStyle(fontSize: 14)),
const Divider(),
],
),
);
}
}