Flutter Application/앱 설계

Flutter 상태 관리 - Bloc pattern

sdchjjj 2024. 11. 29. 13:55
반응형

안녕하세요.

이번 포스팅에서는 Flutter app 개발 시 상태 관리를 위한 디자인 패턴 중 하나인 Bloc pattern에 대해 알아보겠습니다.

 

언어: dart

IDE: Android Studio

Framework: Flutter

Test device: Android

 

Bloc(Business Logic Component) pattern은 Flutter app에서 널리 사용되는 상태 관리 솔루션입니다. 이 패턴은 비즈니스 로직을 UI와 분리하여 명확한 역할 분리를 이루고, 앱의 테스트 및 유지보수를 좀 더 쉽게 할 수 있게 도와줍니다.


핵심 개념

  1. Event (이벤트):
    Event는 Bloc의 입력입니다. 버튼 클릭, API 요청 등 사용자 상호작용을 나타냅니다.
  2. State (상태):
    State는 Bloc의 출력입니다. 로딩 중, 데이터 로드 완료, 에러 상태 등 UI의 현재 상태를 나타냅니다.
  3. Bloc:
    Bloc은 Event를 입력으로 받아 비즈니스 로직에 따라 State를 출력하는 구성 요소입니다. 별도의 클래스로 구현할 수 있습니다.
  4. Stream (스트림):
    Bloc은 Dart의 async 패키지에 포함된 스트림을 사용하여 Event와 State를 처리합니다. Event는 입력 스트림에 추가되고 State는 출력 스트림을 통해 전달됩니다.

Bloc의 동작

  1. 사용자가 UI와 상호작용(버튼 클릭 등)하면 Event가 트리거 됩니다.
  2. 해당 Event가 Bloc에 추가됩니다.
  3. Bloc이 Event를 처리하고 새로운 State를 출력합니다.
  4. UI는 새로운 State에 따라 다시 빌드됩니다.

이번 포스팅에서는 이전에 작성했던 state hoisting 포스팅의 소스를 기반으로 예제를 만들어 보겠습니다.

https://it-of-fortune.tistory.com/48

 

Flutter app 상태 관리 - state hoisting

안녕하세요.이번 포스팅에서는 flutter app에서 상태를 관리하는 방법 중 하나인 state hoisting에 대해 간단히 알아보겠습니다. 언어: dartIDE: Android StudioFramework: FlutterTest device: AndroidState HoistingReact, Flu

it-of-fortune.tistory.com

 

*Bloc pattern 구현으로 넘어가기

 

우선, 기본 상태의 소스입니다. 현재는 state hoisting을 사용하여 상태를 관리하고 있습니다.

main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: LoadingExample(),
  ));
}

class LoadingExample extends StatefulWidget {
  @override
  _LoadingExampleState createState() => _LoadingExampleState();
}

class _LoadingExampleState extends State<LoadingExample> {
  bool _showMessage = false;
  bool _isLoading = false;

  void _setMessageVisibility() async {
    setState(() {
      _isLoading = true;
    });
    await Future.delayed(Duration(seconds: 3));
    setState(() {
      _isLoading = false;
      _showMessage = !_showMessage;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('State Hoisting Example')),
      body: childWidget(
        showMessage: _showMessage,
        isLoading: _isLoading,
        onButtonPressed: _setMessageVisibility,
      ),
    );
  }
}

class childWidget extends StatelessWidget {
  final bool showMessage;
  final bool isLoading;
  final VoidCallback onButtonPressed;

  const childWidget({
    required this.showMessage,
    required this.isLoading,
    required this.onButtonPressed,
  });

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            onPressed: onButtonPressed,
            child: Text('Show message'),
          ),
          SizedBox(height: 20),
          if (isLoading) CircularProgressIndicator(),
          if (showMessage)
            Text(
              'Hello World',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
        ],
      ),
    );
  }
}

 

여기서, 버튼을 show와 hide로 나누어주고, message와 circular progress bar 또한 나누어 주겠습니다.

 

ShowButton 위젯입니다.

main.dart

class ShowButton extends StatelessWidget {
  final VoidCallback onShowPressed;

  const ShowButton({required this.onShowPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onShowPressed,
        child: Text(
          "Show"
        )
    );
  }
}

 

HideButton 위젯입니다.

