Slivers 심화 — CustomScrollView, SliverAppBar, SliverList
Slivers 심화 — CustomScrollView, SliverAppBar, SliverList
Sliver는 스크롤 가능한 영역의 일부를 의미합니다. CustomScrollView 안에서 여러 Sliver 위젯을 조합하면, ListView와 GridView를 한 화면에 함께 넣거나 접히는 AppBar 같은 고급 스크롤 UI를 구현할 수 있습니다.
왜 Sliver가 필요한가?
// 이렇게 하면 에러! ListView 안에 ListView를 넣을 수 없음
ListView(
children: [
ListView.builder(...), // 에러: 무한 높이 충돌
GridView.builder(...), // 에러: 같은 이유
],
)
// Sliver로 해결
CustomScrollView(
slivers: [
SliverList(...),
SliverGrid(...),
],
)
CustomScrollView 기본 구조
CustomScrollView(
slivers: [
// 접히는 AppBar
SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Sliver 예제'),
background: Image.network(
'https://example.com/header.jpg',
fit: BoxFit.cover,
),
),
),
// 리스트
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('아이템 $index'),
),
childCount: 20,
),
),
],
)
SliverAppBar 옵션
| 속성 | 동작 |
|---|---|
floating: true | 스크롤 올리면 바로 나타남 |
pinned: true | 접혀도 상단에 고정 |
snap: true | floating과 함께 사용, 스냅 효과 |
expandedHeight | 펼쳐진 상태의 높이 |
stretch: true | 과도하게 스크롤하면 이미지 늘어남 |
// 흔한 조합들
// 1. 고정 AppBar (Instagram 프로필)
SliverAppBar(pinned: true, expandedHeight: 200)
// 2. 플로팅 AppBar (검색 바)
SliverAppBar(floating: true, snap: true)
// 3. 완전히 사라지는 AppBar
SliverAppBar(floating: false, pinned: false, expandedHeight: 200)
Sliver 위젯 종류
SliverList
// 빌더 방식 (권장)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('$index')),
childCount: 100,
),
)
// SliverList.builder (더 간결)
SliverList.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(title: Text('$index')),
)
// 구분선 포함
SliverList.separated(
itemCount: 50,
itemBuilder: (context, index) => ListTile(title: Text('$index')),
separatorBuilder: (context, index) => const Divider(),
)
SliverGrid
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Card(
child: Center(child: Text('$index')),
),
childCount: 20,
),
)
SliverToBoxAdapter — 일반 위젯을 Sliver로
CustomScrollView(
slivers: [
SliverAppBar(...),
// 일반 위젯을 Sliver 안에 넣기
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'섹션 제목',
style: Theme.of(context).textTheme.titleLarge,
),
),
),
SliverList.builder(...),
// 하단 여백
const SliverToBoxAdapter(
child: SizedBox(height: 80),
),
],
)
SliverPadding
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(...),
)
SliverFillRemaining
// 남은 공간을 모두 채우기
SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: Text('콘텐츠가 없습니다'),
),
)
실전: 프로필 화면
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// 접히는 프로필 헤더
SliverAppBar(
expandedHeight: 250,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('심정훈'),
background: Stack(
fit: StackFit.expand,
children: [
Image.network(
'https://example.com/cover.jpg',
fit: BoxFit.cover,
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black54],
),
),
),
],
),
),
),
// 프로필 정보
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Flutter 개발자'),
const SizedBox(height: 8),
Row(
children: [
_statItem('게시물', '42'),
_statItem('팔로워', '1.2K'),
_statItem('팔로잉', '156'),
],
),
],
),
),
),
// 탭 (고정)
SliverPersistentHeader(
pinned: true,
delegate: _TabHeaderDelegate(),
),
// 게시물 그리드
SliverPadding(
padding: const EdgeInsets.all(2),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 2,
mainAxisSpacing: 2,
),
itemCount: 30,
itemBuilder: (context, index) => Image.network(
'https://picsum.photos/200?random=$index',
fit: BoxFit.cover,
),
),
),
],
),
);
}
Widget _statItem(String label, String count) {
return Expanded(
child: Column(
children: [
Text(count, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(label, style: const TextStyle(color: Colors.grey)),
],
),
);
}
}
SliverPersistentHeader — 고정 헤더
class _TabHeaderDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: const TabBar(
tabs: [
Tab(icon: Icon(Icons.grid_on)),
Tab(icon: Icon(Icons.list)),
],
),
);
}
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
정리
CustomScrollView+ Sliver 위젯으로 복잡한 스크롤 UI를 구현합니다SliverAppBar의floating,pinned,snap조합으로 다양한 동작을 만들 수 있습니다SliverToBoxAdapter로 일반 위젯을 Sliver 안에 넣을 수 있습니다SliverPersistentHeader로 스크롤 시 고정되는 헤더를 만듭니다- ListView와 GridView를 한 화면에 함께 넣을 때 Sliver가 필수입니다
댓글 로딩 중...