DEV Community

Colton Ehrman
Colton Ehrman

Posted on

Flutter: Building Photo App

- 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!

Top comments (0)