이번 포스팅에서는 앞선 포스팅의 연장선, rxdart를 이용하여 나만의 BottomNavigationBar를 만들어보자.
화면 구성할 때 가장 자주 사용하는 scaffold 상위 위젯에 사용을 하고 scaffold의 매개변수 bottomNavigationBar에 직접 만들 것이다.
혹시 rxdart에 대해서 모른다면 나의 앞선 포스팅 두 개를 읽어보고 오면 좋다.
1. ReactiveX가 무엇일까?
2. rxdart가 무엇일까?
버전 정보
Flutter 2.10.5 • channel stable • https://github.com/flutter/flutter.git
Tools • Dart 2.16.2 • DevTools 2.9.2
null safety
pubspec.yaml
environment:
sdk: ">=2.16.2 <3.0.0"
...
dependencies:
rxdart: ^0.27.3
...
파일구성
lib
|--rx_index_controller.dart
|--home1.dart
|--home2.dart
|--home3.dart
|--main
목표 결과
플러터에 내장되어있는 BottomNavigationBar-class를 그대로 사용해도 되지만, 간혹 내장된 걸 지원하지 않는데 필요한 경우가 종종 있다. 그럴 때 커스터마이징 해서 사용하면 좋다.
rx_index_controller.dart
rx_index_controller.dart에서는 index가 변경되는 것을 제어할 것이다. 주요 기능은 다음과 같다.
- bottom index
- BehaviorSubject선언 및 Observe시작
- BehaviorSubject Stream을 getter로 넘겨줌
- addListener 설정
변수 선언
class RxIndexController {
late int bottomIndex;
late BehaviorSubject _subject;
Function()? addListener;
...
- bottomIndex : 현재 index값을 저장한다.
- _subject : bottomIndex를 stream으로 관찰하는 subject.
- addListener : bottomIndex가 업데이트될 때마다 실행하는 함수이다.
처음에는 위에서 언급한 주요 기능과 관련된 변수들을 선언한다. 변수 앞에 late은 null safety를 위해서 붙인 것이다. null인 상태로 변수가 사용되지는 않지만, 처음에 선언할 때는 null이고, 추후 선언을 해준다는 뜻으로 붙인다. Function() 뒤에 붙은 '?'는 null이 될 수 있다는 뜻이다.
생성자
//Constructor(생성자)
RxIndexController({int initIndex = 0, this.addListener}) {
bottomIndex = initIndex;
addListener ??= () {};
_subject = BehaviorSubject.seeded(bottomIndex);
}
- bottomIndex : initIndex로 저장을 하되, initIndex는 값이 들어오지 않을 경우, 0으로 설정.
- addListener : addListener가 null이라면 (){}로 설정
- _subject... : bottomIndex값을 BehaviorSubject으로 관찰하고, 초기값은 bottonIndex의 값으로 정한다.
생성자에서 late로 선언한 변수들을 맞춰준다. 그리고 bottomIndex를 관찰한다.
매소드
//streambuilder에서 stream에 연결될 부분
Stream get stream => _subject.stream;
jumpToIndex(int index) {
if (index != bottomIndex) {
bottomIndex = index;
_subject.sink.add(bottomIndex);
addListener!();
}
}
- Steram get... : BehaviorSubject로 선언된 subject를 stream으로 반환한다.
- jumpToIndex(index) : index값이 변경되었을 경우, subject를 변경한다. 그리고 addListener가 설정된 경우 실행한다.
addListener!()에서! 는 이 값이 null이 되지 않는다는 것을 나타내는 것으로, nullable 한 값에 적어주는 경우이다. nullable한 데이터가 생성자 통해서 혹은 다른 방법으로 null이 되지 않을 경우에! 를 써준다. 만약 null이 될 수 있다면,! 자리에?를 써주면 된다.
전체 코드
import 'package:rxdart/rxdart.dart';
class RxIndexController {
late int bottomIndex;
late BehaviorSubject _subject;
Function()? addListener;
//Constructor(생성자)
RxIndexController({int initIndex = 0, this.addListener}) {
bottomIndex = initIndex;
addListener ??= () {};
_subject = BehaviorSubject.seeded(bottomIndex);
}
//streambuilder에서 stream에 연결될 부분
Stream get stream => _subject.stream;
jumpToIndex(int index) {
bottomIndex = index;
_subject.sink.add(bottomIndex);
addListener!();
}
}
main.dart
initState
late RxIndexController rxIndexController;
@override
void initState() {
rxIndexController = RxIndexController(addListener: () {
log("Add Listener");
});
super.initState();
}
- addListener : index 값이 변경될 때마다 "Add Listener"가 출력이 된다
Build
@override
Widget build(BuildContext context) {
//StreamBuilder의 type이 Object이므로,
//dynamic으로 변경하여 rxIndexController에 맞게 type을 dynamic으로바꾼다.
return StreamBuilder<dynamic>(
//rxcontroller stream선언
stream: rxIndexController.stream,
//streambuilder를 첫 로드할때 사용되는 초기값.
initialData: rxIndexController.bottomIndex,
builder: (context, snapshot) {
//snapshot.data는 subject의 현재 값을 나타낸다. 따라서 currentIndex가 된다.
int currentIndex = snapshot.data;
return Scaffold(
//각각에 해당되는 Widget
body: bodyList[currentIndex],
bottomNavigationBar: SizedBox(
//BottomNavigationBar의 높이
height: 50,
//핸드폰 너비
width: MediaQuery.of(context).size.width,
child: Container(
//가운데 정렬
alignment: Alignment.center,
//scaffold의 body부분과 구분되는 선
//const로 rebuild되지않게하여 효율을 높인다.
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Color(0xffdddddd)))),
child: Row(
//Row의 Children에 있는 Widget들 간의 간격이 일정하게 배치한다.
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
...bottomDesign.asMap().entries.map((e) {
int index = e.key;
BottomNavigationData data = e.value;
return GestureDetector(
onTap: () => indexChanged(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
data.iconData,
//만약 현재index와 wiget의 index와 같다면
color: currentIndex == index
? data.selectedColor
: data.defaultColor,
),
Text(
data.title,
style: TextStyle(
color: currentIndex == index
? data.selectedColor
: data.defaultColor,
fontSize: 10),
)
]),
);
}).toList()
],
),
)),
);
},
);
}
build부분은 어렵지 않으므로 차근차근 읽어보기 바란다.
전체 코드
import 'dart:developer';
import 'package:custom_bottomnavigationbar/CustomNavigationbar/rx_index_controller.dart';
import 'package:custom_bottomnavigationbar/home1.dart';
import 'package:custom_bottomnavigationbar/home2.dart';
import 'package:custom_bottomnavigationbar/home3.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late RxIndexController rxIndexController;
@override
void initState() {
rxIndexController = RxIndexController(addListener: () {
log("Add Listener");
});
super.initState();
}
@override
void dispose() {
super.dispose();
}
//index화면 위젯
final bodyList = [
const Home1(),
const Home2(),
const Home3(),
];
//index 아이콘 및 타이틀
final bottomDesign = [
BottomNavigationData(
iconData: Icons.home,
defaultColor: Colors.grey,
selectedColor: Colors.red,
title: "HOME"),
BottomNavigationData(
iconData: Icons.business,
defaultColor: Colors.grey,
selectedColor: Colors.orange,
title: "BUSINESS"),
BottomNavigationData(
iconData: Icons.school,
defaultColor: Colors.grey,
selectedColor: Colors.blue,
title: "SCHOOL")
];
//BottomNavigationBar의 index 클릭 시 호출되는 함수
indexChanged(int index) => rxIndexController.jumpToIndex(index);
@override
Widget build(BuildContext context) {
//StreamBuilder의 type이 Object이므로,
//dynamic으로 변경하여 rxIndexController에 맞게 type을 dynamic으로바꾼다.
return StreamBuilder<dynamic>(
//rxcontroller stream선언
stream: rxIndexController.stream,
//streambuilder를 첫 로드할때 사용되는 초기값.
initialData: rxIndexController.bottomIndex,
builder: (context, snapshot) {
//snapshot.data는 subject의 현재 값을 나타낸다. 따라서 currentIndex가 된다.
int currentIndex = snapshot.data;
return Scaffold(
//각각에 해당되는 Widget
body: bodyList[currentIndex],
bottomNavigationBar: SizedBox(
//BottomNavigationBar의 높이
height: 50,
//핸드폰 너비
width: MediaQuery.of(context).size.width,
child: Container(
//가운데 정렬
alignment: Alignment.center,
//scaffold의 body부분과 구분되는 선
//const로 rebuild되지않게하여 효율을 높인다.
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Color(0xffdddddd)))),
child: Row(
//Row의 Children에 있는 Widget들 간의 간격이 일정하게 배치한다.
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
...bottomDesign.asMap().entries.map((e) {
int index = e.key;
BottomNavigationData data = e.value;
return GestureDetector(
onTap: () => indexChanged(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
data.iconData,
//만약 현재index와 wiget의 index와 같다면
color: currentIndex == index
? data.selectedColor
: data.defaultColor,
),
Text(
data.title,
style: TextStyle(
color: currentIndex == index
? data.selectedColor
: data.defaultColor,
fontSize: 10),
)
]),
);
}).toList()
],
),
)),
);
},
);
}
}
//BottonNavigationbar의 Object
class BottomNavigationData {
final IconData iconData;
final String title;
final Color selectedColor;
final Color defaultColor;
BottomNavigationData(
{required this.iconData,
required this.defaultColor,
required this.selectedColor,
required this.title});
}
Home1.dart
전체 코드
import 'package:flutter/material.dart';
class Home1 extends StatelessWidget {
const Home1({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
);
}
}
Home1, Home2, Home3 모두 동일한 코드이다.
결과
Github : https://github.com/monocsp/My_Flutter_References/tree/main/custom_bottomnavigationbar
오류, 지적사항 그리고 궁금한 것은 댓글 부탁드립니다.
'Study > Dart,Flutter' 카테고리의 다른 글
13. Flutter[플러터] 안드로이드 화면 띄우기 methodchannel invokeMethod(2) (2) | 2022.06.25 |
---|---|
12. Flutter[플러터] 안드로이드 화면띄우기 method channel invokeMethod(1) (0) | 2022.06.18 |
9. Flutter 상태관리법 Riverpod 0.14.0 그리고 firebase (2, ChangeNotifier) (0) | 2021.11.11 |
8. Dart, Flutter 상태관리 그리고 Riverpod (1) (4) | 2021.10.28 |
7. StatefulWidget의 LifeCycle(생명주기) 그리고 setState (0) | 2021.08.30 |