
1. Sliver
- 스크롤 가능한 위젯의 일종
- 효율적인 스크롤 및 리스트 렌더링을 위해 사용되는 독특한 디자인 패턴
- 리스트의 각 아이템을 개별적으로 렌더링하고 스크롤 이벤트를 처리하는 데 최적화
- CustomScrollView 위젯과 함께 사용되는 경우 매우 효율적인 리스트 렌더링 제공
- 내부에도 모두 Sliver 를 사용해야 함
아니면 화면의 크기를 계산해서 그림을 그려야함
height: MediaQuery.of(context).size.height // 전체 화면 높이
- MediaQuery.of(context).padding.top // 상태바 높이
- _appBar.preferredSize.height // 앱바 높이
- 16 // 텍스트 높이
- 48 // 버튼 높이
2. SliverList / SliverGrid
- gridDelegate를 이용해서 그리드 레이아웃 정의
- 매 index마다 itemBuilder가 생성된 그리드 반환
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return Card(
child: Container(
color: Colors.blue[index % 9 * 100],
child: Center(
child: Text('Grid Item $index'),
),
),
);
},
childCount: 20,
),
),

3. SliverChildListDelegate : 일반적인 위젯 리스트→ SliverList
SliverList(
delegate: SliverChildListDelegate(
[
Container(
child: Image(image: NetworkImage('https://picsum.photos/id/1/200/300'),fit: BoxFit.cover),
),
SizedBox(height: 10,),
Container(
child: Image(image: NetworkImage('https://picsum.photos/id/2/200/300'),fit: BoxFit.cover),
),
SizedBox(height: 10,),
Container(
child: Image(image: NetworkImage('https://picsum.photos/id/3/200/300'),fit: BoxFit.cover),
),
]
),
),
4. SliverChildBuilderDelegate
- ListView.builder나 GridView.builder와 유사한 방식으로 작동
- 매 index마다 itemBuilder 콜백을 사용하여 슬리버에 대한 자식을 반환
- 동적인 리스트나 그리드를 쉽게 생성
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: 10,
(context, index) =>
Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: Image.network("https://picsum.photos/id/${index}/200/300",
fit: BoxFit.contain,),
),
)
),
5. ListView.builder / List.generate
- List.generate : 미리 전체 아이템 리스트를 생성 / 메모리 많이 사용
- ListView.builder : lazy loading기법을 사용하여 리스트 뷰 아이템이 필요할 때만 생성하고 화면에 표시
- itemBuilder : 콜백함수를 이용해서 아이템이 필요할 때만 생성 / 대규모 데이터 처리에 효율적
6. CustomScrollView : 조금 더 세부적인 ScrollView
- snap과 floating옵션 : 위아래로 살짝만 스크롤해도 SliverAppBar가 화면에 나타나고 사라짐
- pinned 옵션 : 스크롤을 올리더라도 앱바가 화면에서 사라지지 않음
- expandedHeight : 앱바의 크기를 지정 / 기본값은 60
- flexibleSpace : 유연한 공간을 제공하여 다양한 콘텐츠를 넣을수 있음
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
snap: true,
floating: true,
title: Text("SliverAppBar"),
pinned: false,
expandedHeight: 250,
flexibleSpace: FlexibleSpaceBar(
title: Text("제목"),
centerTitle: true,
background: Image.network(
'https://picsum.photos/200/300',
fit: BoxFit.cover,
),
),
const SliverAppBar(
title: Text("pinned"),
pinned: true,
centerTitle: true,
),
7. SliverToBoxAdapter
일반적인 위젯을 슬리버에서도 사용할 수 있게 어댑터 역할 해준다.
SliverToBoxAdapter(
child: Container(
height: 200,
child: ListView.builder(
scrollDirection: Axis.horizontal, // 가로 스크롤
itemCount: 7,
itemBuilder: (BuildContext context, int index) {
return Image(
image: AssetImage("assets/다운로드 (${index + 1}).jpg"));
},
),
)),
위 코드는 스크롤뷰 안에 어뎁터로 가로 리스트뷰를 넣었는데 다른 방법으로는
NestedScrollView
안에 ListView
를 넣거나 CustomScrollView
에서 SliverList
를 생성하면 된다.8. SliverFixedExtentList
내부 아이템들의 사이즈를 무시하고 지정한 크기 100으로 고정된다.
SliverFixedExtentList(
itemExtent: 100, // 내부 요소의 크기 무시
delegate: SliverChildBuilderDelegate( // 계산이 필요하면 빌더를 사용
(context, index) {
if (index % 4 == 0 && index != 0)
return Ad(((index / 4) - 1).toInt());
return Diary(index);
},
)),
],
),
);
}
9. SliverFillViewport
현재 뷰포트를 채우기 위한 뷰를 생성한다.
SliverFillViewport(
delegate: SliverChildBuilderDelegate(
childCount: 10,
(context, index) {
return Card(
child: Container(
color: Colors.blue[index % 9 * 100],
child: Center(
child: Text('Fill Viewport Item $index'),
),
),
);
},
),
),
10. SliverFillRemaining : 남은 공간을 채우기 위한 View 생성
// 위에 있는 Sliver,
SliverFillRemaining(
child: Center(
child: Text('This is the remaining content.'),
),
),
11. SliverPersistentHeader : 앱에서 항상 표시되는 헤더
SliverPersistentHeader(
pinned: false,
floating : false,
delegate: MySliverPersistentHeaderDelegate(
minHeight: 50.0,
maxHeight: 120.0,
child: Container(
color: Colors.blue[300],
child: const Center(
child: Text(
'SliverPersistentHeader',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
),
- minHeight : 작은 값으로 축소된 헤더의 크기를 지정할 때 사용 / pinned: true일때 유효
pinned: true일때
- floating : false : 가장 상단으로 올라가야 내려옴
- floating : true : 스크롤을 하자마자 헤더가 내려옴
- SliverPersistentHeaderDelegate를 상속 -> 필요한 메소드와 속성 사용 헤더의 크기와 위치를 동적 조정 가능
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
MySliverPersistentHeaderDelegate({
required this.minHeight,
required this.maxHeight,
required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(covariant MySliverPersistentHeaderDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
pinned false일때
## 상단 PersistenceHeader pinned false 적용


```dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.purple),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text("Kurly"),
),
body: HomeBody(),
),
);
}
}
class HomeBody extends StatelessWidget {
const HomeBody({super.key});
@override
Widget build(BuildContext context) {
return NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverPersistentHeader(delegate: TabBarDelegate(), pinned: false)
];
},
body: TabBarView(
children: [
ScreenA(),
Container(color: Colors.blue),
Container(color: Colors.yellow),
],
),
);
}
}
class ScreenA extends StatelessWidget {
const ScreenA({
super.key,
});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
height: 300,
color: Colors.greenAccent,
),
),
SliverPersistentHeader(delegate: CategoryBreadcrumbs(), pinned: true),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: 40,
(context, index) {
return Container(
height: 40,
// 보는 재미를 위해 인덱스에 아무 숫자나 곱한 뒤 255로
// 나눠 다른 색이 보이도록 함.
color: Color.fromRGBO(
(index * 45) % 255, (index * 70) % 255, (index * 25), 1.0),
);
},
),
),
],
);
}
}
class CategoryBreadcrumbs extends SliverPersistentHeaderDelegate {
const CategoryBreadcrumbs();
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text("총 239개", style: TextStyle(color: Colors.black)),
const Spacer(),
TextButton(
onPressed: () {},
child: const Center(child: Text("필터")),
)
],
),
);
}
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
class TabBarDelegate extends SliverPersistentHeaderDelegate {
const TabBarDelegate();
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return TabBar(
tabs: [
Tab(child: Text("컬리추천")),
Tab(child: const Text("신상품")),
Tab(child: const Text("베스트")),
],
);
}
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
```


첫번째 방법 Scaffold에 appbar와 tabbar 두기
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.purple),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text("Kurly"),
bottom: TabBar(
tabs: [
Tab(child: Text("컬리추천")),
Tab(child: const Text("신상품")),
Tab(child: const Text("베스트")),
],
),
),
body: HomeBody(),
),
);
}
}
class HomeBody extends StatelessWidget {
const HomeBody({super.key});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverFillRemaining(
child: TabBarView(
children: [
ScreenA(),
Container(color: Colors.blue),
Container(color: Colors.yellow),
],
),
),
],
);
}
}
class ScreenA extends StatelessWidget {
const ScreenA({
super.key,
});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
height: 300,
color: Colors.greenAccent,
),
),
SliverPersistentHeader(delegate: CategoryBreadcrumbs(), pinned: true),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: 40,
(context, index) {
return Container(
height: 40,
// 보는 재미를 위해 인덱스에 아무 숫자나 곱한 뒤 255로
// 나눠 다른 색이 보이도록 함.
color: Color.fromRGBO(
(index * 45) % 255, (index * 70) % 255, (index * 25), 1.0),
);
},
),
),
],
);
}
}
class CategoryBreadcrumbs extends SliverPersistentHeaderDelegate {
const CategoryBreadcrumbs();
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text("총 239개", style: TextStyle(color: Colors.black)),
const Spacer(),
TextButton(
onPressed: () {},
child: const Center(child: Text("필터")),
)
],
),
);
}
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
두번째 방법 Column + Expanded
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.purple),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text("Kurly"),
),
body: HomeBody(),
),
);
}
}
class HomeBody extends StatelessWidget {
const HomeBody({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
tabs: [
Tab(child: Text("컬리추천")),
Tab(child: const Text("신상품")),
Tab(child: const Text("베스트")),
],
),
Expanded(
child: TabBarView(
children: [
ScreenA(),
Container(color: Colors.blue),
Container(color: Colors.yellow),
],
),
),
],
);
}
}
class ScreenA extends StatelessWidget {
const ScreenA({
super.key,
});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
height: 300,
color: Colors.greenAccent,
),
),
SliverPersistentHeader(delegate: CategoryBreadcrumbs(), pinned: true),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: 40,
(context, index) {
return Container(
height: 40,
// 보는 재미를 위해 인덱스에 아무 숫자나 곱한 뒤 255로
// 나눠 다른 색이 보이도록 함.
color: Color.fromRGBO(
(index * 45) % 255, (index * 70) % 255, (index * 25), 1.0),
);
},
),
),
],
);
}
}
class CategoryBreadcrumbs extends SliverPersistentHeaderDelegate {
const CategoryBreadcrumbs();
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text("총 239개", style: TextStyle(color: Colors.black)),
const Spacer(),
TextButton(
onPressed: () {},
child: const Center(child: Text("필터")),
)
],
),
);
}
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
class TabBarDelegate extends SliverPersistentHeaderDelegate {
const TabBarDelegate();
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return TabBar(
tabs: [
Tab(child: Text("컬리추천")),
Tab(child: const Text("신상품")),
Tab(child: const Text("베스트")),
],
);
}
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}



