Большинство мобильных приложений содержат различные картинки.
А как же без них? Картинки делают насыщенным и более понятным пользовательский интерфейс пользователя.
Flutter имеет встроенную поддержку картинок. Наиболее часто используемым является класс Image, который мы и рассмотрим в данной статье.
Ну что ж поехали!
Наш план
Часть 1 - введение в разработку, первое приложение, понятие состояния;
Часть 2 - файл pubspec.yaml и использование flutter в командной строке;
Часть 3 - BottomNavigationBar и Navigator;
Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самых простых;
Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;
Часть 6 - работа с формами, текстовые поля и создание поста.
Часть 7 (текущая статья) - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;
Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;
Часть 9 - немного о тестировании;
Добавление картинок в проект
Для начала попробуем добавить собственные картинки в проект.
Будьте осторожнее: картинки добавленные в ваш проект увеличивают размер приложения, так что не переусердствуйте!
Для добавления картинок нам нужно создать новую директорию images
в корне проекта:
Затем нужно прописать директорию images
в файле pubspec.yaml:
# блок зависимостей
dependencies:
flutter:
sdk: flutter
# ...
# зависимости для разработки
dev_dependencies:
# ...
# в данной секции вы можете подключить шрифты и assets файлы
flutter:
# указываем, что мы используем MaterialApp иконки и наше
# приложение соответствует Material Design
uses-material-design: true
# прописываем директорию images
# / указывает на то, что мы собираемся включить
# все файлы которые будут в директории images
assets:
- images/
Теперь добавим парочку изображений. Вы можете воспользоваться уже готовыми и взять их из Github'а:
Отредактируем нашу пустую страницу AlbumListPage
:
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
class AlbumListPage extends StatefulWidget {
@override
_AlbumListPageState createState() => _AlbumListPageState();
}
class _AlbumListPageState extends State<AlbumListPage> {
// указываем список наших изображений
final fileImages = [
"applejack.png",
"fluttershy.png",
"rarity.png",
"starlight_glimmer.png",
"twillight_sparkle.png"
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Album List Page"),
),
body: _buildContent()
);
}
// мы собираемся построить сетку из изображений
Widget _buildContent() {
// ранее мы уже подключали пакет flutter_staggered_grid_view
// который содержит StaggeredGridView
return StaggeredGridView.countBuilder(
// количество изображений
itemCount: fileImages.length,
// crossAxisCount задает количество колонок
// по которым будут выравнены изображения
crossAxisCount: 8,
// отступы по вертикали
mainAxisSpacing: 10,
// отступы по горизонтали
crossAxisSpacing: 10,
staggeredTileBuilder: (index) {
// каждое изображение будет 4 колонки в ширину (первый параметр)
// изображения на четных индексах будут в 2 раза меньше (второй параметр)
return StaggeredTile.count(4, index % 2 == 0 ? 4 : 8);
},
// строим изображение
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.pinkAccent, width: 1)
),
// Image.asset используется для отображения
// изображений которые мы прописали в pubspec.yaml
// помимо asset класс Image имеет другие методы
child: Image.asset("images/${fileImages[index]}"),
);
},
);
}
}
Запускаем.
Вуаля! Получилось довольно забавно.
Картинки из сети
Пришло время воспользоваться REST API для получения картинок.
Возможно вы заметили, что наша страница называется AlbumListPage. На самом деле мы не будем выводить список альбомов, а потом делать переключение между ними, зато это будет хорошей практикой для вас.
Мы сразу выведем все картинки.
Для начала создадим модель Photo
:
// данная модель очень похоже Post
class Photo {
final int _id;
final String _title;
final String _url;
Photo.fromJson(Map<String, dynamic> json) :
_id = json["id"],
_title = json["title"],
_url = json["url"];
}
class PhotoList {
final List<Photo> photos = [];
PhotoList.fromJson(List<dynamic> jsonItems) {
for (var jsonItem in jsonItems) {
photos.add(Photo.fromJson(jsonItem));
}
}
}
abstract class PhotoResult {}
class PhotoResultSuccess extends PhotoResult {
final PhotoList photoList;
PhotoResultSuccess(this.photoList);
}
// произошла ошибка
class PhotoResultFailure extends PhotoResult {
final String error;
PhotoResultFailure(this.error);
}
// загрузка данных
class PhotoResultLoading extends PhotoResult {
PhotoResultLoading();
}
Добавляем новый метод в Repository
:
Future<PhotoList> fetchPhotos() async {
// сначала создаем URL, по которому
// мы будем делать запрос
final url = Uri.parse("$SERVER/photos");
// делаем GET запрос
final response = await http.get(url);
// проверяем статус ответа
if (response.statusCode == 200) {
// если все ок то возвращаем все картинки
// json.decode парсит ответ
return PhotoList.fromJson(json.decode(response.body));
} else {
// в противном случае вызываем исключение
throw Exception("failed request");
}
}
Далее создаем новый контроллер AlbumController
:
// AlbumController очень поход на PostController
class AlbumController extends ControllerMVC {
final Repository repo = Repository();
// текущее состояние
PhotoResult currentState = PhotoResultLoading();
void init() async {
try {
// получение картинок
final photoList = await repo.fetchPhotos();
// успешно
setState(() => currentState = PhotoResultSuccess(photoList));
} catch (error) {
// произошла ошибка
setState(() => currentState = PhotoResultFailure("Нет интернета"));
}
}
}
Осталось только внести соответствующие изменения в AlbumListPage
:
class AlbumListPage extends StatefulWidget {
@override
_AlbumListPageState createState() => _AlbumListPageState();
}
class _AlbumListPageState extends StateMVC {
// добавляем наш контроллер
// late указывает на отложенную инициализацию
late AlbumController _controller;
_AlbumListPageState() : super(AlbumController()){
_controller = controller as AlbumController;
}
@override
void initState() {
super.initState();
// получаем картинки из JSONPlaceholder
_controller.init();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Album List Page"),
),
body: _buildContent()
);
}
Widget _buildContent() {
// получение текущего состояния
final state = _controller.currentState;
if (state is PhotoResultLoading) {
// загрузка
return Center(
child: CircularProgressIndicator(),
);
} else if (state is PhotoResultFailure) {
// ошибка
return Center(
child: Text(
state.error,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline4!.copyWith(color: Colors.red)
),
);
} else {
final images = (state as PhotoResultSuccess).photoList.photos;
// мы используем StaggeredGridView для построения
// кастомной сетки из изображений
return StaggeredGridView.countBuilder(
// количество изображений
itemCount: images.length,
// crossAxisCount задает количество колонок
// по которым будут выравнены изображения
crossAxisCount: 8,
// отступы по вертикали
mainAxisSpacing: 10,
// отступы по горизонтали
crossAxisSpacing: 10,
staggeredTileBuilder: (index) {
// каждое изображение будет в ширину 4 колонки (первый параметр)
// изображения на четных индексах будут в 2 раза меньше (второй параметр)
return StaggeredTile.count(4, index % 2 == 0 ? 4 : 8);
},
// строим изображение
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.pinkAccent, width: 1)
),
// мы используем метод Image.network для
// отображения картинок из сети
child: Image.network(
images[index].url,
// указываем максимальную ширину и высоту
width: double.infinity,
height: double.infinity,
// указываем масштабирование изображения
fit: BoxFit.cover,
// при загрузки изображения
// будет показан текст Loading...
loadingBuilder: (context, widget, imageChunkEvent) {
if (imageChunkEvent == null) {
return widget;
}
return Center(child: Text("Loading..."));
},
// при возникновении ошибки
// вместо изображения будет текст Error!
errorBuilder: (context, obj, stacktrace) => Center(child: Text("Error!")),
),
);
},
);
}
}
}
Попробуем запустить.
Работает!
Заключение
Мы рассмотрели довольно базовые вещи, которые имеет Flutter для работы с картинками.
Image.network
не является панацеей и поэтому в боевых проектах лучше использовать специальные библиотеки с большей функциональностью.
К одной из таких перспективных библиотек относится cached_network_image
Это довольно простая библотека, которая берет на себя все технические моменты и сложности.
Полезные ссылки:
Всем хорошего кода!