Flutter: Pinterest-Style Photo Grids
If you’re a fan of Flutter and like to explore Mediums’ article collection, then you’ve probably seen Romain Rastel’s Flutorial on creating a Staggered Gridview. He begins the article with a nod to Pinterest’s layout, but the code sample provided demonstrates icon grids with colored backgrounds. I made some changes to his code in order to recreate the Pinterest effect.
You can clone this project from the Github repo that I created here. The full main.dart file is also available at the bottom of this article, for easy reference.
Step 1: Create a new flutter project. Open the pubspec.yaml file at the top level of your directory and add flutter_staggered_grid_view to the dependencies. Run flutter packages get to install the package:
dependencies:
flutter:
sdk: flutter
flutter_staggered_grid_view:
Step 2: Open your main.dart file and import the flutter_staggered_grid_view package. Romain’s tutorial provided a code snippet, but not a full file. To speed up the learning process, I will provide the full main.dart code here for his original example.
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';void main() => runApp(new MyApp());class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Example01(),
);
}
}List<StaggeredTile> _staggeredTiles = const <StaggeredTile>[
const StaggeredTile.count(2, 2),
const StaggeredTile.count(2, 1),
const StaggeredTile.count(1, 2),
const StaggeredTile.count(1, 1),
const StaggeredTile.count(2, 2),
const StaggeredTile.count(1, 2),
const StaggeredTile.count(1, 1),
const StaggeredTile.count(3, 1),
const StaggeredTile.count(1, 1),
const StaggeredTile.count(4, 1),
];List<Widget> _tiles = const <Widget>[
const _Example01Tile(Colors.green, Icons.widgets),
const _Example01Tile(Colors.lightBlue, Icons.wifi),
const _Example01Tile(Colors.amber, Icons.panorama_wide_angle),
const _Example01Tile(Colors.brown, Icons.map),
const _Example01Tile(Colors.deepOrange, Icons.send),
const _Example01Tile(Colors.indigo, Icons.airline_seat_flat),
const _Example01Tile(Colors.red, Icons.bluetooth),
const _Example01Tile(Colors.pink, Icons.battery_alert),
const _Example01Tile(Colors.purple, Icons.desktop_windows),
const _Example01Tile(Colors.blue, Icons.radio),
];class Example01 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Example 01'),
),
body: new Padding(
padding: const EdgeInsets.only(top: 12.0),
child: new StaggeredGridView.count(
crossAxisCount: 4,
staggeredTiles: _staggeredTiles,
children: _tiles,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
padding: const EdgeInsets.all(4.0),
)));
}
}class _Example01Tile extends StatelessWidget {
const _Example01Tile(this.backgroundColor, this.iconData); final Color backgroundColor;
final IconData iconData; @override
Widget build(BuildContext context) {
return new Card(
color: backgroundColor,
child: new InkWell(
onTap: () {},
child: new Center(
child: new Padding(
padding: const EdgeInsets.all(4.0),
child: new Icon(
iconData,
color: Colors.white,
),
),
),
),
);
}
}
Above is Romain’s original code verbatim, incorporated into a working main.dart context. Next we will refactor the code to demonstrate how you can customize his plugin to emulate the Pinterest effect.
STEP 3: The first refactor I want to demonstrate is for developers who want to pull in an image asset. For example, you may have .png transparencies that you want to use as an alternative to icons. I will use the flutter logo as an example of how to incorporate image assets.
Begin by creating an images folder and dropping your transparency into it.
# PUBSPEC.YAMLflutter:
uses-material-design: true
assets:
- images/img1.png
Add the image asset to your pubspec.yaml file as shown above.
Next, in the main.dart file shown below, we create a const (e.g. imageOne) set to the image path. As you can see, each _Example01Tile receives the imageOne logo as its second argument.
Within the _Example01Tile StatelessWidget, imageOne takes on the name gridImage. At the bottom, the new Image.asset takes gridImage as an argument. The result is that our flutter icon gets passed through all of the grid tiles.
# MAIN.DARTconst imageOne = 'images/img1.jpg';List<Widget> _tiles = const <Widget>[
const _Example01Tile(Colors.green, imageOne),
const _Example01Tile(Colors.lightBlue, imageOne),
const _Example01Tile(Colors.amber, imageOne),
const _Example01Tile(Colors.brown, imageOne),
const _Example01Tile(Colors.deepOrange, imageOne),
const _Example01Tile(Colors.indigo, imageOne),
const _Example01Tile(Colors.red, imageOne),
const _Example01Tile(Colors.pink, imageOne),
const _Example01Tile(Colors.purple, imageOne),
const _Example01Tile(Colors.blue, imageOne),
];...class _Example01Tile extends StatelessWidget {
const _Example01Tile(this.backgroundColor, this.gridImage); final Color backgroundColor;
final gridImage; @override
Widget build(BuildContext context) {
return new Card(
color: backgroundColor,
child: new InkWell(
onTap: () {},
child: new Center(
child: new Padding(
padding: const EdgeInsets.all(4.0),
child: new Image.asset(gridImage),
),
),
),
);
}
}
As you can see above, this approach works well for transparent images but looks pretty bad when we try to add an ordinary photo. This is what brought me to experiment with the Pinterest look.
I refactored several sections in order to achieve the desired effect. Here is an itemized account of those changes.
Updated the names: _Example01Tile was changed to _ImageTile.
Changing from Image Assets to Network Images: I wanted to display unique images for each grid tile and simulate the novelty of Pinterest. To do this, I switched to Network Images that pull content from Picsum, a random photo generator.
Note: The two numbers in the Picsum url path specify width and height. I chose to increment +1 px for each image, so that a unique API call would be performed for each image. Otherwise the same image is returned for all tiles.
List<Widget> _tiles = const <Widget>[
const _ImageTile('https://picsum.photos/200/300/?random'),
const _ImageTile('https://picsum.photos/201/300/?random'),
const _ImageTile('https://picsum.photos/202/300/?random'),
const _ImageTile('https://picsum.photos/203/300/?random'),
const _ImageTile('https://picsum.photos/204/300/?random'),
const _ImageTile('https://picsum.photos/205/300/?random'),
const _ImageTile('https://picsum.photos/206/300/?random'),
const _ImageTile('https://picsum.photos/207/300/?random'),
const _ImageTile('https://picsum.photos/208/300/?random'),
const _ImageTile('https://picsum.photos/209/300/?random'),
];...image: new NetworkImage(gridImage)...
Applying BoxDecoration with boxFit.cover: In order for the images to cover the full grid tile, I wrote the following code, using BoxDecoration. You can now see the context where NetworkImage(gridImage) actually sits:
class _ImageTile extends StatelessWidget {
const _ImageTile(this.gridImage);
final gridImage;
@override
Widget build(BuildContext context) {
return new Card(
color: const Color(0x00000000),
elevation: 3.0,
child: new GestureDetector(
onTap: () {
print("hello");
},
child: new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new NetworkImage(gridImage),
fit: BoxFit.cover,
),
borderRadius: new BorderRadius.all(const Radius.circular(10.0)),
)
),
),
);
}
}
Set up the onTap function: In the code above, I changed the InkWell to a GestureDetector. Its onTap property has been updated to print “hello” to the console when an image is tapped. This is not critical to the app working, but if you wanted to implement a feature where users can click on the image to navigate to a new page, this would be where that code runs.
Added border radius and set Card background color to transparent: Since our images are set to fill the whole tile, we no longer need background color. However, because of the border radius we applied to the box, the white default card color will show unless the background color is set to transparent.
Card Elevation for Drop Shadow: I set the card’s elevation property (elevation: 3.0) for added effect.
That’s all there is to it. Here is the complete Main.Dart file:
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Staggered Image Grid',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new ImageTile(),
);
}
}
List<StaggeredTile> _staggeredTiles = const <StaggeredTile>[
const StaggeredTile.count(2, 2),
const StaggeredTile.count(2, 1),
const StaggeredTile.count(1, 2),
const StaggeredTile.count(1, 1),
const StaggeredTile.count(2, 2),
const StaggeredTile.count(1, 2),
const StaggeredTile.count(1, 1),
const StaggeredTile.count(3, 1),
const StaggeredTile.count(1, 1),
const StaggeredTile.count(4, 1),
];
List<Widget> _tiles = const <Widget>[
const _ImageTile('https://picsum.photos/200/300/?random'),
const _ImageTile('https://picsum.photos/201/300/?random'),
const _ImageTile('https://picsum.photos/202/300/?random'),
const _ImageTile('https://picsum.photos/203/300/?random'),
const _ImageTile('https://picsum.photos/204/300/?random'),
const _ImageTile('https://picsum.photos/205/300/?random'),
const _ImageTile('https://picsum.photos/206/300/?random'),
const _ImageTile('https://picsum.photos/207/300/?random'),
const _ImageTile('https://picsum.photos/208/300/?random'),
const _ImageTile('https://picsum.photos/209/300/?random'),
];
class ImageTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Staggered Image Grid'),
),
body: new Padding(
padding: const EdgeInsets.only(top: 12.0),
child: new StaggeredGridView.count(
crossAxisCount: 4,
staggeredTiles: _staggeredTiles,
children: _tiles,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
)));
}
}
class _ImageTile extends StatelessWidget {
const _ImageTile(this.gridImage);
final gridImage;
@override
Widget build(BuildContext context) {
return new Card(
color: const Color(0x00000000),
elevation: 3.0,
child: new GestureDetector(
onTap: () {
print("hello");
},
child: new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new NetworkImage(gridImage),
fit: BoxFit.cover,
),
borderRadius: new BorderRadius.all(const Radius.circular(10.0)),
)
),
),
);
}
}