Flutter: Building Photo App
Colton Ehrman
Posted on March 28, 2020
- Course by Stephen Grider
- Project Repo
The course goes into great detail over every piece of code that is written in the App. With this walkthrough, I intend to only highlight the building process, and will not go into much detail. That part is best left to the instructor of the course. I'd like to thank him for the amazing material he releases to his students!
Setting up Initial Project
Let's set up the initial code project after running flutter create [app_name]
. We will clean up the default main.dart
file and add some new folders, files to help keep things organized in the project.
To start off, we will need a Stateful
App, that contains a simple Scaffold
with the appBar
, body
, and floatingActionButton
all set. We will also add some dummy state data.
New File Structure
lib/
main.dart # The main file with the 'main' function
src/
models/ # Will store our Dart Class data structures
App.dart # Our App starting point
lib/main.dart
import 'package:flutter/material.dart';
import './src/App.dart';
void main() => runApp(App());
lib/src/App.dart
import 'package:flutter/material.dart';
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
int _imageCounter = 0;
void _addImage() {
setState(() {
++_imageCounter;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Let\'s See Images!'),
),
body: Center(
child: Text('Image - $_imageCounter'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addImage,
),
),
);
}
}
Complete code for this section on repo
As you can see we wrap our main Widget in a MaterialApp
which then has a Scaffold
to structure/layout the rest of the Widgets. We have an AppBar
to display the App's title at the top. Some Text
in the Center
of the screen to show you the App's current state. Then a FloatingActionButton
that will trigger a setState
when pressed.
The next step will be to implement code that interacts with the image API we will be using - https://jsonplaceholder.typicode.com/photos.
We will be using the http package to make sending the requests to the API easier to work with. Don't forget to add the package to the projects pubspec.yaml
file.
Before we implement the code to interact with the API, let's set up our data model to consume the API.
New File
lib/
src/
models/
ImageModel.dart # New file
lib/src/models/ImageModel.dart
class ImageModel {
int id;
String title;
String url;
ImageModel({ this.id, this.title, this.url });
ImageModel.fromJSON(Map<String, dynamic> encodedJSON) {
id = encodedJSON['id'];
title = encodedJSON['title'];
url = encodedJSON['url'];
}
}
This model will be used to consume the API that we will be using. We set up two constructors, one that can be passed the data directly, and another that will accept JSON data.
Now we are ready to fetch the API for some JSON data.
To save on space and not clutter this article I will only be presenting updates to files that have already been created, instead of showing the whole file
Updated File
lib/
src/
App.dart # Updated file
Updates to lib/src/App.dart
// for decoding raw json data `json.decode()`
import 'dart:convert'; // new import
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // new import
import './models/ImageModel.dart'; // new import
class AppState extends State<App> {
// fetches the image JSON data from the API
// will return the raw json decoded using `json.decode`
dynamic _fetchImage() async {
final imageAPI = 'https://jsonplaceholder.typicode.com/photos/$_imageCounter';
final res = await http.get(imageAPI);
return json.decode(res.body);
}
// handler for when `FloatingActionButton` is pressed
void _addImage() async {
final json = await _fetchImage(); // fetch the data from API
final newImage = ImageModel.fromJSON(json); // consume API
setState(() {
++_imageCounter;
});
}
}
Complete code for this section on repo
Now that we have consumed the photo API, we need to create a Widget to display the data we receive.
Let's create an ImageList
Widget to hold all the data we fetch and display something on the App with it.
New File
lib/
src/
widgets/ # New folder
ImageList.dart # New file
lib/src/widgets/ImageList.dart
import 'package:flutter/material.dart';
import '../models/ImageModel.dart';
class ImageList extends StatelessWidget {
final List<ImageModel> _images;
ImageList(this._images);
Widget _renderImages() {
if (_images.length == 0)
return Text('No Images!');
return ListView.builder(
itemCount: _images.length,
itemBuilder: (BuildContext context, int index) {
return Text(_images[index].title);
}
);
}
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: _renderImages(),
),
);
}
}
We are using the ListView.builder
constructor to build a dynamic scrollable list for the images. We still do not have a proper Image UI in place. But, now you visually see the API being consumed by the application upon user interaction.
Next, we will update the App.dart file to use our new custom Widget.
Updated File
lib/
src/
App.dart # Updated File
Updates to lib/src/App.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import './models/ImageModel.dart';
import './widgets/ImageList.dart'; // new import
class AppState extends State<App> {
dynamic _fetchImage() async {
// moved the imageCounter incrememting to here to prevent
// consecutive calls overlapping before the Future resolves
// and making app fetch the same image more than once
final imageAPI = 'https://jsonplaceholder.typicode.com/photos/${_imageCounter++}';
final res = await http.get(imageAPI);
return json.decode(res.body);
}
void _addImage() async {
final json = await _fetchImage();
final newImage = ImageModel.fromJSON(json);
// modified setState
setState(() => _images.add(newImage));
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Let\'s See Images!'),
),
body: Center(
// updated child to be our custom Widget
child: ImageList(_images),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addImage,
),
),
);
}
}
We have updated our App to include the new custom Widget we built, and also fixed a small bug that caused the _fetchImage
handler to be called consecutively and fetch the same image more than once. See if you can figure out what was causing the bug, and why the new code fixes it.
To be continued...
Follow my journey of learning flutter!
Posted on March 28, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.