Для чего нужно много скроллинг виджетов?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужны скроллинг виджеты?
Скроллинг виджеты — это контейнеры, которые позволяют пользователям просматривать контент больший, чем размер доступного пространства на экране. Они критичны для создания адаптивных приложений, которые работают на экранах разных размеров.
Основная задача скроллинг виджетов
Проблема:
────────
Есть 100 элементов списка
Экран вмещает только 10
Решение: скроллинг виджеты
─────────────────────────
Show visible: 10 элементов
Allow scroll: пользователь может прокручивать
Optimize: рисуй только видимое
Типы скроллинг виджетов
1. SingleChildScrollView
Для одного дочернего виджета, содержимое должно быть небольшим
class ScrollingSingleChild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
Text('Title'),
Image.asset('image1.jpg'),
Text('Description'),
Image.asset('image2.jpg'),
Text('More content'),
],
),
);
}
}
// Когда использовать:
// ✅ Небольшой объем контента
// ✅ Статический контент
// ✅ Простой скроллинг
// ❌ НЕ для больших списков (неэффективно)
2. ListView
Для списка элементов, автоматически использует virtualization
// Простой ListView
class MyListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
],
);
}
}
// ListView.builder для больших списков
class EfficientListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
);
}
}
// Когда использовать:
// ✅ Списки элементов
// ✅ Потенциально большой объем данных
// ✅ Нужна оптимизация производительности
// ✅ Динамический контент
3. GridView
Для сетки элементов в несколько колонок
class MyGridView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 2 колонки
childAspectRatio: 1.0, // Квадратные ячейки
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: 100,
itemBuilder: (context, index) {
return Container(
color: Colors.blue,
child: Center(child: Text('Item $index')),
);
},
);
}
}
// Когда использовать:
// ✅ Галереи фото
// ✅ Товары в интернет-магазине
// ✅ Сетка иконок
// ✅ Адаптивные макеты
4. CustomScrollView + Slivers
Для сложных скроллинг сценариев
class ComplexScroll extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
// Слипер заголовка (коллапсирует при скролле)
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('Expanded Header'),
background: Image.asset('header.jpg', fit: BoxFit.cover),
),
),
// Список элементов
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(title: Text('Item $index'));
},
childCount: 100,
),
),
],
);
}
}
// Когда использовать:
// ✅ Сложные скроллинг интерфейсы
// ✅ Коллапсирующие заголовки
// ✅ Смешанные типы контента
// ✅ Профессиональные приложения
Почему нужны скроллинг виджеты?
1. Экономия памяти
// ❌ Плохо - все в памяти
class BadExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: List.generate(10000, (index) {
return Text('Item $index'); // 10000 виджетов в памяти!
}),
),
);
}
}
// ✅ Хорошо - только видимые в памяти
class GoodExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10000,
itemBuilder: (context, index) {
return Text('Item $index'); // Только ~20 видимых
},
);
}
}
2. Производительность
// Virtualization (виртуализация)
// ListView рендерит только видимые элементы
Экран высотой 800px:
┌────────────────────┐
│ Item 1 (видимый) │
│ Item 2 (видимый) │
│ Item 3 (видимый) │
│ ... │
│ Item 20 (видимый) │
├────────────────────┤ ← Граница экрана
│ Item 21 (не рендер)│
│ Item 22 (не рендер)│
│ ... │
│ Item 10000 (не рендер)│
└────────────────────┘
Отдача: только 20 виджетов вместо 10000!
3. Адаптивность
class ResponsiveGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMobile ? 2 : 4, // Адаптивно!
),
itemCount: 100,
itemBuilder: (context, index) {
return Card(child: Text('Item $index'));
},
);
}
}
// На мобильном: 2 колонки
// На планшете: 4 колонки
Практические примеры
Пример 1: Лента новостей (like Twitter)
class NewsFeed extends StatefulWidget {
@override
State<NewsFeed> createState() => _NewsFeedState();
}
class _NewsFeedState extends State<NewsFeed> {
List<Post> posts = [];
bool isLoading = false;
ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_loadPosts();
// Загрузить больше при приближении к концу
scrollController.addListener(() {
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
_loadMorePosts();
}
});
}
Future<void> _loadPosts() async {
setState(() => isLoading = true);
// Загрузить 20 постов
final newPosts = await fetchPosts(page: 1);
setState(() {
posts = newPosts;
isLoading = false;
});
}
Future<void> _loadMorePosts() async {
final newPosts = await fetchPosts(page: posts.length ~/ 20);
setState(() {
posts.addAll(newPosts);
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: scrollController,
itemCount: posts.length + (isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index == posts.length) {
return Center(child: CircularProgressIndicator());
}
return PostCard(post: posts[index]);
},
);
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
}
Пример 2: Галерея фото
class PhotoGallery extends StatelessWidget {
final List<String> photoUrls;
const PhotoGallery({required this.photoUrls});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
),
itemCount: photoUrls.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _openPhoto(context, index),
child: Image.network(
photoUrls[index],
fit: BoxFit.cover,
),
);
},
);
}
void _openPhoto(BuildContext context, int index) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PhotoViewer(
photoUrls: photoUrls,
initialIndex: index,
),
),
);
}
}
Пример 3: Интернет-магазин
class ProductListing extends StatefulWidget {
@override
State<ProductListing> createState() => _ProductListingState();
}
class _ProductListingState extends State<ProductListing> {
late ScrollController _scrollController;
List<Product> products = [];
@override
void initState() {
super.initState();
_scrollController = ScrollController()
..addListener(_onScroll);
_loadProducts();
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 500) {
_loadMoreProducts();
}
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: _scrollController,
slivers: [
// Фильтры в sticky header
SliverAppBar(
pinned: true,
title: Text('Products'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(50),
child: ProductFilters(),
),
),
// Сетка товаров
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return ProductCard(product: products[index]);
},
childCount: products.length,
),
),
],
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Сравнение скроллинг виджетов
Виджет | Использование | Производительность
──────────────────────┼───────────────────────┼──────────────────
SingleChildScrollView | Статический контент | Плохо (все в памяти)
ListView | Список элементов | Хорошо (virtualized)
ListView.builder | Динамические списки | Отлично (lazy build)
GridView | Сетка элементов | Хорошо (virtualized)
GridView.builder | Дин. сетка | Отлично (lazy build)
CustomScrollView | Сложные макеты | Отлично (slivers)
Оптимизация скроллинга
1. Используй itemExtent для ListView
ListView.builder(
itemExtent: 80, // Высота каждого элемента
itemBuilder: (context, index) => ListTile(...),
)
// Оптимизация: можно рассчитать точное количество видимых
2. Используй RepaintBoundary для кеширования
ListView.builder(
itemBuilder: (context, index) {
return RepaintBoundary(
child: ExpensiveWidget(data: items[index]),
);
},
)
3. Ленивая загрузка изображений
ListView.builder(
itemBuilder: (context, index) {
return Image.network(
urls[index],
cacheHeight: 200,
cacheWidth: 300,
);
},
)
Вывод
Скроллинг виджеты — это критический элемент мобильных приложений:
✅ Экономия памяти — virtualization рендерит только видимое ✅ Производительность — быстрый скроллинг ✅ Адаптивность — работают на экранах разных размеров ✅ UX — естественный способ просмотра контента ✅ Масштабируемость — работают с тысячами элементов
Правило большого пальца:
- Много элементов (>10) → ListView.builder
- Статический контент → SingleChildScrollView
- Сложный макет → CustomScrollView + Slivers
- Галерея → GridView
Без скроллинг виджетов даже простое приложение будет работать медленно и потреблять много памяти!