반응형

BLoC(Business Logic Component) 패턴은 Flutter 애플리케이션에서 비즈니스 로직을 UI 코드와 분리하는 데 사용되는 설계 패턴입니다.

이 패턴은 StreamSink를 활용하여 데이터의 흐름을 관리하고, 상태 관리를 효율적으로 수행할 수 있게 합니다.

BLoC 패턴은 Flutter의 공식 상태 관리 솔루션 중 하나로, 재사용성과 테스트 가능성을 높이는 데 기여합니다.

 

 

주요 개념

 

1. BLoC (Business Logic Component):

비즈니스 로직을 담당하는 컴포넌트입니다. Stream을 통해 UI에 데이터를 전달하고, Sink를 통해 UI로부터 이벤트를 받습니다.

2. Stream:

비동기 데이터의 흐름을 나타내는 Dart의 클래스입니다. BLoC 패턴에서는 상태 변경을 UI로 전달하는 데 사용됩니다.

3. Sink:

데이터를 Stream에 전달하는 입력 통로입니다. BLoC 패턴에서는 UI에서 발생한 이벤트를 BLoC로 전달하는 데 사용됩니다.

4. Event:

UI에서 발생하는 사용자 상호작용을 나타냅니다. 버튼 클릭, 폼 입력 등 다양한 이벤트가 포함됩니다.

5. State:

UI에서 표시할 데이터를 나타냅니다. BLoC는 상태를 관리하고 Stream을 통해 UI로 전달합니다.

 

BLoC 패턴의 구성 요소

 

1. Event 클래스:

사용자가 수행하는 동작을 정의합니다.

2. State 클래스:

애플리케이션의 상태를 정의합니다.

3. BLoC 클래스:

이벤트를 처리하고 상태를 관리하는 로직을 포함합니다.

 

예제

 

간단한 카운터 애플리케이션을 BLoC 패턴으로 구현한 예제를 살펴보겠습니다.

 

Event 클래스

abstract class CounterEvent {}

class Increment extends CounterEvent {}

class Decrement extends CounterEvent {}

 

State 클래스

class CounterState {
  final int count;

  CounterState(this.count);
}

 

BLoC 클래스

import 'dart:async';

class CounterBloc {
  final _stateController = StreamController<CounterState>();
  StreamSink<CounterState> get _inCounter => _stateController.sink;
  Stream<CounterState> get counter => _stateController.stream;

  final _eventController = StreamController<CounterEvent>();
  Sink<CounterEvent> get counterEventSink => _eventController.sink;

  CounterBloc() {
    _eventController.stream.listen(_mapEventToState);
  }

  void _mapEventToState(CounterEvent event) {
    if (event is Increment) {
      _inCounter.add(CounterState(_stateController.stream.value.count + 1));
    } else if (event is Decrement) {
      _inCounter.add(CounterState(_stateController.stream.value.count - 1));
    }
  }

  void dispose() {
    _stateController.close();
    _eventController.close();
  }
}

 

UI 코드

import 'package:flutter/material.dart';
import 'counter_bloc.dart'; // BLoC 파일을 가져옴

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterPage(),
    );
  }
}

