Study/Dart,Flutter

11. flutter Rx를 이용하여 BottomNavigationBar 만들기

코딩 잘 할거얌:) 2022. 4. 24. 23:19
반응형

이번 포스팅에서는 앞선 포스팅의 연장선, rxdart를 이용하여 나만의 BottomNavigationBar를 만들어보자.

 

화면 구성할 때 가장 자주 사용하는 scaffold 상위 위젯에 사용을 하고 scaffold의 매개변수 bottomNavigationBar에 직접 만들 것이다.

천천히 하면 잘 될거에요 (출처 : 독립일기)

 

혹시 rxdart에 대해서 모른다면 나의 앞선 포스팅 두 개를 읽어보고 오면 좋다.

 

 

1. ReactiveX가 무엇일까?

https://pcseob.tistory.com/40

 

2. Reactive Programming

오늘은 Reactive Programming에 대해서 알아보도록 하자. Reactive Programming이란 Reactive programming is programming with asynchronous data streams. 비동기 데이터스트림을 이용한 프로그래밍하는 것을..

pcseob.tistory.com

 

2. rxdart가 무엇일까?

https://pcseob.tistory.com/49

 

10. Reactive programming, rxdart

이번 포스팅은 Reactive Programming이 적용된 RxDart와 그걸 Flutter에 적용해보도록 하겠다. Reactive Programming이란, 비동기 데이터스트림을 이용한 프로그래밍하는 것을 말한다. 혹시 자세한 것을 알고싶

pcseob.tistory.com

 


버전 정보

 

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


목표 결과

 

출처 :&nbsp;https://pub.dev/documentation/base/latest/flutter_material_bottom_navigation_bar/BottomNavigationBar-class.html

 

플러터에 내장되어있는 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


 

 

오류, 지적사항 그리고 궁금한 것은 댓글 부탁드립니다.

728x90