At some point, we all needed to access photos in the phone's storage, either to upload it to a backend, or just to display it in a Widget. Either way, the only choice we had was to use the famous image_picker plugin or some other plugin that uses the same approach.
Not too bad
These plugins are awesome and are enough for many use-cases, But sometimes we want more than just selecting a few photos using an external app. What if we wanted to build that external app itself? Or even less enthusiastic, what if we simply wanted to show a ListView with all the images in the phone's storage?
I got you
This would've been quite hard to achieve if not for this awesome photo_manager plugin. All you need to do is call two functions, and you'll get every album and every photo and video on your phone. Let's take a look at some code snippets:
final albums = await PhotoManager.getAssetPathList();
albums
will now hold a list of every album in the storage. An album is represented by the AssetPathEntity
class, let's take a look at the info it holds:
class AssetPathEntity {
...
// The albums' ID
String id;
// The albums name
String name;
// How many assets the album holds
int assetCount;
// The album's type
int albumType;
...
}
But that's not all, the album class has two very nice methods, that return the photos and videos (assets) it holds.
If you want to get a range of assets use this:
final albumAssets = await album.getAssetListRange(
start: startIndex,
end: endIndex,
);
If you wanna use pagination, use this instead:
final albumAssets = await album.getAssetListPaged(
page,
assetPerPage,
);
Both these methods return a list of AssetEntity
objects, which represents a single photo/video (or other). Again, let's highlight some of the main properties of AssetEntity
:
...
// Get the asset's thumbnail bytes
Future<Uint8List> get thumbData;
// Get the asset bytes
Future<Uint8List> get originBytes;
// Get the asset file
Future<File> get file;
// If the asset is a video, get its duration
int duration;
// The width of the asset
int width;
// The height of the asset
int height;
...
In short, we have everything we need to display and pick photos/videos, without quitting our lovely Flutter application.
Let's start building
Okay, now that we finished exploring the main functionality of the plugin, let's put all that knowledge to use. Here's what the final product will look like:
1- Cloning the base project and installing the plugin
First, start by cloning the base project from the link below. Or if you want, you can apply everything directly to one of your own projects.
https://github.com/aouahib/photo_manager_demo_base
Now let's install the photo_manager plugin, go to your pubspec.yaml
file and add the dependency:
dependencies:
photo_manager: ^0.5.8
iOS config
Add these lines to your Info.plist
file:
<key>NSPhotoLibraryUsageDescription</key>
<string>The app needs to access your photo library</string>
Android config
You don't need to do any platform-specific configuration.
By now, you should be good to go, make sure the app runs on your device (don't forget to flutter pub get
though).
2- Requesting permission
The base project is very simple, the first screen contains a button, clicking on it navigates us to another screen where we are going to display the photos/videos. For simplicity, everything is located in a single file: lib/main.dart
First, we need to ask the user for permission to access the external storage. Let's make sure we have that permission before navigating to the second page. Go to the MyApp
class and modify the code as follows:
// import the library
import 'package:photo_manager/photo_manager.dart';
...
return RaisedButton(
// make the function async
onPressed: () async {
// ### Add the next 2 lines ###
final permitted = await PhotoManager.requestPermission();
if (!permitted) return;
// ######
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => Gallery()),
);
},
child: Text('Open Gallery'),
);
This simply requests permission, and if it's not granted, we don't navigate to the next screen. Run the app and make sure it works.
NOTE: After installing new plugins, quit the app completely and re-run it from cold, otherwise, you will get errors
3- Fetching photos and videos
Now the fun part begins. Head to the Gallery
class. As you see, it's a StatefulWidget
with no state so far. Let's start by declaring some state variables:
class _GalleryState extends State<Gallery> {
// This will hold all the assets we fetched
List<AssetEntity> assets = [];
...
}
Next, let's write a function that fetches the assets, and updates the state. Add the following code somewhere inside the _GalleryState
class, I explain everything in the comments:
_fetchAssets() async {
// Set onlyAll to true, to fetch only the 'Recent' album
// which contains all the photos/videos in the storage
final albums = await PhotoManager.getAssetPathList(onlyAll: true);
final recentAlbum = albums.first;
// Now that we got the album, fetch all the assets it contains
final recentAssets = await recentAlbum.getAssetListRange(
start: 0, // start at index 0
end: 1000000, // end at a very big index (to get all the assets)
);
// Update the state and notify UI
setState(() => assets = recentAssets);
}
NOTE: If the phone has too many photos/videos, the function above will take too much time to execute. So for better practice, you should implement some kind of pagination using getAssetListPaged()
This function should be called when we first navigate to the Gallery screen, so override initState
and call it from there:
@override
void initState() {
_fetchAssets();
super.initState();
}
Finally, just to make sure everything works, modify the text widget so it displays the number of assets in the list:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Gallery'),
),
body: Center(
// Modify this line as follows
child: Text('There are ${assets.length} assets'),
)
);
}
Run the app, open the gallery, and if you didn't make any mistakes, the displayed number should be greater than 0 (Unless you have no photos/videos on your phone :p), something like this:
Good job, you're halfway there, all we need to do now is display the fetched assets.
4- Displaying photos and videos
We're going to display a GridView
with the thumbnails of the assets. One way of getting the thumbnail is to use the thumbData
getter of the AssetEntity
object. It returns a Future<Uint8List>
representing the actual bytes of the thumbnail. So we will be using an Image.memory()
widget to display it. Let's use all of this to make an AssetThumbnail
widget. Copy the code below somewhere in main.dart
:
class AssetThumbnail extends StatelessWidget {
const AssetThumbnail({
Key key,
@required this.asset,
}) : super(key: key);
final AssetEntity asset;
@override
Widget build(BuildContext context) {
// We're using a FutureBuilder since thumbData is a future
return FutureBuilder<Uint8List>(
future: asset.thumbData,
builder: (_, snapshot) {
final bytes = snapshot.data;
// If we have no data, display a spinner
if (bytes == null) return CircularProgressIndicator();
// If there's data, display it as an image
return Image.memory(bytes, fit: BoxFit.cover);
},
);
}
}
Now, instead of showing the number of assets, let's change the code to show a GridView
of AssetThumbnail
widgets. Go to _GalleryState
class, and change the build method:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Gallery'),
),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// A grid view with 3 items per row
crossAxisCount: 3,
),
itemCount: assets.length,
itemBuilder: (_, index) {
return AssetThumbnail(asset: assets[index]);
},
),
);
}
That's it, re-run the app and you should see all your photos/videos displayed on the gallery screen
Almost done
You did it! Congrats on reaching this far, we're finally seeing some results. We're almost done, let's make the thumbnails clickable, so we can display the original asset on a new screen. Also while we're on it, let's add a Play icon on top of the thumbnail if it's a video, because right now we can't differentiate between the two types. Go to the AssetThumbnail
widget and modify the build method as follows:
@override
Widget build(BuildContext context) {
// We're using a FutureBuilder since thumbData is a future
return FutureBuilder<Uint8List>(
future: asset.thumbData,
builder: (_, snapshot) {
final bytes = snapshot.data;
// If we have no data, display a spinner
if (bytes == null) return CircularProgressIndicator();
// If there's data, display it as an image
return InkWell(
onTap: () {
// TODO: navigate to Image/Video screen
},
child: Stack(
children: [
// Wrap the image in a Positioned.fill to fill the space
Positioned.fill(
child: Image.memory(bytes, fit: BoxFit.cover),
),
// Display a Play icon if the asset is a video
if (asset.type == AssetType.video)
Center(
child: Container(
color: Colors.blue,
child: Icon(
Icons.play_arrow,
color: Colors.white,
),
),
),
],
),
);
},
);
}
Re-run the app, and if you have any videos, you'll see a play icon on top of their thumbnails.
Displaying the original image/video
The asset data can be retrieved as a File
using the file
getter of the AssetEntity
objects.
For images
We can simply display the original image using Image.file()
, let's create the ImageScreen
widget:
NOTE: make sure to import File from dart:io and not dart:html
class ImageScreen extends StatelessWidget {
const ImageScreen({
Key key,
@required this.imageFile,
}) : super(key: key);
final Future<File> imageFile;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
alignment: Alignment.center,
child: FutureBuilder<File>(
future: imageFile,
builder: (_, snapshot) {
final file = snapshot.data;
if (file == null) return Container();
return Image.file(file);
},
),
);
}
}
Now go to the onTap
method of the AssetThumbnail
class, and navigate from there to the newly created ImageScreen
:
...
return InkWell(
onTap: () {
if (asset.type == AssetType.image) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ImageScreen(imageFile: asset.file),
),
);
}
// TODO: navigate to the video screen
},
...
Re-run the app, click on an image thumbnail, and you should navigate to another screen containing the full image.
For videos
We'll be using the video_player plugin. You can find a better tutorial for this plugin here in case the following code was hard to understand. Anyway, let's create a VideoScreen
widget:
Add the dependency
dependencies:
video_player: ^0.11.1+2
After running flutter pub get
, import the library:
import 'package:video_player/video_player.dart';
Add the VideoScreen
widget
class VideoScreen extends StatefulWidget {
const VideoScreen({
Key key,
@required this.videoFile,
}) : super(key: key);
final Future<File> videoFile;
@override
_VideoScreenState createState() => _VideoScreenState();
}
class _VideoScreenState extends State<VideoScreen> {
VideoPlayerController _controller;
bool initialized = false;
@override
void initState() {
_initVideo();
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
_initVideo() async {
final video = await widget.videoFile;
_controller = VideoPlayerController.file(video)
// Play the video again when it ends
..setLooping(true)
// initialize the controller and notify UI when done
..initialize().then((_) => setState(() => initialized = true));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: initialized
// If the video is initialized, display it
? Scaffold(
body: Center(
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
// Use the VideoPlayer widget to display the video.
child: VideoPlayer(_controller),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Wrap the play or pause in a call to `setState`. This ensures the
// correct icon is shown.
setState(() {
// If the video is playing, pause it.
if (_controller.value.isPlaying) {
_controller.pause();
} else {
// If the video is paused, play it.
_controller.play();
}
});
},
// Display the correct icon depending on the state of the player.
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
)
// If the video is not yet initialized, display a spinner
: Center(child: CircularProgressIndicator()),
);
}
}
Navigate to it
Go to the onTap
method of the AssetThumbnail
class, and modify it as follows:
...
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
if (asset.type == AssetType.image) {
// If this is an image, navigate to ImageScreen
return ImageScreen(imageFile: asset.file);
} else {
// if it's not, navigate to VideoScreen
return VideoScreen(videoFile: asset.file);
}
},
),
);
},
...
Re-run the app again, now click on a video thumbnail, and you should navigate to a new screen where you can play the video.
NOTE: After installing new plugins, quit the app completely and re-run it from cold, otherwise, you will get errors
That's a wrap
Congrats! We're done with this tutorial. You just built a Gallery app using Flutter. You can apply this knowledge in a lot of use-cases. For example, if you're making a chat app, you can directly display photos and videos on the chat screen, and the user will be able to send them directly without quitting the app (Below are screenshots of Whatsapp and Messenger apps).
You can find the final code in this github repo. If you have any questions, just ask them in the comments and I'll try to help. Bye Bye!
Top comments (7)
Hello, great article, I implemented it on my app but I have a bit of a situation, grid list keeps twitching, here's a link to how it behaves
drive.google.com/file/d/1katkcgDZS...
I can't tell.
If I had to guess, I'd say the
FutureBuilder
for each thumbnail keeps rebuilding for some reason.So I fixed it, but then again, it works well on IOS but crashes on Android, any idea what could be the cause of the crash.
with this error
Hey,
Can you please tell me How you solve the above issue?
Thanks for the article, but I would know how to say since you put WhatsApp and Messenger as an example: how would this be implemented to handle huge amounts of images and videos without breaking the app, I mean the use of memory and cache. Since apparently no flutter Widget can draw images brought from the Internet efficiently. After reviewing many articles and trying many ways to achieve what I gave up on flutter. the only way to do that is with native code.
I am not sure I understand.
First, we are not loading images from the internet, but from the phone storage.
Second, I don't think loading many images will cause any performance issues, since we'll be using (List/GridView).builder, so we'll only be displaying a few images each time.
You can check this github issue for more insights: github.com/flutter/flutter/issues/...
Hi! Nice article, but do you now, that when you accessing an image as a file on IOS, it creates a copy of image in your application folder? You can see this if you open the photo_manager sources. So, I think, it's better to implement your own ImageProvider that can work with AssetEntity and decodes its originBytes.