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_512in a grid cell andpreview_1024when 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
404withunknown_viewmeans a view id is wrong. The nine ids are the ones inviewOrder, 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
- API docs: endpoints, keys, limits and errors.
- All dog breeds, each with nine views.
- Pricing for full-size transparent PNG files.
- A dog breed card with Tailwind CSS
- A dog breed gallery in SvelteKit
- A dog breed picker in Angular
- A dog breed selector in Vue 3