반응형
Overview
Bottom Navigation Bar 위젯을 이용하여 화면 전환을 하는 앱을 만들어봅니다.
이 포스팅에서는 바텀 내비게이션 바의 사용법만을 설명하는 포스팅입니다.
다른 설명이 필요하면 아래의 링크를 참고해주세요.
- 01. Flutter - ListView, Card, Navigator - 스크롤 가능한 목록 표시, 목록 선택시 특정 목록 내용 보여주기
- 02. Flutter - StatefulWidget 활용 - 실시간 검색 기능 구현하기
포스팅에서 다루는 예시 프로젝트는 아래의 깃허브 링크에서 다운로드 받을 수 있습니다.
'4_add_bottom_navigation_bar_example' 폴더를 확인해주세요.
BottomNavigationBar 란?
- 화면 하단에 내비게이션 바를 제공해주는 위젯
- 앱 하단에서 특정 작업을 수행할 수 있도록 표시
- 일반적으로 페이지 전환으로 많이 사용
속성
- items
- 내비게이션 바의 각 버튼을 정의
- currentIndex
- 현재 선택된 버튼의 인덱스
- onTap
- 버튼을 누르면 호출되는 콜백 함수
- backgroundColor
- 내비게이션 바의 배경색
- elevation
- 내비게이션 바의 그림자
- type
- 내비게이션 바의 종류
- fixed : 내비게이션 바의 모든 항목을 표시
- shifting : 선택한 항목의 레이블과 아이콘만 표시
- showSelectedLabels
- 선택한 항목의 레이블을 표시할지 여부를 결정
- 부울 값
- showUnselectedLabels
- 선택하지 않은 항목의 레이블을 표시할지 여부를 결정
- 부울 값
설계 및 코딩
요구사항
- 하단의 내비게이션 바 구현
- 구현된 바텀 내비게이션 바를 터치하면 화면 전환이 이루어져야 함
- 페이지는 메모, 커뮤니티, 내 정보 총 3개의 페이지로 구성
- 단, 커뮤니티, 내 정보 페이지는 임시 페이지로써 글자만 출력하게 끔 페이지 생성
설계
- 기존의 하나만 있던 페이지(메모)를 여러 페이지로 분리하고 이동하기 하나의 안내 페이지 생성
- 즉, 내비게이션 바의 경로를 지정할 안내 페이지 (class : MyAppPage)
- 메모 항목을 보여주던 페이지를 메모 페이지로 이름 변경 (class : MyMemoPage)
- 바텀 내비게이션 바의 다른 항목을 보여줄 예시 페이지 2개 작성
- 커뮤니티 페이지, 내 정보 페이지 (임시로 텍스트만 있는 페이지)
- 연결할 페이지들을 리스트<위젯> 으로 정의
- 이 포스팅에서의 설명하는 변수명 : List<Widget> _navIndex
- 연결할 페이지들이 있는 위젯 리스트를 본문(body)에 인덱스를 기준으로 페이지 표시
- body: _navIndex.elementAt(인덱스) : 인덱스에 해당하는 페이지 표시
- 바텀 내비게이션 바 항목의 인덱스를 저장하고 상태를 변경하는 함수 정의
- 이 포스팅에서의 설명하는 함수명 : _onNavTapped
- 상태 변경 : setState(() { 보여줄 인덱스 = 누른 항목의 인덱스; });}
- 바텀 내비게이션 바를 누를 경우, 누른 항목의 인덱스의 페이지를 호출하는 함수 호출
- onTap : _onNavTapped
코딩 (구현)
- 메인 함수
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MemoApp',
home: MyAppPage(),
);
}
}
- 바텀 내비게이션의 연결을 위한 페이지 (메인 함수와 연결되어 있는 페이지)
더보기
// 기본 홈
class MyAppPage extends StatefulWidget {
const MyAppPage({super.key});
@override
State<MyAppPage> createState() => MyAppState();
}
class MyAppState extends State<MyAppPage> {
// 바텀 네비게이션 바 인덱스
int _selectedIndex = 0;
final List<Widget> _navIndex = [
MyMemoPage(),
CommunityPage(),
MyInfoPage(),
];
void _onNavTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _navIndex.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
fixedColor: Colors.blue,
unselectedItemColor: Colors.blueGrey,
showUnselectedLabels: true,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.my_library_books_outlined),
label: '메모',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.chat_bubble_2),
label: '커뮤니티',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
label: '내 정보',
),
],
currentIndex: _selectedIndex,
onTap: _onNavTapped,
),
);
}
}
- 메모 페이지 (바텀 내비게이션 바의 인덱스 0)
더보기
// 메모 페이지
// 앱의 상태를 변경해야하므로 StatefulWidget 상속
class MyMemoPage extends StatefulWidget {
const MyMemoPage({super.key});
@override
MyMemoState createState() => MyMemoState();
}
class MyMemoState extends State<MyMemoPage> {
// 검색어
String searchText = '';
// 리스트뷰에 표시할 내용
final List<String> items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
final List<String> itemContents = [
'Item 1 Contents',
'Item 2 Contents',
'Item 3 Contents',
'Item 4 Contents'
];
// 플로팅 액션 버튼을 이용하여 항목을 추가할 제목과 내용
final TextEditingController titleController = TextEditingController();
final TextEditingController contentController = TextEditingController();
// 리스트뷰 카드 클릭 이벤트
void cardClickEvent(BuildContext context, int index) {
String content = itemContents[index];
Navigator.push(
context,
MaterialPageRoute(
// 정의한 ContentPage의 폼 호출
builder: (context) => ContentPage(content: content),
),
);
}
// 플로팅 액션 버튼 클릭 이벤트
Future<void> addItemEvent(BuildContext context) {
// 다이얼로그 폼 열기
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('항목 추가하기'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: '제목',
),
),
TextField(
controller: contentController,
decoration: InputDecoration(
labelText: '내용',
),
),
],
),
actions: <Widget>[
TextButton(
child: Text('취소'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('추가'),
onPressed: () {
setState(() {
String title = titleController.text;
String content = contentController.text;
items.add(title);
itemContents.add(content);
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Bottom Navigation Bar Example'), // 앱 상단바 설정
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20.0),
child: TextField(
decoration: InputDecoration(
hintText: '검색어를 입력해주세요.',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {
searchText = value;
});
},
),
),
Expanded(
child: ListView.builder(
// items 변수에 저장되어 있는 모든 값 출력
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
// 검색 기능, 검색어가 있을 경우
if (searchText.isNotEmpty &&
!items[index]
.toLowerCase()
.contains(searchText.toLowerCase())) {
return SizedBox.shrink();
}
// 검색어가 없을 경우, 모든 항목 표시
else {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.elliptical(20, 20))),
child: ListTile(
title: Text(items[index]),
onTap: () => cardClickEvent(context, index),
),
);
}
},
),
),
],
),
// 플로팅 액션 버튼
floatingActionButton: FloatingActionButton(
onPressed: () => addItemEvent(context), // 버튼을 누를 경우
tooltip: 'Add Item', // 플로팅 액션 버튼 설명
child: Icon(Icons.add), // + 모양 아이콘
),
);
}
}
// 선택한 항목의 내용을 보여주는 추가 페이지
class ContentPage extends StatelessWidget {
final String content;
const ContentPage({Key? key, required this.content}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Content'),
),
body: Center(
child: Text(content),
),
);
}
}
- 커뮤니티 페이지, 내 정보 페이지 (빈 페이지, 각각 바텀 내비게이션 바의 인덱스 1, 2)
더보기
// 커뮤니티 페이지
class CommunityPage extends StatefulWidget {
const CommunityPage({super.key});
@override
State<CommunityPage> createState() => CommunityState();
}
class CommunityState extends State<CommunityPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'community page',
),
),
);
}
}
// 내 정보 페이지
class MyInfoPage extends StatelessWidget {
const MyInfoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'my info page',
),
),
);
}
}
전체 소스 코드
더보기
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MemoApp',
home: MyAppPage(),
);
}
}
// 기본 홈
class MyAppPage extends StatefulWidget {
const MyAppPage({super.key});
@override
State<MyAppPage> createState() => MyAppState();
}
class MyAppState extends State<MyAppPage> {
// 바텀 네비게이션 바 인덱스
int _selectedIndex = 0;
final List<Widget> _navIndex = [
MyMemoPage(),
CommunityPage(),
MyInfoPage(),
];
void _onNavTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _navIndex.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
fixedColor: Colors.blue,
unselectedItemColor: Colors.blueGrey,
showUnselectedLabels: true,
type: BottomNavigationBarType.fixed,
items: const [
// BottomNavigationBarItem(
// icon: Icon(Icons.home_filled),
// label: '홈',
// backgroundColor: Colors.white,
// ),
BottomNavigationBarItem(
icon: Icon(Icons.my_library_books_outlined),
label: '메모',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.chat_bubble_2),
label: '커뮤니티',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
label: '내 정보',
),
],
currentIndex: _selectedIndex,
onTap: _onNavTapped,
),
);
}
}
// 메모 페이지
// 앱의 상태를 변경해야하므로 StatefulWidget 상속
class MyMemoPage extends StatefulWidget {
const MyMemoPage({super.key});
@override
MyMemoState createState() => MyMemoState();
}
class MyMemoState extends State<MyMemoPage> {
// 검색어
String searchText = '';
// 리스트뷰에 표시할 내용
final List<String> items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
final List<String> itemContents = [
'Item 1 Contents',
'Item 2 Contents',
'Item 3 Contents',
'Item 4 Contents'
];
// 플로팅 액션 버튼을 이용하여 항목을 추가할 제목과 내용
final TextEditingController titleController = TextEditingController();
final TextEditingController contentController = TextEditingController();
// 리스트뷰 카드 클릭 이벤트
void cardClickEvent(BuildContext context, int index) {
String content = itemContents[index];
Navigator.push(
context,
MaterialPageRoute(
// 정의한 ContentPage의 폼 호출
builder: (context) => ContentPage(content: content),
),
);
}
// 플로팅 액션 버튼 클릭 이벤트
Future<void> addItemEvent(BuildContext context) {
// 다이얼로그 폼 열기
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('항목 추가하기'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: '제목',
),
),
TextField(
controller: contentController,
decoration: InputDecoration(
labelText: '내용',
),
),
],
),
actions: <Widget>[
TextButton(
child: Text('취소'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('추가'),
onPressed: () {
setState(() {
String title = titleController.text;
String content = contentController.text;
items.add(title);
itemContents.add(content);
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Bottom Navigation Bar Example'), // 앱 상단바 설정
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20.0),
child: TextField(
decoration: InputDecoration(
hintText: '검색어를 입력해주세요.',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {
searchText = value;
});
},
),
),
Expanded(
child: ListView.builder(
// items 변수에 저장되어 있는 모든 값 출력
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
// 검색 기능, 검색어가 있을 경우
if (searchText.isNotEmpty &&
!items[index]
.toLowerCase()
.contains(searchText.toLowerCase())) {
return SizedBox.shrink();
}
// 검색어가 없을 경우, 모든 항목 표시
else {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.elliptical(20, 20))),
child: ListTile(
title: Text(items[index]),
onTap: () => cardClickEvent(context, index),
),
);
}
},
),
),
],
),
// 플로팅 액션 버튼
floatingActionButton: FloatingActionButton(
onPressed: () => addItemEvent(context), // 버튼을 누를 경우
tooltip: 'Add Item', // 플로팅 액션 버튼 설명
child: Icon(Icons.add), // + 모양 아이콘
),
);
}
}
// 선택한 항목의 내용을 보여주는 추가 페이지
class ContentPage extends StatelessWidget {
final String content;
const ContentPage({Key? key, required this.content}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Content'),
),
body: Center(
child: Text(content),
),
);
}
}
// 커뮤니티 페이지
class CommunityPage extends StatefulWidget {
const CommunityPage({super.key});
@override
State<CommunityPage> createState() => CommunityState();
}
class CommunityState extends State<CommunityPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'community page',
),
),
);
}
}
// 내 정보 페이지
class MyInfoPage extends StatelessWidget {
const MyInfoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'my info page',
),
),
);
}
}
참고
반응형