[Flutter 심화] 7. Sliver (5) - CustomScrollView

김주희's avatar
Aug 05, 2025
[Flutter 심화] 7. Sliver (5) - CustomScrollView
커스텀스크롤뷰 = 외부 스크롤
탭바뷰 내부의 ListView = 내부 스크롤
1층 스크롤과 2층 스크롤이 있다고 이해
 
모든 스크롤은 자신의 스크롤을 가짐
내부 스크롤과 외부 스크롤이 따로 반응 = CustomScrollView의 문제
 
CustomScrollView 내부의 SliverList는 스크롤이 없음 → CustomScrollView에만 스크롤 있음
⇒ SliverList로 해결X
 
TabBar는 좀 특이함
탭바 내부에 스크롤 존재
 
처음에는 내부 스크롤을 잡아도 1층인 외부 스크롤이 잡혀야 됨
1층이 다 사라지면 2층이 잡힘
올라갈때는 2층부터 잡히고 2층 다 내려오면 1층이 잡혀야 됨
 
내가 잡은 스크롤의 커서가 바뀌어야 됨
원래는 스크롤이벤트로 내가 직접 처리 해야됨
위로 스크롤할 때 if 1층이 minExtent가 아니면 (어딜 잡든)
minExtent가 되면 스크롤이 2층으로 변경되어야 함
if 반대 방향이면, if maxExtent가 아니면 다시 1층으로
⇒ NestedScrollView는 이걸 다 알아서 해줌 1층 = header 2층 = body(SliverFillRemaining으로 구현되어있음)
 
scroll의 listener는 onTap에 두면 안되고 build 후에 되도록 근데 rebuild 되면 리셋되니까 stateful로 해서 init에 넣어두면 됨
 
1층에는 Sliver 타입만 넣고
2층은 일반 위젯을 넣음( → SliverFillRemaining은 삭제하자)
2층의 특징 : body에 넣으면 높이가 자동으로 잡힘
 
NestedScrollView
header~ = 1층 → Sliver 타입들만
body = 2층 → 탭바뷰 (자동으로 SliverFillRemaining 같은 기능이 잡혀있음)
 
 
스크롤 이벤트의 제어권 뺏어가는 거임
 
 
 
 
고치기 전
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( // CustomScrollView 내부에 TabBarView를 직접 넣어 중첩 스크롤 버그를 발생시키는 디자인입니다. // CustomScrollView와 TabBarView가 각각 스크롤을 처리하려 하기 때문에 충돌이 발생합니다. body: CustomScrollView( slivers: [ SliverAppBar( title: const Text("중첩 스크롤 버그"), expandedHeight: 200, pinned: true, flexibleSpace: const FlexibleSpaceBar( background: Placeholder(), // 앱바 배경 ), // CustomScrollView의 헤더 부분에 TabBar를 추가합니다. bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: '탭 1'), Tab(text: '탭 2'), Tab(text: '탭 3'), ], ), ), // SliverToBoxAdapter를 사용하여 TabBarView를 CustomScrollView 안에 넣습니다. // 이 부분이 스크롤 충돌의 원인이 됩니다. SliverFillRemaining( // SliverFillRemain이 더 나음 child: TabBarView( controller: _tabController, children: [ // TabBarView 내부의 리스트입니다. // 이 리스트는 스크롤이 되어야 하지만, 상위 CustomScrollView 때문에 제대로 동작하지 않습니다. ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('탭 1의 항목 $index'), ); }, ), ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('탭 2의 항목 $index'), ); }, ), ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('탭 3의 항목 $index'), ); }, ), ], ), ), ], ), ); } }
 
nestedScrollView 적용 후
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( // CustomScrollView 내부에 TabBarView를 직접 넣어 중첩 스크롤 버그를 발생시키는 디자인입니다. // CustomScrollView와 TabBarView가 각각 스크롤을 처리하려 하기 때문에 충돌이 발생합니다. body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( title: const Text("중첩 스크롤 버그"), expandedHeight: 200, pinned: true, flexibleSpace: const FlexibleSpaceBar( background: Placeholder(), // 앱바 배경 ), // CustomScrollView의 헤더 부분에 TabBar를 추가합니다. bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: '탭 1'), Tab(text: '탭 2'), Tab(text: '탭 3'), ], ), ), ]; }, body: TabBarView( controller: _tabController, children: [ // TabBarView 내부의 리스트입니다. // 이 리스트는 스크롤이 되어야 하지만, 상위 CustomScrollView 때문에 제대로 동작하지 않습니다. ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('탭 1의 항목 $index'), ); }, ), ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('탭 2의 항목 $index'), ); }, ), ListView.builder( itemCount: 50, itemBuilder: (context, index) { return ListTile( title: Text('탭 3의 항목 $index'), ); }, ), ], ), ), ); } }
Share article

jay0628