Урок 8 из 25 · Месяц 4. Состояние, сеть и Firebase — WeatherApp
Изображения из сети: NetworkImage и errorBuilder
Содержание урока
- Image.network: самый простой способ
- NetworkImage: поставщик картинки
- loadingBuilder: что показать во время загрузки
- errorBuilder: если картинка не загрузилась
- frameBuilder: плавное появление
- Переиспользуемый виджет
- Кэширование
- Что такое кэш
- cached_network_image: кэш на диске
- CachedNetworkImageProvider
- Экономим память
- Предзагрузка и очистка
- Картинки в WeatherApp
- Типичные ошибки
- Практика
- Итоги
Аватарки, фото товаров, фон с видом города — почти все картинки в приложении приходят из интернета. Загрузка может занять время, сеть может пропасть, а ссылка — оказаться битой. В этом уроке разберём, как показывать изображения из сети через Image.network и NetworkImage, как сделать красивый индикатор загрузки и запасной вариант при ошибке, и как кэшировать картинки на диске пакетом cached_network_image, чтобы они не скачивались каждый раз заново.
Image.network: самый простой способ
Image.network(
'https://picsum.photos/id/1018/800/400',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
)
Разбор:
- Первый аргумент — адрес картинки (URL).
widthиheightзадают размер области. Без них картинка займёт свой «родной» размер, и вёрстка будет «прыгать», когда она загрузится.fit— как вписать картинку в область:
BoxFit |
Что делает |
|---|---|
cover |
Заполняет всю область, лишнее обрезает. Идеально для фона и карточек |
contain |
Помещается целиком, могут остаться пустые поля |
fill |
Растягивает, искажая пропорции |
fitWidth / fitHeight |
Подгоняет по ширине или высоте |
NetworkImage: поставщик картинки
Image.network — это виджет. А NetworkImage — это ImageProvider, «поставщик» картинки: он знает, откуда взять изображение, но сам ничего не рисует. Аналогия: NetworkImage — служба доставки, Image — рамка на стене, куда доставленное вешают.
Поставщик нужен там, где виджет ожидает не виджет, а именно источник картинки:
// Аватарка
CircleAvatar(
radius: 32,
backgroundImage: const NetworkImage('https://i.pravatar.cc/150?img=3'),
onBackgroundImageError: (error, stackTrace) => debugPrint('Аватар не загрузился'),
)
// Фон контейнера
Container(
height: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
image: const DecorationImage(
image: NetworkImage('https://picsum.photos/id/1036/800/400'),
fit: BoxFit.cover,
),
),
)
// Обычный Image с поставщиком — то же самое, что Image.network
const Image(image: NetworkImage('https://picsum.photos/200'))
На самом деле Image.network(url) внутри просто создаёт Image(image: NetworkImage(url)).
loadingBuilder: что показать во время загрузки
Пока картинка грузится, по умолчанию на её месте пусто. loadingBuilder позволяет показать индикатор, причём с настоящим прогрессом:
Image.network(
'https://picsum.photos/id/1015/1200/800',
height: 220,
width: double.infinity,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child; // загрузка завершена
final total = loadingProgress.expectedTotalBytes;
final loaded = loadingProgress.cumulativeBytesLoaded;
return SizedBox(
height: 220,
child: Center(
child: CircularProgressIndicator(value: total != null ? loaded / total : null),
),
);
},
)
Разбор по параметрам loadingBuilder:
child— сама картинка. Когда загрузка закончилась, возвращаем её.loadingProgress— объектImageChunkEventс информацией о загрузке, илиnull, если всё загружено.cumulativeBytesLoaded— сколько байт уже скачано.expectedTotalBytes— сколько всего. Может бытьnull, если сервер не сообщил размер, — тогда показываем «бесконечный» индикатор (value: null).
errorBuilder: если картинка не загрузилась
Без обработки ошибки битая ссылка или отсутствие интернета покажут в debug-режиме красный блок с текстом исключения, а в консоли появится длинная ошибка. errorBuilder заменяет это аккуратной заглушкой:
Image.network(
'https://example.com/broken.jpg',
height: 220,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 220,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
alignment: Alignment.center,
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image_outlined, size: 48),
SizedBox(height: 8),
Text('Не удалось загрузить фото'),
],
),
);
},
)
error— что случилось (404, нет сети, файл не является картинкой).stackTrace— стек вызовов, пригодится для логирования.- Цвет заглушки берём из темы, чтобы она хорошо смотрелась и в тёмном режиме (см. урок про темы).
frameBuilder: плавное появление
frameBuilder вызывается, когда готов первый кадр. С его помощью картинка может появляться плавно, а не «выпрыгивать»:
Image.network(
url,
fit: BoxFit.cover,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) return child; // уже была в кэше — без анимации
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: const Duration(milliseconds: 300),
child: child,
);
},
)
frame == null — первый кадр ещё не готов, картинка прозрачная. Как только кадр появился — прозрачность плавно меняется до 1.
Переиспользуемый виджет
Чтобы не копировать три билдера по всему проекту, соберём свой виджет:
class AppNetworkImage extends StatelessWidget {
const AppNetworkImage(this.url, {super.key, this.height = 200, this.radius = 12});
final String url;
final double height;
final double radius;
@override
Widget build(BuildContext context) {
final placeholderColor = Theme.of(context).colorScheme.surfaceContainerHighest;
return ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image.network(
url,
height: height,
width: double.infinity,
fit: BoxFit.cover,
loadingBuilder: (context, child, progress) => progress == null
? child
: Container(
height: height,
color: placeholderColor,
alignment: Alignment.center,
child: const CircularProgressIndicator(),
),
errorBuilder: (context, error, stackTrace) => Container(
height: height,
color: placeholderColor,
alignment: Alignment.center,
child: const Icon(Icons.image_not_supported_outlined),
),
),
);
}
}
ClipRRect обрезает углы картинки по радиусу — так фото выглядят аккуратнее в карточках.
Кэширование
Что такое кэш
Кэш — сохранённая копия данных, чтобы не загружать их повторно. Как блокнот у вас в кармане: проще посмотреть номер в нём, чем каждый раз звонить в справочную.
У Flutter есть встроенный кэш в памяти (ImageCache): если та же картинка уже показывалась и не была вытеснена, она берётся мгновенно. Но этот кэш живёт только пока работает приложение. После перезапуска все картинки скачиваются заново — это трафик пользователя и время.
cached_network_image: кэш на диске
flutter pub add cached_network_image
Пакет сохраняет скачанные картинки в файлы на устройстве. При следующем показе — даже после перезапуска — они берутся с диска.
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: 'https://picsum.photos/id/1043/800/400',
height: 200,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) => const Icon(Icons.error_outline),
fadeInDuration: const Duration(milliseconds: 300),
)
Сравните с Image.network:
Image.network |
CachedNetworkImage |
|---|---|
url первым аргументом |
imageUrl: |
loadingBuilder |
placeholder или progressIndicatorBuilder |
errorBuilder |
errorWidget |
frameBuilder для анимации |
fadeInDuration уже встроен |
| Кэш только в памяти | Кэш в памяти и на диске |
Если нужен процент загрузки — вместо placeholder используйте progressIndicatorBuilder:
CachedNetworkImage(
imageUrl: url,
progressIndicatorBuilder: (context, url, progress) =>
Center(child: CircularProgressIndicator(value: progress.progress)),
errorWidget: (context, url, error) => const Icon(Icons.error_outline),
)
CachedNetworkImageProvider
Для CircleAvatar и DecorationImage есть кэширующий поставщик — замена NetworkImage:
CircleAvatar(
radius: 32,
backgroundImage: CachedNetworkImageProvider(user.avatarUrl),
)
Экономим память
Фото с сервера часто огромные (4000×3000 пикселей), а показываем мы их размером 100×100. Декодированная картинка занимает память по своему реальному размеру. Попросите Flutter уменьшить её при декодировании:
Image.network(url, cacheWidth: 300); // встроенный Image
CachedNetworkImage(imageUrl: url, memCacheWidth: 300); // cached_network_image
Хорошая практика — запрашивать у сервера уже уменьшенные версии (многие API принимают размер в URL, как picsum.photos/300/200).
Предзагрузка и очистка
// заранее скачать картинку следующего экрана
await precacheImage(const NetworkImage('https://picsum.photos/id/1050/800/400'), context);
// удалить одну картинку из кэша, например после смены аватара
await CachedNetworkImage.evictFromCache(user.avatarUrl);
Картинки в WeatherApp
В погодном приложении удобно показывать фон по городу и аватар пользователя из Firebase Auth (user.photoURL, если он вошёл через Google):
Stack(
children: [
CachedNetworkImage(
imageUrl: 'https://picsum.photos/seed/${Uri.encodeComponent(city)}/800/400',
height: 220,
width: double.infinity,
fit: BoxFit.cover,
errorWidget: (context, url, error) =>
Container(height: 220, color: Theme.of(context).colorScheme.primaryContainer),
),
Positioned(
left: 16,
bottom: 16,
child: Text(city, style: Theme.of(context).textTheme.headlineMedium),
),
],
)
seed/<город> у picsum.photos возвращает одну и ту же картинку для одного и того же слова — у каждого города будет «свой» фон. Uri.encodeComponent нужен, потому что в названии могут быть пробелы и кириллица.
Типичные ошибки
- Нет размера у картинки — список «прыгает» при загрузке.
- Нет
errorBuilder— красный блок ошибки в debug и пустота в релизе. - В
loadingBuilderне возвращаютchildприnull— вечная загрузка. - Используют
http://вместоhttps://— Android и iOS по умолчанию блокируют незащищённые соединения. - Огромные картинки в маленьких превью без
cacheWidth— приложение тормозит и падает по памяти на слабых устройствах.
Практика
- Галерея. Сделайте
GridViewиз 30 картинокhttps://picsum.photos/id/<номер>/300/300черезImage.networkсloadingBuilderиerrorBuilder. Ожидаемый результат: при прокрутке видны индикаторы, а не пустые ячейки. - Ошибка. Добавьте в галерею несколько заведомо битых адресов. Ожидаемый результат: на их месте аккуратная иконка, без красных блоков.
- Профиль. Сделайте шапку профиля с фоном через
DecorationImage(image: NetworkImage(...))и круглым аватаромCircleAvatar. Ожидаемый результат: оба изображения корректно обрезаны и не искажены. - Кэш на диске. Переведите галерею на
CachedNetworkImage, откройте её, затем включите авиарежим и перезапустите приложение. Ожидаемый результат: ранее просмотренные картинки показываются без интернета. - WeatherApp. Добавьте в карточку погоды фон города через
CachedNetworkImageи плавное появление. Ожидаемый результат: при смене города фон меняется, а при повторном выборе того же города грузится мгновенно.
Итоги
Image.network— виджет для картинки из сети;NetworkImage— поставщик (ImageProvider) дляCircleAvatar,DecorationImageиImage.- Всегда задавайте размер и
fit, чаще всегоBoxFit.cover. loadingBuilderпоказывает загрузку (не забудьтеreturn childприnull),errorBuilder— заглушку при ошибке.- Встроенный кэш Flutter живёт только в памяти;
cached_network_imageхранит картинки на диске. CachedNetworkImageиспользуетplaceholder/progressIndicatorBuilderиerrorWidget, а для аватаров естьCachedNetworkImageProvider.cacheWidth/memCacheWidthэкономят память на больших фото.