핵심은 NestedScrollView는 중첩된 스크롤을 공유해준다는 것이다. 보통 TabBar와 많이 활용된다
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text("앱바"),
pinned: false,
//snap: true,
//floating: true,
expandedHeight: 250,
flexibleSpace: Container(
color: Colors.green,
),
),
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 300,
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 30,
itemBuilder: (context, index) {
return Container(
width: 50,
color: Colors.yellow[((index % 9) + 1) * 100], // 0 ~ 8
);
},
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: 20,
(context, index) {
return Container(
height: 50,
color: Colors.blue[((index % 9) + 1) * 100], // 0 ~ 8
);
},
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
//반응하는 앱바
title: Text("앱바"),
pinned: true, // 앱바 고정
// pinned: false, // 앱바 사라짐
// snap: true,
// //손가락으로 튕구다
// floating: true,
expandedHeight: 250,
flexibleSpace: Container(
color: Colors.green,
),
),
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 300,
),
),
SliverPersistentHeader(delegate: )
SliverList(
// 특정 타이밍에 자식의 스크롤을 무력화 시킨다. 포문 돌리면서 넣는거
delegate: SliverChildBuilderDelegate(
childCount: 20,
(context, index) {
return Container(
color: Colors.yellow[((index % 9) + 1) * 100], //0~8
height: 50,
);
},
),
),
SliverList(
// 특정 타이밍에 자식의 스크롤을 무력화 시킨다. 포문 돌리면서 넣는거
delegate: SliverChildBuilderDelegate(
childCount: 20,
(context, index) {
return Container(
color: Colors.blue[((index % 9) + 1) * 100], //0~8
height: 50,
);
},
),
)
],
),
);
}
}
class MyPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
MyPersistentHeaderDelegate({
required this.minHeight,
required this.maxHeight,
});
@override
Widget build(BuildContext context, double shrinkOffset,
bool overlapsContent) {
return Container(
color: Colors.blueGrey,
child: Center(
child: Text(
'SliverPersistentHeader',
style: TextStyle(fontSize: 20.0, color: Colors.white),
),
),
);
}
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}

