[031] 플러터 (Flutter) 배우기 - 상태 관리1 (BloC 패턴 적용)

2023. 2. 3. 14:54모바일어플개발/Flutter

반응형

안녕하세요~ totally 개발자입니다.

 

BLoC 패턴

 

오늘은 State Management(상태 관리) 방법 중 하나인 BLoC 패턴에 대해 살펴보도록 하겠습니다. BLoC 패턴은 추후 살펴볼 Provider 패턴보다는 다소 복잡할 수 있지만 유지 보수에 있어서는 매우 유용한 방법 중 하나입니다. 

 

BLoC 이해

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']);
}
view raw album.dart hosted with ❤ by GitHub

 

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));
}
}
}
view raw albums.dart hosted with ❤ by GitHub

 

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();
}
}
view raw album_bloc.dart hosted with ❤ by GitHub

 

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,
);
}
},
),
),
);
}
}
view raw album_view.dart hosted with ❤ by GitHub

 

만약 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

 

[유튜브 강좌 영상]

 

https://youtu.be/8Vik3S8lVG8

 

반응형