일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- study book
- Computer Architecture
- bloc
- system hacking
- 영상처리
- llm을 활용 단어장 앱 개발일지
- PCA
- C++
- Kaggle
- BAEKJOON
- pytorch
- BFS
- Algorithm
- 백준
- Stream
- ARM
- MDP
- 파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습
- Dreamhack
- Flutter
- BOF
- FastAPI
- ML
- fastapi를 사용한 파이썬 웹 개발
- rao
- Got
- Widget
- DART
- Image Processing
- MATLAB
- Today
- Total
Bull
[Flutter::State] Bloc Repository의 역할 (with FastAPI) 본문
[Flutter::State] Bloc Repository의 역할 (with FastAPI)
Bull_ 2024. 8. 15. 19:37* 정확하지 않고 이해한 바탕으로 쉽게 설명하기 위해 대략적으로 적었으니 틀린 내용이 있을 수 있습니다.
Bloc 간단 브리핑
Bloc Pattern은 Flutter에서 상태관리 패키지 중 하나로, [bloc, event, state] 클래스를 나누어 세부적으로 각각 역할이 주어진다. event는 말 그대로 이벤트를 처리하는 기능을 구현하는 클래스이다. state는 상태를 나타내는 클래스라고 할 수 있다. 그러면 bloc은? bloc은 이 event와 state를 전체적으로 관리해주는 역할을 한다.
// event
abstract class BlocEvent {}
class BlocEvent1 extends BlocEvent {}
class BlocEvent2 extends BlocEvent {}
// ...
//state
abstract class BlocState {}
class BlocState1 extends BlocState {}
class BlocState2 extends BlocState {}
// ...
// bloc
class Bloc1 extends Bloc<BlocEvent, BlocState> {
Bloc1() : super() {
on<BlocEvent1>(Event1);
on<BlocEvent2>(Event2);
// ...
}
void Event1(BlocEvent1 event, Emitter<BlocState> emit) {
emit(BlocState1);
}
void Event1(BlocEvent2 event, Emitter<BlocState> emit) {
emit(BlocState2);
}
}
bloc을 [blac, event, state]로 나눌 때 파일로 나누는 게 좋지만 이해를 위해 위의 코드처럼 나타내었다. 하나의 단위에 대해서 여러 state와 event가 존재하기 때문에 여러 개가 있다는 것을 나타내기 위해서 2개정도 적어보았다.
Bloc1
class 를 보면 Bloc
class 를 상속받는데 제네릭으로 event와 state를 받는 것을 알 수 있다. 생성자를 통해 다시 on
메소드를 사용해서 이벤트를 정의한다. 그 이벤트 메소드는 아래 메소드 정의를 통해서 다시 정의할 수 있다. 이때 emit
메소드는 return은 아니지만 비슷한 느낌으로 생각해도 좋을 것 같다. state에 정의해준 부분을 호출할 수 있다.
Bloc Repository
플러터에서 상태관리는 저 세 부분으로 나뉜 클래스를 정의하여 사용할 수 있다. 그런데 Repository? 왜 필요한 지 궁금했다. 그래서 실제로 실습을 통해 이해해볼 필요가 있다고 생각했다.
What is a BLoC repository?
The repository layer is a wrapper around one or more data providers with which the Bloc Layer communicates.
정의만 봐서는 잘 모르겠다. 통신, bloc 계층, 데이터 제공. 느낌만 받아진다. 먼저 내가 이해하는 개념으로 말하자면 Bloc에서 Repository는 말 그대로 저장소 개념이다. 보통 API 호출이나 DB를 접근할 때 사용한다고 한다. 깊이있는 설명은 못하겠지만 이해 위주로 학습하고 싶었다. 그래서 실제로 FastAPI를 통해서 간단한 데이터를 불러오는 실습을 해보겠다.
Bloc Repository 실습
데이터는 이렇다. user 아이디를 누르면 api를 통해서 user 아이디에 대한 비밀번호도 같이 가져오는 예제이다. 인증은 아니지만 AuthRepository
를 실습하겠다.
"user1": {"username": "user1", "password": "password1"},
"user2": {"username": "user2", "password": "password2"},
"user3": {"username": "user3", "password": "password3"},
FastAPI 설정
FastAPI로 간단하게 GET
요청을 할 수 있는 서버를 로컬에서 켜준다. 파일은 간단하게 진행할 것이니 Flutter의 프로젝트에 fastapi_server/main.py
를 생성하고 실행한다. FastAPI 코드는 다음과 같다.
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/users/{username}")
async def get_user(username: str):
users_db = {
"user1": {"username": "user1", "password": "password1"},
"user2": {"username": "user2", "password": "password2"},
"user3": {"username": "user3", "password": "password3"},
}
user = users_db.get(username)
if user:
return user
else:
raise HTTPException(status_code=404, detail="User not found")
# 패키지 설치
pip install fastapi uvicorn
# 실행
uvicorn main:app --reload
* 실행할 때 main.py가 들어있는 부분에서 실행해야한다.
Flutter 디렉터리 구조
flutter_app/
│
├── lib/
│ ├── main.dart
│ ├── blocs/
│ │ └── auth_bloc.dart
│ ├── repositories/
│ │ └── auth_repository.dart
│ └── pages/
│ └── user_page.dart
├── fastapi_server/
└── main.py
└── pubspec.yaml
Bloc AuthRepository 실습
// filename: puspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.6
http: ^1.2.2
// filename: main.dart
import 'package:flutter/material.dart';
import '../pages/user_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: UserPage(),
);
}
}
기본 설정은 다음과 같다. UserPage
는 이후에 정의할 것이니 일단 StatelessWidget
으로 대략적으로 생성만 해놓는다. 이제 bloc을 설명하겠다.
AuthEvent
// filename: auth_bloc.dart
abstract class AuthEvent {}
class FetchUserEvent extends AuthEvent {
final String username;
FetchUserEvent(this.username);
}
main에서 button을 눌렀을 때 api 요청을 위해 user 아이디를 전달해주는 event이다. 규모가 크면 AuthEvent
에 대한 여러 기능을 하는 메소드가 있을테니 상속을 받고 FetchUserEvent
클래스로 정의해준다.
AuthState
// filename: auth_bloc.dart
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthLoaded extends AuthState {
final Map<String, String> user;
AuthLoaded(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
event와 마찬가지이다. AuthState
도 규모에 따라 여러 상태를 나타낼 수 있다. AuthInitial
, AuthLoading
상태는 정의만 하고 기능은 구현하지 않았다. 하지만 state 를 통해 위젯을 나타낼 것이기 때문에 미리 정의해준다.
AuthLoaded
는 api호출을 통해 데이터를 성공적으로 가져왔을 때 state의 user 프로퍼티를 초기화해준다.
AuthError
는 에러가 났을 때 message 프로퍼티에 초기화해준다.
AuthBloc
// filename: auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository authRepository;
// 생성자를 통해 on 메소드로 정의. 이 때 위에 정의한 레파지토리를 초기화
AuthBloc(this.authRepository) : super(AuthInitial()) {
on<FetchUserEvent>(_onFetchUserEvent);
}
// api 호출을 통해 user 데이터를 가져온다. emit을 통해 상태를 나타내고 아규먼트로 값을 넣어 상태를 관리해준다.
void _onFetchUserEvent(FetchUserEvent event, Emitter<AuthState> emit) async {
// 비동기를 통해 데이터를 가져오는 동안 로딩 상태로 나타내줌.
emit(AuthLoading());
try {
// 데이터를 가져온다.
final user = await authRepository.fetchUser(event.username);
// 비동기가 완료되면 아규먼트로 user 데이터를 넣어서 상태를 초기화해주고 상태를 AuthLoaded로 변환.
emit(AuthLoaded(user));
} catch (e) {
emit(AuthError('Failed to load user'));
}
}
}
AuthBloc
은 event와 state를 통해서 전체적인 Bloc을 관리해준다. 이제 여기서 AuthRepository
를 불러오는 기능도 구현해볼 것이다. AuthBloc
는 관리를 하는 클래스인데 이때 생성차를 통해 AuthRepository
를 초기화하는 것은 다른 Bloc 패턴에서도 이렇게 사용한다.
전체 코드(auth_bloc.dart)
// filename: auth_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../repositories/auth_repository.dart';
abstract class AuthEvent {}
class FetchUserEvent extends AuthEvent {
final String username;
FetchUserEvent(this.username);
}
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthLoaded extends AuthState {
final Map<String, String> user;
AuthLoaded(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository authRepository;
AuthBloc(this.authRepository) : super(AuthInitial()) {
on<FetchUserEvent>(_onFetchUserEvent);
}
void _onFetchUserEvent(FetchUserEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final user = await authRepository.fetchUser(event.username);
emit(AuthLoaded(user));
} catch (e) {
emit(AuthError('Failed to load user'));
}
}
}
AuthRepository
// filename: auth_repository.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
class AuthRepository {
final String baseUrl;
AuthRepository({this.baseUrl = 'http://127.0.0.1:8000'});
Future<Map<String, String>> fetchUser(String username) async {
final response = await http.get(Uri.parse('$baseUrl/users/$username'));
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
if (response.statusCode == 200) {
try {
return Map<String, String>.from(json.decode(response.body));
} catch (e) {
print('Failed to decode JSON: $e');
throw Exception('Failed to decode user data');
}
} else {
throw Exception('Failed to load user');
}
}
}
AuthRepository
는 처음에 말했던 것과 같이 API 호출기능을 할 수 있는 구현부이다. 우선 AuthRepository
생성자를 통해 baseUrl 프로퍼티를 초기화 해준다. 이후 http 패키지를 통해서 주소를 통해 데이터를 가져올 수 있다. 이전에 FastAPI를 통해 서버를 켜놓았다. 이 때 IP주소나 포트번호가 동일해야하니 확인해야한다.
fetchUser
메소드는 해시형태의 값을 비동기 처리로 반환해야 한다. 이제 http.get 메소드로 입력했던 username을 통해 users/$username
디렉터리가 있는 지 확인하고 딕셔너리(json) 형태의 데이터를 가져온다. 여기에 적힌 username은 이전에 플러터에서 적은 username을 가져와주는 event를 구현했었다.
즉 final user = await authRepository.fetchUser(event.username);
에서 FetchUserEvent
를 통해 가져온 username을 아규먼트로 넘겨줄 수 있는 것이다.
UserPage
// filename: user_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/auth_bloc.dart';
import '../repositories/auth_repository.dart';
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: BlocProvider(
create: (context) => AuthBloc(AuthRepository()),
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
(... CODE ...)
},
),
),
);
}
}
BlocProvider
는 bloc을 생성해주는 역할을 한다. create 프로퍼티를 통해서 위의 코드와 같이 AuthBloc(AuthRepository())
로 생성할 수 있다. 생성한 Bloc을 사용할 수 있게 BlocBuilder
를 통해 context와 state 를 전달해서 위젯과 같이 사용할 수 있도록 builder 프로퍼티에 등록해준다.
// filename: user_page.dart
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthInitial) {
return _buildInitialInput(context);
} else if (state is AuthLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is AuthLoaded) {
return Column(
children: [
_buildUserDisplay(state.user),
_buildInitialInput(context),
],
);
} else if (state is AuthError) {
return _buildInitialInput(context, error: state.message);
}
return Container();
},
)
바로 BlocBuilder에 생략되었던 코드들이다. 여기서는 간단하다. 우리가 _onFetchUserEvent
메소드에서 emit
메소드를 사용함으로써 state의 bloc의 상태를 바로바로 알 수 있다. 이 메소드에서 처음에는 emit(AuthLoading())
로 로딩상태를 나타내었고 emit(AuthLoaded(user))
를 통해 데이터 가져오기 완료상태를 나타내었다. 이제 위젯은 이 상태가 업데이트된 것을 BlocBuilder
가 감지하여 위젯도 새로 렌더링해주는 것이다.
분기문은 대충 이해되었으니 _buildInitialInput
와 _buildUserDisplay
를 이해해보자. 사실 이 두 위젯은 복잡함을 줄이기 위해 리팩터한 것이고 디자인은 생략하고 어떤 기능을 하는 지 살펴보자.
Widget _buildInitialInput(BuildContext context, {String error = ''}) {
final TextEditingController _usernameController = TextEditingController();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _usernameController,
decoration: InputDecoration(labelText: 'Username'),
),
SizedBox(height: 16),
if (error.isNotEmpty)
Text(error, style: TextStyle(color: Colors.red)),
ElevatedButton(
onPressed: () {
final username = _usernameController.text;
BlocProvider.of<AuthBloc>(context).add(FetchUserEvent(username));
},
child: Text('Fetch User'),
),
],
),
);
}
디자인은 건너 뛰면 컨트롤러와 onPressed
만 보면된다. 이 위젯은 TextField
이므로 적힌 텍스트를 관리할 수 있다. 그때 .text
메소드로 우리가 적은 텍스트를 가져올 수 있는데 username 변수에 저장해준다. 그 다음은 우리가 적은 이름을 가져와주는 event를 정의한 FetchUserEvent
이벤트를 사용한다.
이것을 BlocProvider.of<AuthBloc>(context).add(FetchUserEvent(username))
처럼 add
메소드로 이벤트를 발생시켜준다.
Widget _buildUserDisplay(Map<String, String> user) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Username: ${user['username']}'),
Text('Password: ${user['password']}'),
],
),
);
}
_buildUserDisplay
메소드는 불러온 상태를 Text 위젯으로 화면에 띄어주고 파라미터로 user가 사용되었다. 이 파라미터는 어떤 아규먼트를 받아서 나타낸 것일까?
_buildUserDisplay(state.user)
로 아규먼트가 전달됐다. state는? builder: (context, state)
프로퍼티로 전달되었다. BlocBuilder
클래스의 프로퍼티이다.
최종적으로 state란 단어는 여기가 마지막이지만 사실 BlocProvider
로 생성된 state 이다. 이 때 BlocProvider
를 통해 AuthBloc
이 관리되고 있으니 FetchUserEvent
인스턴스는 생성하지 않았지만 동일 시 되는 인스턴스를 생각할 수 있을 것이다.
즉 FetchUserEvent
에 대한 event로 만들어진 인스턴스가 있다고 하면 AuthBloc
의 메소드인
_onFetchUserEvent
에서 이벤트의 프로퍼티에 접근을 해도 문제가 되지 않는 것이다. 이것이 Bloc을 복잡하지만 간편하게 사용할 수 있는 이유가 아닐까?
전체 코드(user_page.dart)
// filename: user_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/auth_bloc.dart';
import '../repositories/auth_repository.dart';
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: BlocProvider(
create: (context) => AuthBloc(AuthRepository()),
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthInitial) {
return _buildInitialInput(context);
} else if (state is AuthLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is AuthLoaded) {
return Column(
children: [
_buildUserDisplay(state.user),
_buildInitialInput(context),
],
);
} else if (state is AuthError) {
return _buildInitialInput(context, error: state.message);
}
return Container();
},
),
),
);
}
Widget _buildInitialInput(BuildContext context, {String error = ''}) {
final TextEditingController _usernameController = TextEditingController();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _usernameController,
decoration: InputDecoration(labelText: 'Username'),
),
SizedBox(height: 16),
if (error.isNotEmpty)
Text(error, style: TextStyle(color: Colors.red)),
ElevatedButton(
onPressed: () {
final username = _usernameController.text;
BlocProvider.of<AuthBloc>(context).add(FetchUserEvent(username));
},
child: Text('Fetch User'),
),
],
),
);
}
Widget _buildUserDisplay(Map<String, String> user) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Username: ${user['username']}'),
Text('Password: ${user['password']}'),
],
),
);
}
}
'Software Framework > Flutter' 카테고리의 다른 글
[Flutter::News] LG TV webOS 기반 애플리케이션 Flutter 추진 요약 (0) | 2024.08.20 |
---|---|
[Flutter] 플러터에서 마크다운 적용하기! (0) | 2024.08.18 |
[Flutter::Widget] 토글 스위치...? 토글 버튼? (ToggleButtons class) (0) | 2024.08.14 |
[Flutter::Widget] Drawer icon 변경 방법! (0) | 2024.08.14 |
[Flutter::Widget] Textfield Widget size 조절 방법! (0) | 2024.08.14 |