Guides / Tutorials

Show dog breed images in Flutter

By the Dog Image API team. Updated .

Add the http package, request GET /api/v1/breeds/{breed}, read each view's preview_512 URL and pass it to Image.network. The nine views of one breed fit a three by three GridView. Previews need no key, and you show a credit link next to them.

The images are AI-generated illustrations of one adult dog on a transparent square canvas. Because every view uses the same canvas, a grid of the nine looks like a turnaround sheet without any layout tricks.

Keep the key out of the app

A Flutter binary can be unpacked, so treat everything inside it as public. This app calls only keyless URLs: the catalog, the breed detail and the preview files. Full-size PNG files, 2048 pixels square, need a key. Fetch them from your backend, store them, and let the app load them from your storage. The caching guide covers that job.

Set up

flutter pub add http url_launcher

http makes the requests. url_launcher opens the credit link.

Model classes

The breed detail has one entry per variant, and each variant maps a view id to its URLs. Two small classes are enough.

class ViewImages {
  ViewImages({required this.preview512, required this.preview1024});

  final String preview512;
  final String preview1024;

  factory ViewImages.fromJson(Map<String, dynamic> json) => ViewImages(
        preview512: json['preview_512'] as String,
        preview1024: json['preview_1024'] as String,
      );
}

class BreedSheet {
  BreedSheet({required this.slug, required this.name, required this.views});

  final String slug;
  final String name;
  final Map<String, ViewImages> views; // view id -> preview URLs

  factory BreedSheet.fromJson(Map<String, dynamic> json) {
    final variants = json['variants'] as List<dynamic>;
    final defaultSlug = json['default_variant'] as String?;
    final variant = variants.cast<Map<String, dynamic>>().firstWhere(
          (v) => v['slug'] == defaultSlug,
          orElse: () => variants.first as Map<String, dynamic>,
        );
    final rawViews = variant['views'] as Map<String, dynamic>;
    return BreedSheet(
      slug: json['slug'] as String,
      name: json['name'] as String,
      views: rawViews.map(
        (id, value) => MapEntry(id, ViewImages.fromJson(value as Map<String, dynamic>)),
      ),
    );
  }
}

The factory picks the breed's default variant, which is the main color set. If you want a color switcher, keep the whole variants list instead.

Fetch one breed

import 'dart:convert';
import 'package:http/http.dart' as http;

const apiBase = 'https://dogimage.dev/api/v1';

Future<BreedSheet> fetchBreed(String slug) async {
  final response = await http.get(Uri.parse('$apiBase/breeds/$slug'));
  if (response.statusCode != 200) {
    // 404 unknown_breed lists the valid choices in the JSON body.
    throw Exception('Breed request failed: ${response.statusCode}');
  }
  return BreedSheet.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}

Slugs look like pug, golden-retriever or german-shepherd. Pass one from your own data, or read the list from GET /api/v1/breeds and let the user choose.

Show one preview

Image.network decodes WebP, as the Flutter Image documentation lists. Add a loading and an error builder so a slow connection does not leave a hole.

Widget previewImage(String url) {
  return Image.network(
    url,
    fit: BoxFit.contain,
    loadingBuilder: (context, child, progress) =>
        progress == null ? child : const Center(child: CircularProgressIndicator()),
    errorBuilder: (context, error, stack) => const Icon(Icons.pets),
  );
}

A grid of the nine views

The catalog call also returns the nine view ids with their labels, in a stable order. For a quick screen you can list the ids yourself, as below.

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

const viewOrder = [
  'front', 'front-left', 'left', 'rear-left', 'rear',
  'rear-right', 'right', 'front-right', 'head',
];

class BreedGridPage extends StatelessWidget {
  const BreedGridPage({super.key, required this.slug});
  final String slug;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<BreedSheet>(
      future: fetchBreed(slug),
      builder: (context, snapshot) {
        if (snapshot.hasError) return const Center(child: Text('Could not load this breed.'));
        if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
        final breed = snapshot.data!;
        return Scaffold(
          appBar: AppBar(title: Text(breed.name)),
          body: Column(
            children: [
              Expanded(
                child: GridView.count(
                  crossAxisCount: 3,
                  padding: const EdgeInsets.all(8),
                  mainAxisSpacing: 8,
                  crossAxisSpacing: 8,
                  children: [
                    for (final id in viewOrder)
                      if (breed.views[id] != null)
                        ColoredBox(
                          color: Colors.black12,
                          child: previewImage(breed.views[id]!.preview512),
                        ),
                  ],
                ),
              ),
              TextButton(
                onPressed: () => launchUrl(
                  Uri.parse('https://dogimage.dev/dogs/${breed.slug}'),
                ),
                child: Text('${breed.name} image: Dog Image API'),
              ),
            ],
          ),
        );
      },
    );
  }
}

Open it with BreedGridPage(slug: 'samoyed'). The ColoredBox matters because the background of every image is transparent. Without it, a white dog on a white page disappears.

Details worth knowing

  • Use preview_512 in a grid cell and preview_1024 when one image fills the screen width.
  • The preview files carry a one-year immutable cache header, so repeat views are cheap.- The credit is a link to the breed page, shown near the image. The text form is the breed name, then "image: Dog Image API".
  • A 404 with unknown_view means a view id is wrong. The nine ids are the ones in viewOrder, and the docs describe the API.

If you later need print-quality files or a 2048 pixel hero, that is a paid, server-side step. See pricing for the plans.

Questions

Does Flutter's Image widget decode WebP?

Yes. The Flutter Image class lists WebP among its supported formats, so the 512 and 1024 pixel previews load with Image.network.

Where does the API key go in a Flutter app?

Nowhere. The catalog and the previews need no key. Full-size PNG files need one, and a server of yours should fetch those and hand them to the app.

Can the Flutter web build call the API?

Yes for the breed list, the breed detail and the preview files, because cross-origin reads are allowed. The full-size endpoint is for servers.

Do the nine views share a size?

Yes. Every view is a square canvas with a transparent background, so a three by three grid lines up without extra cropping.

Next steps