class CounterPage extends StatelessWidget {
  final CounterBloc _bloc = CounterBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BLoC Counter Example'),
      ),
      body: Center(
        child: StreamBuilder<CounterState>(
          stream: _bloc.counter,
          initialData: CounterState(0),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return CircularProgressIndicator();
            }
            return Text(
              'Counter: ${snapshot.data.count}',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              _bloc.counterEventSink.add(Increment());
            },
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () {
              _bloc.counterEventSink.add(Decrement());
            },
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

 

예시(실습)

 

 

패키지 추가

rxdart:
http:



model 부터 생성해줍니다.

class Album {
  int? userId;
  int? id;
  String? title;

  Album({this.userId, this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}
import './album.dart';

class Albums {
  late List<Album> albums;

  Albums({required this.albums});

  Albums.fromJSON(List<dynamic> json) {
    albums = List<Album>.empty(growable: true);
    for (dynamic val in json) {
      albums.add(Album.fromJson(val));
    }
  }
}

 

provider 를 생성해줍니다.

import 'dart:convert';

import 'package:http/http.dart' show Client;
import 'package:flutter_lecture/model/albums.dart';


class AlbumApiProvider {
  Client client = Client();
  
  Future<Albums> fetchAlbumList() async {
    final response = await client
        .get(Uri.parse("https://jsonplaceholder.typicode.com/albums"));

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return Albums.fromJSON(data);
    } else {
      throw Exception("Failed");
    }
  }
}

 

repository 를 생성합니다.

import 'package:flutter_lecture/data_provider/api_provider.dart';
import 'package:flutter_lecture/model/albums.dart';

class AlbumRepository {

  final AlbumApiProvider _albumApiProvider = AlbumApiProvider();

  Future<Albums> fetchAllAlbums() async => _albumApiProvider.fetchAlbumList();
  
}

 

bloc 파일을 만들어줍니다. 

import 'package:flutter_lecture/repository/ablum_repository.dart';
import 'package:rxdart/rxdart.dart';

import '../model/albums.dart';

class AlbumBloc {
  final AlbumRepository _albumRepository = AlbumRepository();
  final PublishSubject<Albums> _albumFetcher = PublishSubject<Albums>();

  Stream<Albums> get allAlbums => _albumFetcher.stream;

  Future<void> fetchAllAlbums() async {
    Albums albums = await _albumRepository.fetchAllAlbums();
    _albumFetcher.sink.add(albums);
  }

  dispose() {
    _albumFetcher.close();
  }
}

 

bloc 파일을 적용할 view 파일을 마지막으로 만들어줍니다.

import 'package:flutter/material.dart';
import 'package:flutter_lecture/model/albums.dart';
import 'package:flutter_lecture/bloc/album_bloc.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final AlbumBloc _albumBloc = AlbumBloc();


  @override
  void initState() {
    _albumBloc.fetchAllAlbums();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("test title"),
      ),
      body: StreamBuilder<Albums>(
        stream: _albumBloc.allAlbums,
        builder: (context, snapshot) {
          if(snapshot.hasData) {
            Albums? albumList = snapshot.data;
            return ListView.builder(
              itemCount: albumList?.albums.length,
              itemBuilder: (context, index) {
                return Container(
                  padding: const EdgeInsets.all(10),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text("ID: ${albumList?.albums[index].id.toString()}"),
                      Text("Title: ${albumList?.albums[index].title}" )
                    ],
                  ),
                );
              }
            );
          } else if(snapshot.hasError){
              return Center(
                child: Text(snapshot.error.toString()),
              );

          } else {
            return const Center(
              child: CircularProgressIndicator(
                strokeWidth: 2,
              ),
            );
          }
        },
      ),
    );
  }
}

 

 

BLoC 패턴의 장점

 

1. UI와 비즈니스 로직의 분리:

UI 코드와 비즈니스 로직을 명확히 분리하여 코드의 가독성과 유지보수성을 높입니다.

2. 재사용성과 테스트 용이성:

BLoC 컴포넌트는 독립적으로 동작하므로 다른 프로젝트나 화면에서 재사용할 수 있습니다. 또한, 비즈니스 로직이 UI와 분리되어 있어 단위 테스트가 용이합니다.

3. 비동기 데이터 처리:

StreamSink를 사용하여 비동기 데이터 처리를 효과적으로 관리할 수 있습니다.

 

참고 문서

 

Bloc 패턴 공식 문서

Flutter의 상태 관리

Dart의 Stream 클래스 문서

 

이 정보를 통해 Flutter 애플리케이션에서 BLoC 패턴을 효과적으로 사용하여 상태 관리를 수행할 수 있습니다. BLoC 패턴을 활용하면 코드의 구조를 개선하고 유지보수성을 높일 수 있습니다.

반응형

'프론트엔드 > Flutter' 카테고리의 다른 글

flutter get_it 패턴  (1) 2024.07.22
flutter provider 패턴  (1) 2024.07.22
flutter 리프레쉬 인디케이터  (0) 2024.07.22
dart 반복문 정리  (0) 2024.07.21
dart 조건문 정리  (0) 2024.07.21

+ Recent posts