main.dart

class HideButton extends StatelessWidget {
  final VoidCallback onHidePressed;

  const HideButton({required this.onHidePressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onHidePressed,
        child: Text(
            "Hide"
        )
    );
  }
}

 

Message 위젯입니다.

main.dart

class Message extends StatelessWidget {
  final bool showMessage;

  const Message({required this.showMessage});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: showMessage
          ? Text(
        'Hello World',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
      ) : SizedBox.shrink(),
    );
  }
}

 

Loading(원형 프로그레스 바) 위젯입니다.

main.dart

class Loading extends StatelessWidget {
  final bool isLoading;

  const Loading({required this.isLoading});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: isLoading ? CircularProgressIndicator() : SizedBox.shrink()
    );
  }
}

 

이제 상위 클래스명과 세부 사항들을 수정해 주겠습니다.

main.dart

class BlocExample extends StatefulWidget {
  @override
  _BlocExampleState createState() => _BlocExampleState();
}

class _BlocExampleState extends State<BlocExample> {
  bool _showMessage = false;
  bool _isLoading = false;

  void _show() async {
    setState(() {
      _isLoading = true;
    });
    await Future.delayed(Duration(seconds: 3));
    setState(() {
      _isLoading = false;
      _showMessage = true;
    });
  }

  void _hide() async {
    setState(() {
      _isLoading = true;
    });
    await Future.delayed(Duration(seconds: 3));
    setState(() {
      _isLoading = false;
      _showMessage = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('State Hoisting Example')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ShowButton(onShowPressed: _show),
          HideButton(onHidePressed: _hide),
          Message(showMessage: _showMessage),
          Loading(isLoading: _isLoading)
        ],
      )
    );
  }
}

 

전체 코드입니다.

main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: BlocExample(),
  ));
}

class BlocExample extends StatefulWidget {
  @override
  _BlocExampleState createState() => _BlocExampleState();
}

class _BlocExampleState extends State<BlocExample> {
  bool _showMessage = false;
  bool _isLoading = false;

  void _show() async {
    setState(() {
      _isLoading = true;
    });
    await Future.delayed(Duration(seconds: 3));
    setState(() {
      _isLoading = false;
      _showMessage = true;
    });
  }

  void _hide() async {
    setState(() {
      _isLoading = true;
    });
    await Future.delayed(Duration(seconds: 3));
    setState(() {
      _isLoading = false;
      _showMessage = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('State Hoisting Example')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ShowButton(onShowPressed: _show),
          HideButton(onHidePressed: _hide),
          Message(showMessage: _showMessage),
          Loading(isLoading: _isLoading)
        ],
      )
    );
  }
}

class ShowButton extends StatelessWidget {
  final VoidCallback onShowPressed;

  const ShowButton({required this.onShowPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onShowPressed,
        child: Text(
          "Show"
        )
    );
  }
}

class HideButton extends StatelessWidget {
  final VoidCallback onHidePressed;

  const HideButton({required this.onHidePressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onHidePressed,
        child: Text(
            "Hide"
        )
    );
  }
}

class Message extends StatelessWidget {
  final bool showMessage;

  const Message({required this.showMessage});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: showMessage
          ? Text(
        'Hello World',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
      ) : SizedBox.shrink(),
    );
  }
}

class Loading extends StatelessWidget {
  final bool isLoading;

  const Loading({required this.isLoading});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: isLoading ? CircularProgressIndicator() : SizedBox.shrink()
    );
  }
}

보시는 바와 같이 상태와 관련된 코드들이 늘어났으며, 복잡해 보이는 것을 알 수 있습니다.

이제 이를 Bloc pattern을 사용하도록 수정해 보겠습니다.

 

Bloc pattern example

가장 먼저 터미널에서 명령어를 입력하여 bloc 관련 패키지를 추가해 줍니다(figure 1 참조).

flutter pub add flutter_bloc

figure 1

 

그리고 새로운 dart 파일들을 생성해 줍니다(figure 2 참조).

figure 2

 

이제 하나씩 만들어 가보겠습니다.

상태들을 추가합니다.

example_state.dart

abstract class MessageState {}

class InitialState extends MessageState {}

class LoadingState extends MessageState {}

class MessageShownState extends MessageState {}

class MessageHiddenState extends MessageState {}

 

