스크롤 뷰로 되어있는 화면 안에 여러개의 뷰&리스트뷰가 들어가 있는 형태의 상세페이지가 있다.
업체 측의 이번 요청은 이 화면에서 중간에 있는 뷰가 스크롤 되고 있을 때는 상단의 탭바 부분이 계속 상단에 고정되어 있다가, 그 뷰의 스크롤이 끝나면 탭바도 사라지게 해달라는 내용이었다.
iOS를 전공으로 하고 Flutter를 배워나가는 내 머릿속을 퍼뜩 스친 건 UITableView SectionHeader와 Sliver였더랬다.
그런데 Sliver로만 검색하면 흔히 나오는 건, UINavigationBar LargeTitle 모드에서 스크롤하면 기본 NaviBar 형태로 바뀌는 것 마냥, AppBar 쪽이 커다랗게 보였다가 작아졌다가 하는 형태였다.
하지만 iOS에서 만들 수 있는 UI를 Flutter에서 못할 리가 없을텐데, 라는 생각으로 키워드 조금씩 바꿔 구글링하던 와중,
🔗 flutter_sticky_header Package
빠밤!
그냥 예제 코드에 그치는 게 아니라 아예 그대로 갖다쓰면 되는 패키지를 발견했다. 역시 세상에는 부지런한 천재 개발자들이 참 많아b
코드 맛보기용 사용은 연습용 프로젝트 여기에서 했다.
1. 문서 공식 Example 코드 적용해보기
1-a. 패키지 설치
flutter pub add flutter_sticky_header
1-b. 코드 작성
일단 패키지 ReadMe에 올라와 있는 gif를 보자면 내가 원하는 동작에 부합하는 것 같은데, 실제 코드 수정하면서 확인해봐야 하니 예시로 올라온 코드 조금 수정해서 먼저 돌려줘봤다.
CustomScrollView(
slivers: [
SliverStickyHeader(
header: Container(
height: 60.0,
color: Colors.lightBlue,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: const Text(
'Header #0',
style: TextStyle(color: Colors.white),
),
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => ListTile(
leading: const CircleAvatar(
child: Text('0'),
),
title: Text('List tile #$i'),
),
childCount: 20,
),
),
),
...
- Sliver를 사용하기 위해서는 ```CustomScrollView```를 사용해야 한다.
- Colum의 children에 위젯들을 넣어주는 것처럼, slivers 배열에 sliver 위젯을 넣어주면 된다.
- 여기서는 패키지에서 제공하는 ```SliverStickyHeader``` 를 하나씩 넣고, 이 헤더는 마다마다 sliver 파라미터에 body 위젯을 가진 형태다.
실행해보면 ReadMe에 나와있는 것처럼 섹션이 바뀔 때마다 상단에 고정된 섹션에 맞게 잘 바뀌는 걸 확인할 수 있다b
2. sliver 코드 수정하기
2-a. 만들어뒀던 커스텀 위젯 사용하도록 코드 수정하기
기존 프로젝트에서 사용하던 위젯은 그냥 Container도 있고 ListView도 있는 형태였다.
위의 패키지 예제 코드를 보면 List는 SliverList를 사용하고 있는데, 기존 프로젝트에서 사용하던 ListView가 이미 여기저기 재사용되고 있는 위젯이어서 새로 만들 것 없이 있는 걸 그대로 사용하고 싶다고 생각했다.
그렇다. 아는 사람들은 알겠지만, ```SliverToBoxAdapter```가 바로 그 역할을 해준다.
CustomScrollView(
slivers: [
SliverStickyHeader(
header: Container(
height: 60.0,
color: Colors.lightBlue,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: const Text(
'Header #0',
style: TextStyle(color: Colors.white),
),
),
sliver: const SliverToBoxAdapter(
/// 일반 위젯을 SliverToBoxAdapter로 감쌈
child: SubWidget(),
),
),
SliverStickyHeader(
header: Container(
height: 60.0,
color: Colors.lightBlue,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: const Text(
'Header #1',
style: TextStyle(color: Colors.white),
),
),
sliver: const SliverToBoxAdapter(
child: SubWidget2(),
),
),
SliverStickyHeader(
header: Container(
height: 60.0,
color: Colors.lightBlue,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
alignment: Alignment.centerLeft,
child: const Text(
'Header #2',
style: TextStyle(color: Colors.white),
),
),
sliver: SliverToBoxAdapter(
child: SubListView(
itemBuilder: (context, index) {
return SubListItemView(title: "$index 제목");
},
separatorBuilder: (context, index) => const Divider(
color: Colors.black38,
),
itemCount: _listCount,
onSelected: (index) {},
onPressedMore: _incrementCounter,
),
),
),
...
(sub_list_view.dart는 다른 프로젝트에서 구현해뒀던걸 슬쩍 긁어와서 상황에 맞게 조금만 수정했다.)
3. header 코드 수정하기
3-a. 일부 섹션에서는 헤더 제거하기
위에서 얘기한 특정 탭바 부분에서만 헤더가 필요하고 그 외 다른 서브 위젯에는 모두 헤더가 필요없다.
SliverStickyHeader에서 header 파라미터에는 null 값을 넣을 수 없긴 하지만 방법은 있다.
...
SliverStickyHeader(
/// 일부 섹션에서 헤더 제거
header: const SizedBox.shrink(),
sliver: const SliverToBoxAdapter(
/// 일반 위젯을 SliverToBoxAdapter로 감쌈
child: SubWidget(),
),
),
...
```SizedBox.shrink()```는 개인적으로 특정 상태일 때만 위젯을 보여줘야 할 때, 삼항연산자로 많이 사용한다.
ex. isWidgetShowing ? const CustomWidget() : const SizedBox.shrink()
3-b. 탭바 헤더 적용해보기
우선 테스트용으로 추가한 탭바 코드는 아래와 같다.
사실 ```TabbarView```는 탭바 아래 동일한 영역을 풀로 차지하고 있어야 하는데, 이 프로젝트의 경우 탭바 클릭 시 바뀌는 뷰의 높이가 제각각이라서 탭바만 ```TabBar```를 사용하고 실제 콘텐츠는 삼항연산자 사용해서 구분되도록 구현해뒀다.
import 'package:flutter/material.dart';
class SubTabBarView extends StatefulWidget {
const SubTabBarView({super.key});
@override
State<SubTabBarView> createState() => _SubTabBarViewState();
}
class _SubTabBarViewState extends State<SubTabBarView> with SingleTickerProviderStateMixin {
late TabController? _tabController;
int _currentTab = 0;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 2,
initialIndex: 0,
vsync: this,
animationDuration: Duration.zero,
);
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.settings), text: 'Settings'),
],
onTap: (value) {
setState(() {
_currentTab = value;
});
},
),
_currentTab == 0
/// 첫 번째 탭이 선택됐을 때
? Container(
color: Colors.yellow,
height: 600,
child: const Center(child: Text('Home Screen', style: TextStyle(fontSize: 24))),
)
/// 두 번째 탭이 선택됐을 때
: Container(
color: Colors.green,
height: 400,
child: const Center(child: Text('Settings Screen', style: TextStyle(fontSize: 24))),
),
],
);
}
}
그럼 이제 여기서 TabBar 부분만 분리하여 header에 넣고, 콘텐츠 부분만 분리하여 sliver에 넣으면 되겠다.
...
SliverStickyHeader(
header: Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.settings), text: 'Settings'),
],
onTap: (value) {
setState(() {
_currentTab = value;
});
},
),
),
sliver: SliverToBoxAdapter(
child: _currentTab == 0
/// 첫 번째 탭이 선택됐을 때
? Container(
color: Colors.yellow,
height: 600,
child: const Center(child: Text('Home Screen', style: TextStyle(fontSize: 24))),
)
/// 두 번째 탭이 선택됐을 때
: Container(
color: Colors.green,
height: 400,
child: const Center(child: Text('Settings Screen', style: TextStyle(fontSize: 24))),
),
),
)
...
위젯 파일 따로 만들면 변수로 currentTab을 넘겨주고 넘겨 받아야할텐데, 그렇게 만들기 귀찮아서 그냥 Screen 파일에 코드 붙여넣기 했다. (여기에서 새삼 느끼는 Provider의 편리함)
TabBar 배경색이 스크롤하고 보니 투명한 색이길래 Container로 한 번 감싸준 거 외에 코드 수정은 없다.
'개발노트 > Flutter' 카테고리의 다른 글
Android 13 이상을 타겟팅하는 모든 개발자는 앱에서 광고 ID를 사용하는지 여부를 Google Play에 알려야 합니다. (0) | 2025.02.04 |
---|---|
Flutter 특정 버전으로 업데이트 하기 (0) | 2025.01.16 |
MVVM Clean Architecture (+Provider) (2) | 2024.11.26 |
Flutter에서 Firebase Authentication으로 이메일 로그인 구현하기 (0) | 2024.11.24 |
Flutter에서 로컬 DB 구현하기 (SQFlite) (0) | 2024.11.23 |