오늘하루도 우힣ㅎ

Flutter) BLoC으로 counter 만들기 본문

flutter/BLoC

Flutter) BLoC으로 counter 만들기

우힣 2020. 1. 18. 00:37

앞의 포스팅에서는 BLoC에 대한 정보를 알아 보았다. 이 포스팅에서는 플러터 프로젝트 처음생성시 만들어지는 couter를 BLoC으로 만들어보려 한다.

1. 파일 구조

  •    파일 구조는 lib folder 아래에 bloc 폴더를 만들고 그안에 필요한 bloc floder를 만든다. 프로젝트가 거대해지면 그만큼 많은 수의 bloc들이 생기게 될것이고 그것을 쉽게 관리하기 위해 bloc별로 폴더를 만들어 관리하는 것이 더 쉽다고 느꼈다.
  •    UI부분과 BLoC부분을 나누어 주는데 확실한 구분점들 두기 위해 나는 UI부분들은 Page 폴더를 만들어 모아 두는 것을 선호한다.
  •    해당 예제는 main.dart하나에서 모두 관리가 가능하기에 따로 나누지는 않았다.

 

2. main.dart 분석

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }//setSate를 통하여 counter가 늘어나는것을 제어 하도록 구현이 되어 있다.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,//버튼을 누르게 되면 _incrementCounter함수가 실행이 되게 된다.
        tooltip: 'Increment',
        child: Icon(Icons.add),//현재는 애드를 위한 버튼 만이 존재한다.
      ), 
    );
  }
}
  •  main.dart 페이지에서는 현재 setSate()로 state를 관리하게 되어 있는데 이를 모두 bloc을 통해 제어할수 있도록 바꾸어 주어야 한다.
  • bloc으로 모두 제어하기 위해서는 먼저 필요한 state, event 그리로 bloc을 정의 하여야 한다.
    • state는 사용자들에게 연산의 결과를 보여줄 count만 있으면 충분하다.
    • event의 경우 더하기 버튼을 눌렀을때, 빼기 버튼을 눌렀을때를 정의하는 event가 필요하게 된다.
    • bloc의 경우 event를 input으로 받아 state를 아웃풋으로 전달해준다고 생각할수 있다. 그렇기 때문에 더하기 버튼, 빼기 버튼에 해당하는 bloc들이 각각 필요 하게 된다.

3. BLoC forder 보기 

  • bloc.dart : 이 파일은 나중 import의 편의를 위해 만들어 준 파일이다.
  • counter_bloc.dart : counter_bloc.dart의 경우 사용하게될 bloc들을 모두 모아둔 파일이다.
  • counter_event.dart : event들을 정의해 놓은 파일이다.
  • counter_state.dart : 사용하게 될 state를 정의해 놓은 파일이다.

 

4. counter_state.dart

import 'package:meta/meta.dart';

@immutable
class CounterState {
  final int count;//이것이 counter를 위해 사용되어질 state이다.

  CounterState({@required this.count});

  factory CounterState.empty() {
    return CounterState(count: 0);
  }//state는 초기화 과정이 필요하게 되는데 이를 위한 것이다.

  CounterState update({
    int count,
  }) {
    return copyWith(count: count);
  } //bloc에서의 state업데이트를 위해 사용하게 되는 것이다.

  CounterState copyWith({
    int count,
  }) {
    return CounterState(count: count ?? this.count);
  }
}
  • state의 경우 항상 초기화 과정이 필요하고, bloc에서의 output은 state인데 이를 위해서 사용 되어지는 것이 update라고 보면 된다.

5. counter_event.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class CounterEvent extends Equatable {
  CounterEvent([List props = const []]) : super(props);
}

class PageLoaded extends CounterEvent{
  @override
  String toString() {
    return 'page loaded';
  }
}//제일 처음 페이지가 불리게 되면 실행될 작업을 위해 선언한 이벤트

class IncrementBtnPressed extends CounterEvent{
  @override
  String toString() {
    return "Increment button pressed";
  }
}//증가 버튼을 누르게되면 발생할 event

class DecrementBtnPressed extends CounterEvent{
  @override
  String toString() {
    return "Decrement button pressed";
  }
}//감소 버튼을 누르게 되면 발생할 event

