Are you looking for a fun project to scale up your learning in Flutter? Then this is the one for you! This article will guide you on how to build a photo gallery app using the image_editor
package in Flutter. The steps will include getting images from local storage, creating a sort functionality, and making an edit functionality.
To jump ahead:
This tutorial requires the reader to have:
To kick off this tutorial, let’s work on four major components spanning three Dart files that are essential to building our photo gallery application. The components include:
Let’s begin!
Before we start, we need to create a new Flutter project.
First, create a new directory using the command mkdir <foldername>
in your terminal. To change the existing directory to the new directory you created, use the command cd <foldername>
. Then, create a new Flutter project using the command Flutter create <projectname>
and leave the rest for the Flutter SDK to handle.
(Note: Change the keywords <foldername>
and <pojectname>
to the names of your choice)
Next, paste the codes below into the main.dart
file.
import 'package:flutter/material.dart'; import 'homepage.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); static const String _title = 'Gallery App'; // This widget is the root of your application. @override Widget build(BuildContext context) => const MaterialApp( title: _title, debugShowCheckedModeBanner: false, home: MyHomePage(title: 'Gallery'), ); }
The code above simply instantiates the MyHomePage
class. So we will need to create a new class, and we will do that in a new Dart file called homepage.dart
, as imported in the code above.
Next, we will paste the code below into the homepage.dart
file:
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'gallerywidget.dart'; const whitecolor = Colors.white; const blackcolor = Colors.black; class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final urlImages = [ 'assets/images/a.jpg', 'assets/images/b.jpg', 'assets/images/c.jpg', 'assets/images/d.jpg', ]; var transformedImages = []; Future<dynamic> getSizeOfImages() async { transformedImages = []; for (int i = 0; i < urlImages.length; i++) { final imageObject = {}; await rootBundle.load(urlImages[i]).then((value) => { imageObject['path'] = urlImages[i], imageObject['size'] = value.lengthInBytes, }); transformedImages.add(imageObject); } } @override void initState() { getSizeOfImages(); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, backgroundColor: whitecolor, centerTitle: true, title: Text( widget.title, style: const TextStyle(color: blackcolor), ), iconTheme: const IconThemeData(color: blackcolor), ), // Body area body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), decoration: const BoxDecoration( color: whitecolor, ), child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 5, mainAxisSpacing: 5, ), itemBuilder: (context, index) { return RawMaterialButton( child: InkWell( child: Ink.image( image: AssetImage(transformedImages\[index\]['path']), height: 300, fit: BoxFit.cover, ), ), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => GalleryWidget( urlImages: urlImages, index: index, ))); }, ); }, itemCount: transformedImages.length, ))) ], )), ); } }
In the code above, we created an array list comprising four image paths retrieved from the assets folder in the project app structure.
Then, we created a Future <dynamic>
instance called getSizeOfImages
to get the size of each file. Finally, we called initState
, a lifecycle method, to call our getSizeOfImages
function.
We returned our scaffold with a simple appBar
and grid for our widgets. appBar
contains our title, and it will house the action property that we’ll discuss later.
The GridView.builder
will display the images in a grid format as designated by their index in the array. In our itemBuilder
, we wrapped an Inkwell
widget around our image with an onPressed
property, which will direct us to a new page when we click an image.
Here is the result of the code above:
We need the organized feature to sort the arrangement of our images based on size and name. So, within our homepage.dart
file, let’s create four callback Future <dynamic>
functions above the initState
function (sortImagesByIncreseSize, sortImagesByDecreaseSize, sortImagesByNamesIncrease, sortImagesByNamesDecrease
).
We will call the functions above once we click on the corresponding button to which we linked it.
Here is the code:
Future<dynamic> sortImagesByIncreseSize() async { transformedImages.sort((a, b) => a['size'].compareTo(b['size'])); } Future<dynamic> sortImagesByDecreseSize() async { transformedImages.sort((b, a) => a['size'].compareTo(b['size'])); } Future<dynamic> sortImagesByNamesIncrease() async { transformedImages.sort((a, b) => a['path'].compareTo(b['path'])); } Future<dynamic> sortImagesByNamesDecrease() async { transformedImages.sort((b, a) => a['path'].compareTo(b['path'])); }
Now, we want to use the callback functions we created. So, we will create an action property within our appBar
with four TextButtons
: ascending name, descending name, ascending size, and descending size.
Here is what the code looks like:
actions: <Widget>[ GestureDetector( onTap: () { // show the dialog showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text("Filter By"), // content: const Text("This is my message."), actions: [ TextButton( child: Column( children: const [ Text('By size (Ascending)'), ], ), onPressed: () { sortImagesByIncreseSize() .then((value) => setState(() {})); Navigator.pop(context); }, ), TextButton( child: Column( children: const [ Text('By size (descending)'), ], ), onPressed: () { sortImagesByDecreseSize() .then((value) => setState(() {})); Navigator.pop(context); }, ), TextButton( child: Column( children: const [ Text('By name (Ascending)'), ], ), onPressed: () { sortImagesByNamesIncrease() .then((value) => setState(() {})); Navigator.pop(context); }, ), TextButton( child: Column( children: const [ Text('By name (descending)'), ], ), onPressed: () { sortImagesByNamesDecrease() .then((value) => setState(() {})); Navigator.pop(context); }, ), ], ); }, ); }, child: Container( margin: const EdgeInsets.only(right: 20), child: const Icon(Icons.more_vert), ), ) ],
Let’s break down what’s going on here.
We used the methods described above to implement the sorting functionality. Thus, to sort images by size, our app will use the getSizeOfImages
function initially created to loop through the array of images and obtain their sizes.
Then, it will sort the images based on the size, both increasing and decreasing, using the sortImagesByIncreasingSize
, sortImagesByDecreasingSize
, sortImagesByNameIncrease
, and sortImagesByNameDecrease
functions respectively.
Here is what it looks like at the moment:
The goal of this section is to learn how to display each image once we click it. To do this, we will refer back to the onPressed
property in our GridView.builder
.
As we mentioned before, once we click an image, it will navigate us to a new page with the GalleryWidget
class.
We need to create the GalleryWidget
class, and we will do that within a new file called gallerywidget.dart
.
Within our GalleryWidget
class, we want to be able to scroll through our images, and to get our images by using their index once we click them. So, we need to create a pageController
, which we will use to control the pages’ index.
Next, we need to create a build method, and within it, we will create a PhotoViewGallery
, which comes from the photo_view
package we installed earlier.
To correctly call our image index, we’ll put the pageController
inside the PhotoViewGallery
. Next, we’ll use a builder
property to build each image with the help of the index.
Then, we’ll return the PhotoViewGalleryPageOptions
, which we got from the photo_view
package, and inside it, we will put the image (urlImage
) with the AssetImage
class.
Here is an implementation of the explanation above:
import 'package:flutter/material.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'homepage.dart'; class GalleryWidget extends StatefulWidget { final List<String> urlImages; final int index; final PageController pageController; // ignore: use_key_in_widget_constructors GalleryWidget({ required this.urlImages, this.index = 0, }) : pageController = PageController(initialPage: index); @override State<GalleryWidget> createState() => _GalleryWidgetState(); } class _GalleryWidgetState extends State<GalleryWidget> { var urlImage; @override void initState() { provider = AssetImage(widget.urlImages[widget.index]); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, backgroundColor: whitecolor, centerTitle: true, title: const Text( 'Gallery', style: TextStyle(color: blackcolor), ), iconTheme: const IconThemeData(color: blackcolor), leading: IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back)), ), body: Column( children: <Widget>[ Expanded( child: PhotoViewGallery.builder( pageController: widget.pageController, itemCount: widget.urlImages.length, builder: (context, index) { urlImage = widget.urlImages[index]; return PhotoViewGalleryPageOptions( imageProvider: AssetImage(urlImage), ); }, ), ), ], ), ); } }
Now, we want to create a bottom navigation bar. To do that, we will start by calling the bottomNavigationBar
widget below our body. Within this widget, create an onTap
function that we will use to navigate to a new page (edit page). We want to create a BottomNavigationBarItem
with the Edit
label.
Back to the onTap
property, we will need to get the selected navigation bar item. We can do that by setting an index in the _GalleryWidgetState
class. We will use the code below to implement the functionality:
int bottomIndex = 0;
Now, we will use the setState
function to keep track of the indexes, and after that, we will create an if
statement within the setState
to check if the index is equal to one. If it is true, it will take us to the edit section.
Here is what the navigation widget looks like:
bottomNavigationBar: BottomNavigationBar( onTap: (e) { setState(() { bottomIndex = e; if (e == 1) { Navigator.push( context, MaterialPageRoute( builder: (context) => EditScreen(image: urlImage))); } }); }, currentIndex: bottomIndex, backgroundColor: Colors.white, iconSize: 30, selectedItemColor: Colors.black, unselectedIconTheme: const IconThemeData( color: Colors.black38, ), elevation: 0, items: const <BottomNavigationBarItem>[ BottomNavigationBarItem(icon: Icon(Icons.share), label: 'Share'), BottomNavigationBarItem( icon: Icon(Icons.edit), label: 'Edit', ), ], ),
And here is the result of the code above:
What is a gallery application without an edit feature? The following section is an essential part of this guide as it will show you how to create an edit feature using the image_editor
package in Flutter.
Let’s begin!
edit_screen.dart
We will start by creating a new Dart file called edit_screen.dart
. Within this file, we will call the EditScreen
class we are routing to in our navigation bar. We will create two editing features in this section: rotation and flipping.
Here is a breakdown of the processes involved:
restore
function and set the value to the currently selected image_flipHorizon
and _flipVert
, to flip our images horizontally and verticallyrotate
function and set the value to a function called handleOption
Here is an implementation of the previous explanation:
import 'dart:convert'; import 'dart:typeddata'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:imageeditor/imageeditor.dart'; import 'package:imagesizegetter/imagesize_getter.dart'; import 'homepage.dart'; class EditScreen extends StatefulWidget { String image; EditScreen({required this.image, Key? key}) : super(key: key); @override State<EditScreen> createState() => _EditScreenState(); } class _EditScreenState extends State<EditScreen> { ImageProvider? provider; bool horizon = true; bool vertic = false; int angle = 1; int angleConstant = 90; @override void initState() { provider = AssetImage(widget.image); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, backgroundColor: whitecolor, centerTitle: true, title: const Text( 'Gallery', style: TextStyle(color: blackcolor), ), iconTheme: const IconThemeData(color: blackcolor), actions: <Widget>[ IconButton( icon: const Icon(Icons.settings_backup_restore), onPressed: restore, tooltip: 'Restore image to default.', ), ], ), body: Column( children: <Widget>[ if (provider != null) AspectRatio( aspectRatio: 1, child: Image( image: provider!, ), ), Expanded( child: Scrollbar( child: SingleChildScrollView( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ ElevatedButton( child: const Text('Flip Horizontal'), onPressed: () { _flipHorizon(FlipOption( horizontal: horizon, vertical: vertic)); }, ), ElevatedButton( child: const Text('Flip Vertical'), onPressed: () { _flipVert(FlipOption( horizontal: vertic, vertical: horizon)); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( child: const Text('Rotate R 90'), onPressed: () { _rotate(RotateOption(angleConstant * angle!)); setState(() { angle = angle + 1; if ((angleConstant * angle) > 360) angle = 1; }); }, ), ElevatedButton( child: const Text('Rotate L 90'), onPressed: () { _rotate( RotateOption(360 - (angleConstant * angle!))); setState(() { angle = angle + 1; if ((angleConstant * angle) > 360) angle = 1; }); }, ) ], ) ], ), ), ), ), ], ), ); } void setProvider(ImageProvider? provider) { this.provider = provider; setState(() {}); } void restore() { setProvider(AssetImage(widget.image)); } Future<Uint8List> getAssetImage() async { final ByteData byteData = await rootBundle.load(widget.image!); return byteData.buffer.asUint8List(); } Future<void> _flipHorizon(FlipOption flipOption) async { handleOption(<Option>[flipOption]); setState(() { horizon = !horizon; }); } Future<void> _flipVert(FlipOption flipOption) async { handleOption(<Option>[flipOption]); setState(() { horizon = !horizon; }); } Future<void> _rotate(RotateOption rotateOpt) async { handleOption(<Option>[rotateOpt]); } Future<void> handleOption(List<Option> options) async { final ImageEditorOption option = ImageEditorOption(); for (int i = 0; i < options.length; i++) { final Option o = options[i]; option.addOption(o); } option.outputFormat = const OutputFormat.png(); final Uint8List assetImage = await getAssetImage(); final srcSize = ImageSizeGetter.getSize(MemoryInput(assetImage)); print(const JsonEncoder.withIndent(' ').convert(option.toJson())); final Uint8List? result = await ImageEditor.editImage( image: assetImage, imageEditorOption: option, ); if (result == null) { setProvider(null); return; } final resultSize = ImageSizeGetter.getSize(MemoryInput(result)); print('srcSize: $srcSize, resultSize: $resultSize'); final MemoryImage img = MemoryImage(result); setProvider(img); } }
Here is the result of the code above:
We have come to the end of the tutorial, and I hope it has been helpful. Let’s summarize the notable parts of this project. In the Creating the organized feature section, we detailed changing the arrangement of the images (ascending and descending order) with respect to the size or name of the image file.
Then, in the Routing to image index section, we spoke about navigating to each image with respect to its position (index). The edit_screen.dart
section talked about transforming the image placement (flip and rotate) and reverting it back to its original placement using the image_editor
package in Flutter.
Here are some other articles you can explore to get a better idea of the components we built in this tutorial:
Thank you for reading! Happy coding đź’».
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.