![[Fultter] 커리어톡 앱 모작 / Sliver 구조 안에 커스텀 TabBar 넣기](https://image.inblog.dev?url=https%3A%2F%2Finblog.ai%2Fapi%2Fog%3Ftitle%3D%255BFultter%255D%2520%25EC%25BB%25A4%25EB%25A6%25AC%25EC%2596%25B4%25ED%2586%25A1%2520%25EC%2595%25B1%2520%25EB%25AA%25A8%25EC%259E%2591%2520%252F%2520Sliver%2520%25EA%25B5%25AC%25EC%25A1%25B0%2520%25EC%2595%2588%25EC%2597%2590%2520%25EC%25BB%25A4%25EC%258A%25A4%25ED%2585%2580%2520TabBar%2520%25EB%2584%25A3%25EA%25B8%25B0%26logoUrl%3Dhttps%253A%252F%252Finblog.ai%252Finblog_logo.png%26blogTitle%3Dclubnerdy&w=3840&q=75)
Sliver 구조 안에 있는 TabBar를 만들어보자

커리어톡의 현재 메인화면 구조는 첨부한 이미지와 같다.
가장 상단에
SliverToBoxAdaptor구조 안에 페이지가 넘어가는 배너가 있다.그 아래로
SliverPersistentHeader에 필터 메뉴들이 있다.필터 메뉴 가운데서도 탭바 구조를 하고있는 메뉴를 구현해보자.
1. 샘플링
구조를 짜기 전 해당 레이아웃에 사용되는 위젯을 찾아 샘플링해보자

import 'package:flutter/material.dart';
/// Flutter code sample for [TabBar].
void main() => runApp(const TabBarApp());
class TabBarApp extends StatelessWidget {
const TabBarApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: TabBarExample());
}
}
class TabBarExample extends StatefulWidget {
const TabBarExample({super.key});
@override
State<TabBarExample> createState() => _TabBarExampleState();
}
/// [AnimationController]s can be created with `vsync: this` because of
/// [TickerProviderStateMixin].
class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin {
late final 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(
appBar: AppBar(
title: const Text('TabBar Sample'),
bottom: TabBar(
controller: _tabController,
tabs: const <Widget>[
Tab(icon: Icon(Icons.cloud_outlined)),
Tab(icon: Icon(Icons.beach_access_sharp)),
Tab(icon: Icon(Icons.brightness_5_sharp)),
],
),
),
body: TabBarView(
controller: _tabController,
children: const <Widget>[
Center(child: Text("It's cloudy here")),
Center(child: Text("It's rainy here")),
Center(child: Text("It's sunny here")),
],
),
);
}
}
플러터 문서에서 Tabbar 위젯의 사용 방법 및 샘플 코드를 확인할 수 있다. 프로젝트에 적용하기 전 샘플코드를 실행해보고 내 프로젝트에 맞게 커스텀해보자.
1) 테스트해보기

테스트 코드 입력 시 문제 없이 동작한다.
여기서 문제가 있다면 내 모작 프로젝트의 경우에는 appbar에서 사용되는게 아닌 Sliver 구조 내에서 사용된다는 것이다.
2. Sliver 구조 안으로 적용
여기서부터는 도저히 감이 잡히지 않아 지피티의 도움을 받았다.
1) SliverPersistentHeader의 델리게이트 만들기
TabBar의 헤더를 올려둘 핀고정 헤더이다.
class _TabsHeaderDelegate extends SliverPersistentHeaderDelegate {
_TabsHeaderDelegate({required this.child, required this.height});
final Widget child;
final double height;
@override
double get minExtent => height;
@override
double get maxExtent => height;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
@override
bool shouldRebuild(covariant _TabsHeaderDelegate old) {
return old.child != child || old.height != height;
}
}2) 샘플을 Sliver 구조로 리팩터링
- AppBar에 있던
TabBar를 SliverPersistentHeader로 이동
- 본문은 NestedScrollView.body에
TabBarView
- 상단(회색)영역은 나중에
SingleChildScrollView로 바꾸기 쉽게SliverToBoxAdapter로 감싸둠
import 'package:flutter/material.dart';
class DummyPage extends StatelessWidget {
const DummyPage({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: TabBarExample());
}
}
class TabBarExample extends StatefulWidget {
const TabBarExample({super.key});
@override
State<TabBarExample> createState() => _TabBarExampleState();
}
class _TabBarExampleState extends State<TabBarExample>
with TickerProviderStateMixin {
late final TabController _tabController;
final tabs = const [
Tab(text: '🔥 인기급상승'),
Tab(text: '채용소식'),
Tab(text: '기업정보'),
Tab(text: '직무정보'),
// 필요시 더 추가
];
@override
void initState() {
super.initState();
_tabController = TabController(length: tabs.length, vsync: this, initialIndex: 0);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const activeColor = Colors.black;
final inactiveColor = Colors.grey.shade500;
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
// (회색) 상단 컨텐츠: 나중에 SingleChildScrollView로 갈아끼우기 쉬움
SliverToBoxAdapter(
child: Container(
height: 160,
color: Colors.grey.shade300,
alignment: Alignment.center,
child: const Text('여기 상단 헤더(배너/필터 등)'),
),
),
// 탭바가 들어가는 핀고정 헤더
SliverPersistentHeader(
pinned: true,
delegate: _TabsHeaderDelegate(
height: 44,
child: Material(
color: Colors.white,
child: TabBar(
controller: _tabController,
isScrollable: true,
tabs: tabs,
labelColor: activeColor,
unselectedLabelColor: inactiveColor,
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500),
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(width: 3, color: Colors.black),
insets: EdgeInsets.symmetric(horizontal: 14),
),
indicatorSize: TabBarIndicatorSize.label,
labelPadding: const EdgeInsets.symmetric(horizontal: 12),
splashFactory: NoSplash.splashFactory,
overlayColor: WidgetStatePropertyAll(Colors.transparent),
),
),
),
),
],
// 탭별 콘텐츠
body: TabBarView(
controller: _tabController,
children: [
_buildList('인기급상승'),
_buildList('채용소식'),
_buildList('기업정보'),
_buildList('직무정보'),
],
),
),
);
}
// 샘플 콘텐츠(탭 안 리스트)
Widget _buildList(String title) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: 30,
itemBuilder: (_, i) => ListTile(title: Text('$title - Item $i')),
separatorBuilder: (_, __) => const Divider(height: 1),
);
}
}
⚠️ TabBar 사용 시 index 0번 앞에 발생하는 여백문제

샘플링 코드에 내 코드를 적용했을 때, 첫 탭에 앞에 알 수 없는 여백이 발생했다. 공식 문서를 뒤져보았으나 원인을 찾기 어려웠고 같은 문제를 겪어 인프런에 질문을 올린 글을 찾을 수 있었다.

TabBar의 속성 중 tabAlignment을 통해 이를 해결할 수 있다는 해답도 발견했고 내 프로젝트에 적용해보았다. 적용 후 완성 본은 다음과 같다.

Share article