6. counter_bloc.dart

import 'package:bloc/bloc.dart';

import 'counter_event.dart';
import 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  @override
  // TODO: implement initialState
  CounterState get initialState => CounterState.empty();//state의 초기화를 위해 사용이 되어진다.
  
  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    if (event is PageLoaded) {
      yield* _mapPageLodaedToState();
    } else if (event is IncrementBtnPressed) {
      yield* _mapIncrementBtnPressedToState();
    } else if (event is DecrementBtnPressed) {
      yield* _mapDecrementBtnPressedToState();
    }
  }//각각 이벤트에 따라 실행이 되게될 bloc들을 if문을 통해 분류하여 준다. 

  Stream<CounterState> _mapPageLodaedToState() async* {
    yield state.update(count: 0);
  }//처음 페이지가 불리게 되면 사용되어진다.

  Stream<CounterState> _mapIncrementBtnPressedToState() async* {
    yield state.update(count: state.count + 1);
  }//증가 버튼을 부르게 되면 사용되어진다

  Stream<CounterState> _mapDecrementBtnPressedToState() async* {
    yield state.update(count: state.count - 1);
  }//감소 버튼을 부르게 되면 사용되어진다.
}
  • 각각의 블럭들은 if 문을 통해 어떤 event에 따라 불리게 될지 결정되어 지게 된다.
  • bloc은 event를 input()으로 받아 적절한 조치를 취한후 state를 반환하게 된다. 이때 state 반환을 위해 yield를 사용한다.

7. bloc.dart

export 'counter_bloc.dart';
export 'counter_event.dart';
export 'counter_state.dart';
  • bloc에 필요한 모든 파일을 export시켜 이파일만 import하여도 bloc관련 파일들이 모두 import 될수 있도록 한다.

8. main.dart

  • 현재 main.dart의 코드는 (2.main.dart 분석 참고) add를 위한 button 밖에 없으며, BLoC가 전혀 적용이 되지 않은 상태이다.
  • 먼저 감소를 위한 버튼을 먼저 만들어 주도록 한다.
//----------------------------------BEFORE-------------------------------------
floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,//버튼을 누르게 되면 _incrementCounter함수가 실행이 되게 된다.
        tooltip: 'Increment',
        child: Icon(Icons.add),//현재는 애드를 위한 버튼 만이 존재한다.
      ), 
//-----------------------------------AFTER-------------------------------------

floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      //store btn
      floatingActionButton: Container(
        child: Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(20),
              child: FloatingActionButton(
                child: Icon(Icons.remove),
                onPressed: () {},
              ),
            ),
            Spacer(
              flex: 1,
            ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {},
              ),
            ),
          ],
        ),
      ),

이처럼 두개의 floating button이 생긴것을 볼수가 있다.

  •  다음은 bloc과 연결을 해주는 작업이 필요하다 이를 위해서 제일 먼저 해야 할 작업은 flutter_bloc:^2.0.0을 pubspec.yaml 파일에 추가하고 해당 library를 import하는 작업이다.
main.dart파일에 해당 library를 import
  •  그 후의 작업은 bloc을 초기화 하는 작업과 동시에 empty 를 통해 state를 초기화 하는 작업이 필요하게 된다. 
  •  그와 동시에 setState와 main.dart에서 선언된 _counter variable을 사용하지 않을 것이기 때문에 지우도록 하겠다.
//----------------------------------BEFORE-------------------------------------
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }//setSate를 통하여 counter가 늘어나는것을 제어 하도록 구현이 되어 있다.

  @override
  Widget build(BuildContext context) {
  ...
  }
 }
//-----------------------------------AFTER-------------------------------------
class _MyHomePageState extends State<MyHomePage> {
  CounterBloc _counterBloc;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _counterBloc = CounterBloc();
  }
  
  @override
  Widget build(BuildContext context) {
  ...
  }
 }
  • 여기서 부터는 실질적으로 BLoC을 UI쪽과 연결시키게 되는 작업을 하게 되는데 BLoC Builder를 이용한다. BLoC과 실질적으로 연결 되는 body 부분을 BlocBuilder<Bloc,state>{bloc : , builder : (context,state{})}로 감싸게 되고 해당 bloc을 계속하여 사용 하게 된다.
  •  이때 Bloc 부분에는 자신이 사용할 bloc과 state부분에는 그 bloc에의해 결과가 산출되는 state를 적어 줄수 있도록 한다.
  •  아래는 body 부분을 BlocBuilder로 감싸준 후의 모습을 나타낸다.