이벤트를 추가합니다.

example_event.dart

abstract class MessageEvent {}

class ShowMessageEvent extends MessageEvent {}

class HideMessageEvent extends MessageEvent {}

 

bloc을 추가합니다.

example_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'example_event.dart';
import 'example_state.dart';

class MessageBloc extends Bloc<MessageEvent, MessageState> {
  MessageBloc() : super(InitialState()) {
    on<ShowMessageEvent>((event, emit) async {
      emit(LoadingState());
      await Future.delayed(Duration(seconds: 3));
      emit(MessageShownState());
    });
    on<HideMessageEvent>((event, emit) async {
      emit(MessageHiddenState());
    });
  }
}

event가 발생하면 그에 상응하는 동작을 수행 후 state를 발행합니다. 기존의 동작을 약간 수정해서 message가 나타나기 전에는 3초의 loading이 있지만, 사라질 때는 바로 사라지도록 구현하였습니다.

 

이제 UI 코드를 수정할 차례입니다.

 

main 함수를 아래와 같이 수정합니다.

main.dart

void main() {
  runApp(MaterialApp(
    home: BlocProvider(
      create: (context) => MessageBloc(),
        child: BlocExample()
    ),
  ));
}

 

_BlocExampleState를 지워주고, BlocExample을 StatelessWidget으로 바꿔줍니다. 이제 UI widget 내에서 state를 관리하지 않기 때문입니다.

main.dart

class BlocExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Pattern Example')),
      body: BlocBuilder<MessageBloc, MessageState>(
        builder: (context, state) {
          final isLoading = state is LoadingState;
          final showMessage = state is MessageShownState;

          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ShowButton(
                onShowPressed: () {
                  context.read<MessageBloc>().add(ShowMessageEvent());
                },
              ),
              HideButton(
                onHidePressed: () {
                  context.read<MessageBloc>().add(HideMessageEvent());
                },
              ),
              Message(showMessage: showMessage),
              Loading(isLoading: isLoading),
            ],
          );
        },
      ),
    );
  }
}

 

UI 부분의 전체 코드입니다.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:for_practice/src/example_bloc.dart';
import 'src/example_event.dart';
import 'src/example_state.dart';

void main() {
  runApp(MaterialApp(
    home: BlocProvider(
      create: (context) => MessageBloc(),
        child: BlocExample()
    ),
  ));
}

class BlocExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Pattern Example')),
      body: BlocBuilder<MessageBloc, MessageState>(
        builder: (context, state) {
          final isLoading = state is LoadingState;
          final showMessage = state is MessageShownState;

          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ShowButton(
                onShowPressed: () {
                  context.read<MessageBloc>().add(ShowMessageEvent());
                },
              ),
              HideButton(
                onHidePressed: () {
                  context.read<MessageBloc>().add(HideMessageEvent());
                },
              ),
              Message(showMessage: showMessage),
              Loading(isLoading: isLoading),
            ],
          );
        },
      ),
    );
  }
}

class ShowButton extends StatelessWidget {
  final VoidCallback onShowPressed;

  const ShowButton({required this.onShowPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onShowPressed,
        child: Text(
          "Show"
        )
    );
  }
}

class HideButton extends StatelessWidget {
  final VoidCallback onHidePressed;

  const HideButton({required this.onHidePressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onHidePressed,
        child: Text(
            "Hide"
        )
    );
  }
}

class Message extends StatelessWidget {
  final bool showMessage;

  const Message({required this.showMessage});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: showMessage
          ? Text(
        'Hello World',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
      ) : SizedBox.shrink(),
    );
  }
}

class Loading extends StatelessWidget {
  final bool isLoading;

  const Loading({required this.isLoading});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: isLoading ? CircularProgressIndicator() : SizedBox.shrink()
    );
  }
}

상태 관리 부분이 빠진 UI 코드는 훨씬 깔끔해 보이며, 알아보기도 더욱 쉬워졌습니다.

 

이제 테스트를 해보겠습니다.

result

정상적으로 동작합니다.

 

이상 포스팅을 마치겠습니다.

 

감사합니다.

728x90
반응형

'Flutter Application > 앱 설계' 카테고리의 다른 글

Flutter app 상태 관리 - state hoisting  (0) 2024.11.28