Muyiwa Femi-Ige Femi-Ige Muyiwa Oladele is a statistics major from the Federal University of Technology, Minna. He is an enthusiastic programmer versed in programming languages like Python and JavaScript.

Building a photo gallery app using Flutter

8 min read 2401

How To Build A Flutter Photo Gallery App

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:

Prerequisites

This tutorial requires the reader to have:

  1. Flutter
  2. Dependencies (image_size_getter, image_editor, photo_view)
  3. And some coffee (not required, but recommended :))

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:

  • Building the homepage
  • Creating the organized feature
  • Routing to image index
  • Creating a navigation bar within the routes

Let’s begin!

Building the homepage

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:

Gallery Home

Creating the organized feature

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:

Filtering Gallery Size

Routing to image index

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.


More great articles from LogRocket:


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),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Creating a bottom navigation bar

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:

Bottom Navigation Bar

Creating an edit feature

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:

  1. Once we click the edit button, it will route us to the edit page, where we will have the image on a full screen with a bottom modal to select an option to edit the image
  2. Next, we will create a restore function and set the value to the currently selected image
  3. Then, we will create two functions, _flipHorizon and _flipVert, to flip our images horizontally and vertically
  4. Finally, we’ll create a rotate 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:

Flutter Photo Gallery App

Conclusion

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 💻.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Muyiwa Femi-Ige Femi-Ige Muyiwa Oladele is a statistics major from the Federal University of Technology, Minna. He is an enthusiastic programmer versed in programming languages like Python and JavaScript.

Leave a Reply