//----------------------------------BEFORE-------------------------------------
...
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
        	...
        )
      ...
 }
//-----------------------------------AFTER-------------------------------------
...
@override
Widget build(BuildContext context) {
    return BlocBuilder(
        bloc: _counterBloc,
        builder: (BuildContext context, CounterState state) {
          return Scaffold(
          	...
          )
          ...
        }
        ...
    );
 }
     
  •  다음으로는 state값을 쓸수 있도록 하여 주어야 하는데 여기서 사용하는 state의 값은 count 값이다. 이것은 state.count를 통해 접근이 가능하다.
  •  counter의 값은 0으로 초기화가 되어 있는 상태이다..(counter_state.dart파일 empty에서 0으로 초기화가 된다.)
//----------------------------------BEFORE-------------------------------------
...
return Center(
	child: Column(
		mainAxisAlignment: MainAxisAlignment.center,
		children: <Widget>[
			Text('You have pushed the button this many times:',),
            Text(
                 '$_counter',//해당 _counter 변수는 더이상 사용을 하지 않는다.
                 style: Theme.of(context).textTheme.display1,),
         ],
    ),
);
...
//-----------------------------------AFTER-------------------------------------
...
return Center(
	child: Column(
		mainAxisAlignment: MainAxisAlignment.center,
		children: <Widget>[
			Text('You have pushed the button this many times:',),
            Text(
                 '${state.count}',//이것을 통해 사용자가 하는 행동에 맞는 state를 보여주게 된다.
                 style: Theme.of(context).textTheme.display1,),
         ],
    ),
);
...
  • 마지막 작업으로는 각각의 버튼들에 알맞는 event를 연결하여 주게 되면 Bloc연결 작업은 끝이나게 된다.
//-----------------------------------AFTER-------------------------------------
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      //store btn
      floatingActionButton: Container(
        child: Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(20),
              child: FloatingActionButton(
                child: Icon(Icons.remove),
                onPressed: () {},
              ),
            ),
            Spacer(
              flex: 1,
            ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {},
              ),
            ),
          ],
        ),
      ),
//-----------------------------------AFTER-------------------------------------
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      //store btn
      floatingActionButton: Container(
        child: Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(20),
              child: FloatingActionButton(
                child: Icon(Icons.remove),
                onPressed: () {
                	_counterBloc.add(DecrementBtnPressed());
                },
              ),
            ),
            Spacer(
              flex: 1,
            ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                	_counterBloc.add(IncrementBtnPressed());
                },
              ),
            ),
          ],
        ),
      ),

9. main.dart 전체 코드

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import './bloc/counter_bloc/bloc.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  CounterBloc _counterBloc;

  @override
  void initState() {
    super.initState();
    _counterBloc = CounterBloc();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
        bloc: _counterBloc,
        builder: (BuildContext context, CounterState state) {
          return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text(
                    'You have pushed the button this many times:',
                  ),
                  Text(
                    '${state.count}',
                    style: Theme.of(context).textTheme.display1,
                  ),
                ],
              ),
            ),

            floatingActionButtonLocation:
                FloatingActionButtonLocation.centerFloat,
            //store btn
            floatingActionButton: Container(
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.all(20),
                    child: FloatingActionButton(
                      child: Icon(Icons.remove),
                      onPressed: () {
                        _counterBloc.add(DecrementBtnPressed());
                      },
                    ),
                  ),
                  Spacer(
                    flex: 1,
                  ),
                  Padding(
                    padding: const EdgeInsets.all(20),
                    child: FloatingActionButton(
                      child: Icon(Icons.add),
                      onPressed: () {
                        _counterBloc.add(IncrementBtnPressed());
                        //print(state.count);
                      },
                    ),
                  ),
                ],
              ),
            ),
          );
        });
  }
}

10. 결과물

 

0보다 작아지는것을 막고 싶다면 counter_bloc.dart에서 _mapDecrementBtnPressedToState()부분을 건드리면 될것이다.!!

Comments