import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text("Flutter Shopping App"),
centerTitle: true,
pinned: false,
),
SliverAppBar(
title: Text("앱바"),
pinned: true,
//snap: true,
//floating: true,
expandedHeight: 250,
flexibleSpace: Container(
color: Colors.green,
),
),
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 300,
),
),
SliverPersistentHeader(
pinned: true,
delegate: MyPersistentHeaderDelegate(
child: Container(
color: Colors.blueGrey,
child: Center(
child: Text(
'SliverPersistentHeader',
style: TextStyle(fontSize: 20.0, color: Colors.white),
),
),
),
minHeight: 50,
maxHeight: 500,
),
),
SliverPersistentHeader(
pinned: true,
delegate: MyPersistentHeaderDelegate(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 30,
itemBuilder: (context, index) {
return Container(
width: 100,
color: Colors.yellow[((index % 9) + 1) * 100], // 0 ~ 8
);
},
),
minHeight: 100,
maxHeight: 100,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: 20,
(context, index) {
return Container(
height: 50,
color: Colors.blue[((index % 9) + 1) * 100], // 0 ~ 8
);
},
),
),
SliverFillRemaining(
// 마지막 남은 높이 계산
child: Center(
child: Text('This is the remaining content.'),
),
),
],
),
);
}
}
class MyPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final double minHeight;
final double maxHeight;
final Widget child;
MyPersistentHeaderDelegate({
required this.child,
required this.minHeight,
required this.maxHeight,
});
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
}
Share article