Never give up

Flutter - Bloc의 이해 with stream 본문

해왔던 삽질..

Flutter - Bloc의 이해 with stream

대기만성 개발자 2021. 2. 28. 20:39
반응형

필자가 bloc을 이해하는 과정에서 조금 고생과 삽질을 해서

 

이 부분을 조금 더 수월하게 접근하실 수 있도록 Streambuilder와 bloc을 통해 예제를 만들어봤습니다

 

먼저 StreamController를 이용해 bloc형식과 유사하게 구현을 한 후

 

bloc 패키지를 이용해 카운터 예제를 구현할 예정입니다

 

Event class

abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

Bloc class

class CounterBloc {
  CounterBloc() {
    counterEventController.stream.listen(_mapEventToState);
  }

  int _counter = 0;

  final _counterStateController = StreamController<int>();

  Stream<int> get counter => _counterStateController.stream; // output

  final counterEventController = StreamController<CounterEvent>();

  void _mapEventToState(CounterEvent event) {
    if (event is IncrementEvent) {
      _counter++;
    } else {
      _counter--;
    }

    _counterStateController.add(_counter);
  }

  void dispose() {
    counterEventController.close();
    _counterStateController.close();
  }
}

Main

class StreamBlocExample extends StatefulWidget {
  @override
  _StreamBlocExampleState createState() => _StreamBlocExampleState();
}

class _StreamBlocExampleState extends State<StreamBlocExample> {
  final _bloc = CounterBloc();

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Bloc example')),
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          StreamBuilder<int>(
              stream: _bloc.counter,
              initialData: 0,
              builder: (context, snapshot) {
                return Center(child: Text('Counted number : ${snapshot.data}'));
              }),
          SizedBox(height: 40),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              CircleAvatar(
                  backgroundColor: Colors.red,
                  child: FlatButton(
                      padding: EdgeInsets.all(4),
                      child: Icon(Icons.remove, color: Colors.white),
                      onPressed: () =>
                          _bloc.counterEventSink.add(DecrementEvent()))),
              CircleAvatar(
                  backgroundColor: Colors.blue,
                  child: FlatButton(
                      padding: EdgeInsets.all(4),
                      child: Icon(Icons.add, color: Colors.white),
                      onPressed: () =>
                          _bloc.counterEventSink.add(IncrementEvent()))),
            ],
          )
        ]));
  }
}

실제 bloc에서는 model과 state, bloc을 분리합니다만 위 부분에서는 하지 않았습니다

 

먼저 bloc은 eventstate로 이뤄져있고 이벤트(입력)에 따라 상태(출력)값이 변화가 됩니다

 

이 부분을 먼저 잘 기억하시고 따라오시면 됩니다

 

입력 이벤트를 만든 후 bloc에서 input과 output을 해줄 StreamController를 정의해주고

 

Constructor에서 stream.listen을 통해 입력에 따른 출력을 처리해줍니다

 

StreamController는 자동으로 close가 되지않기 때문에 dispose에 호출할 메소드도 정의해줍니다

 

메인에서 bloc 클래스를 인스턴스화 하는과정에서 listen이 처리가 되고

 

버튼을 누를 때 eventController의 event에 따라 더하거나 빼고

 

이 과정을 StreamBuilder를 통해 출력을 해줍니다

 

stream이나 추상클래스가 익숙하지 않다면 조금 헷갈릴 수 있으나

 

이 부분을 알면 크게 어렵지 않은 예제입니다

 

 

이제 실제로 bloc 패키지를 써서 동일한 과정을 처리해보겠습니다

 

NumberState

class NumberState {
  int num = 0;

  NumberState(this.num);

  NumberState update(int value) => NumberState(value); //이 부분을 통해 업데이트가 됩니다
}

Event

abstract class BlocEvent {}

class Increment extends BlocEvent {}

class Decrement extends BlocEvent {}

Bloc

class CounterBloc extends Bloc<BlocEvent, NumberState> {
  CounterBloc() : super(NumberState(0));

  @override
  Stream<NumberState> mapEventToState(BlocEvent event) async* {
    if (event is Increment) {
      yield* _mapIncrement();
    } else if (event is Decrement) {
      yield* _mapDecrement();
    }
  }

  Stream<NumberState> _mapIncrement() async* {
    yield state.update(state.num + 1);
  }

  Stream<NumberState> _mapDecrement() async* {
    yield state.update(state.num - 1);
  }
}

Main

class BlocMain extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(create: (_) => CounterBloc(), child: BlocExample()),
    );
  }
}

class BlocExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Bloc example')),
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          BlocBuilder<CounterBloc, NumberState>(builder: (context, state) {
            return Text('Counted number : ${state.num}');
          }),
          SizedBox(height: 40),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              CircleAvatar(
                  backgroundColor: Colors.red,
                  child: FlatButton(
                      padding: EdgeInsets.all(4),
                      child: Icon(Icons.remove, color: Colors.white),
                      onPressed: () =>
                          context.read<CounterBloc>().add(Decrement()))),
              CircleAvatar(
                  backgroundColor: Colors.blue,
                  child: FlatButton(
                      padding: EdgeInsets.all(4),
                      child: Icon(Icons.add, color: Colors.white),
                      onPressed: () =>
                          context.read<CounterBloc>().add(Increment()))),
            ],
          )
        ]));
  }
}

먼저 화면에 출력해줄 State부분을 정의해줍니다

(여기서 update부분 처리를 어떻게하는지 주목해주세요)

 

이벤트를 생성해주는 과정은 동일하고 Bloc부분을 보겠습니다

 

먼저 Constructor부분에서 State의 초기값을 줍니다

 

그리고 mapEventToState에서 event에 따른 출력을 설정해줍니다

 

update과정에서 return값이 NumberState인것을 꼭 확인하고 넘어가주세요

 

여기서 굳이 메소드로 처리 안해주고 yield를 써서 처리해줘도 됩니다

  @override
  Stream<NumberState> mapEventToState(BlocEvent event) async* {
    if (event is Increment) {
      yield state.update(state.num + 1);
    } else if (event is Decrement) {
      yield state.update(state.num - 1);
    }
  }

하지만 로직이 복잡해지면 메소드로 분리하는게 더 관리하기 좋습니다

 

그리고 메인으로 넘어와서 보면

 

blocbuilder에 제네릭 값으로 bloc과 state값을 넣어주면 cubit값을 따로 정의해주지 않아도 됩니다

 

그리고 builder안에서 event(입력)에 따른 state(출력)의 변화에 따라

 

값이 변화되는것을 확인할 수 있습니다

 

이벤트를 주는 방법으로는 stream.add와 동일한데

 

context.read를 통해 부른다는 부분만 확인해주면 될거같습니다

 

이 부분은 전에 짚고 넘어갔었던 extension예제를 상기해보면

(링크 : devmemory.tistory.com/44)

 

bloc패키지 안에 context extension으로 read를 만들어준걸 알 수 있겠습니다

 

 

 

-- 이하 필자의 헛소리타임(?) 시작될 예정이므로 예제만 보고싶으신 분은 스킵하시면 됩니다..

 

 

 

이전까지는 상태관리 패키지중 provider만 써왔습니다

 

먼저 provider를 선택했던 이유는 간단하고 빠른 개발 및 유지보수 또한 용이하다고 생각했는데

(사실 아무것도 모르는 상태에서 구글에서 공식적으로 밀고있는 state management방법이고 모 강의를 들으면서 배웠던게 provider라 계속해서 사용을 해왔습니다)

 

협업을 시작하게 될 때 다른 패키지와 디자인 패턴 등을 알아놓는게 좋을거 같아서

 

다른 상태관리 패키지를 적용해봤습니다

 

처음에는 핫한 getX 그리고 riverpod를 먼저 사용해봤는데

 

getX를 쓰다보니 데이터타입(rxint, rxlist 등등)도 달라지고

 

Navigator를 할 때 getTo, getOff등 제가 계속 사용해왔던 것들과 너무 다르기도 했고

 

편했던 부분도 있었지만 유지보수 할 때 더 큰 비용을 지불할수도 있겠다 라는 느낌이 들었습니다

 

그리고 Riverpod는 provider지만 다른 provider라는 문구만 보고 한번 시도해봤는데

 

이것도 hooks_riverpod라는 패키지도 같이사용해야 더 제작자의 의도에 맞출 수 있고

 

아직 완전하지 않다는 제작자의 문구도 봤을때 조금 아쉽다는 느낌을 받았습니다

 

그래서 bloc을 이해해보려고 공식홈페이지 튜토리얼을 따라해봤는데 잘 와닿지가 않더군요

 

blocProvider랑 blocBuilder는 뭐만하면 레드스크린만 띄우고..

 

< 봐도 뭐가뭔지 모르겠... >

그래서 블로그 유튜브 채널 등 찾아보면서 적용해보고

 

제가 이해한것을 토대로 글을 작성해봤고 완벽하게는 아니지만 컨셉을 이해할 수 있었습니다

 

그리고 저와 같은 경험을 하게될 분들을 위해 포스트를 작성해봤습니다

 

저야 뭐.. 삽질이 주특기니까요 하하...

< 오늘도 계속되는 삽질 >

반응형

'해왔던 삽질..' 카테고리의 다른 글

Flutter 2.0 - Callback Function  (0) 2021.03.13
공지 (feat. Flutter 2.0)  (0) 2021.03.04
Flutter - list with paging  (0) 2021.02.24
두번째 앱 등록 후기  (0) 2021.02.22
Flutter - audio plugin test  (0) 2021.02.14
Comments