2023. 2. 3. 14:54ㆍ모바일어플개발/Flutter
안녕하세요~ totally 개발자입니다.
BLoC 패턴
오늘은 State Management(상태 관리) 방법 중 하나인 BLoC 패턴에 대해 살펴보도록 하겠습니다. BLoC 패턴은 추후 살펴볼 Provider 패턴보다는 다소 복잡할 수 있지만 유지 보수에 있어서는 매우 유용한 방법 중 하나입니다.

BLoC 개념을 보자면 UI Screen(View), BLOC(Presenter, ViewModel), Data Layer 부분인 Repository(Data Handler), Provider(Data Provider)가 존재합니다.
Provider (Data Provider) | 데이터 제공 및 수집, 데이터 처리 |
Repository (Data Handler) | Data Provider에서 제공 받은 데이터를 필터링 등 변형하여 BLOC에게 데이터를 제공 |
BLOC (Presenter, ViewModel) | Business Logic을 담은 패키지로, 화면에 적용시킬 수 있도록 stream을 통해 add하여 화면에 반영할 수 있도록 함 |
UI Screen (View) | initState로 초기 데이터를 먼저 받아오고 추후 stream을 통해 계속 데이터를 갱신해서 가져올 수 있음 |
위 개념을 기반으로 하여 실습으로 들어가보도록 하겠습니다. https://jsonplaceholder.typicode.com/albums 이 예제 json url를 사용하여 데이터를 불러오고 화면에 출력해보도록 하겠습니다.
Step 1: 다음처럼 4개의 폴더를 만들어주고 각각 파일들을 만들어줍니다. (bloc, data_provider, model, repository) model 폴더 내에는 album.dart와 albums.dart 파일들을 각각 만들어주었는데 그 이유는 albums를 앨범 한 개가 아닌 여러 개의 앨범 즉 리스트로 가져올 것이기 때문에 albums.dart도 선언해주었습니다.

Step 2: pubspec.yaml에 이 실습에 필요한 rxdart 패키지와 http 패키지를 추가해주시면 됩니다.

Step 3: 먼저 model 내에 파일들을 아래처럼 지정해주면 됩니다.
album.dart
class Album { | |
int? userId; | |
int? id; | |
String? title; | |
Album({this.userId, this.id, this.title}); | |
factory Album.fromJSON(Map<String, dynamic> json) => | |
Album(userId: json['userId'], id: json['id'], title: json['title']); | |
} |
albums.dart
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)); | |
} | |
} | |
} |
Step 4: data_provider 내에 api_provider.dart 파일을 아래처럼 작성합니다
import 'dart:convert'; | |
import 'package:bloc_project/model/albums.dart'; | |
import 'package:http/http.dart' show Client; | |
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 to fetch data.'); | |
} | |
} | |
} |
Step 5: repository 폴더 내에 album_repository.dart 파일을 아래처럼 작성합니다.
import '../data_provider/api_provider.dart'; | |
import '../model/albums.dart'; | |
class AlbumRepository { | |
final AlbumApiProvider _albumApiProvider = AlbumApiProvider(); | |
Future<Albums> fetchAllAlbums() async => _albumApiProvider.fetchAlbumList(); | |
} |
Step 6: bloc 폴더 내에 album_bloc.dart도 아래처럼 작성합니다.
import '../model/albums.dart'; | |
import '../repository/album_repository.dart'; | |
import 'package:rxdart/rxdart.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(); | |
} | |
} |
Step 7: 마지막으로 view의 album_view.dart에서 StreamBuilder까지 완료해주시면 됩니다.
import '../bloc/album_bloc.dart'; | |
import '../model/albums.dart'; | |
import 'package:flutter/material.dart'; | |
class AlbumView extends StatefulWidget { | |
const AlbumView({super.key}); | |
@override | |
State<AlbumView> createState() => _AlbumViewState(); | |
} | |
class _AlbumViewState extends State<AlbumView> { | |
final AlbumBloc _albumBloc = AlbumBloc(); | |
@override | |
void initState() { | |
_albumBloc.fetchAllAlbums(); | |
super.initState(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
child: Scaffold( | |
appBar: AppBar( | |
title: const Text("앨범 리스트"), | |
), | |
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 CircularProgressIndicator( | |
strokeWidth: 2, | |
); | |
} | |
}, | |
), | |
), | |
); | |
} | |
} |
만약 StreamBuilder내 변수 부분에 오류 메시지 출력되시는 분들은 Null ? 관련일 수 있으니 ? 붙여보시면서 오류 메시지가 없어지는지 확인하시면 됩니다. (32, 34, 41, 42번째 줄)

Step 9: 실행해본 모습입니다.

References
https://pub.dev/packages/rxdart
https://adamnain.medium.com/flutter-tips-fetching-data-from-the-api-using-bloc-8289debfbf40
https://dhruvnakum.xyz/flutter-bloc-v8-how-to-fetch-data-from-an-api-2022-guide
[유튜브 강좌 영상]
'모바일어플개발 > Flutter' 카테고리의 다른 글
[033] 플러터 (Flutter) 배우기 - 상태 관리3 (get_it 사용) (0) | 2023.02.05 |
---|---|
[032] 플러터 (Flutter) 배우기 - 상태 관리2 (Provider 사용) (0) | 2023.02.04 |
[030] 플러터 (Flutter) 배우기 - 아이폰 Face ID(페이스아이디,얼굴인식 구현하기) (0) | 2023.02.02 |
[029] 플러터 (Flutter) 배우기 - RefreshIndicator (아래로 스와이프하여 새로고침) + FutureBuilder, Dio (0) | 2023.01.31 |
[028] 플러터 (Flutter) 배우기 - 애니메이션 만들기2: 카드 뒤집기 (TweenAnimationBuilder, Transform 사용) (0) | 2023.01.30 |