← Назад к вопросам

Для чего нужно много скроллинг виджетов?

2.2 Middle🔥 221 комментариев
#Flutter виджеты#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Для чего нужны скроллинг виджеты?

Скроллинг виджеты — это контейнеры, которые позволяют пользователям просматривать контент больший, чем размер доступного пространства на экране. Они критичны для создания адаптивных приложений, которые работают на экранах разных размеров.

Основная задача скроллинг виджетов

Проблема:
────────
Есть 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

Без скроллинг виджетов даже простое приложение будет работать медленно и потреблять много памяти!