아직 진행중이지만 withTFT프로젝트 github 링크 남겨 놓겠습니다
WITHTFT 링크
이번에는 제가 사용하는 flutter bloc 패턴 세팅을 정리해보겠습니다.
부족한 부분은 댓글로 알려주시면 감사하겠습니다.
일단 폴더 구조 입니다.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
runApp(const AppPage());
}
빌드하면
WidgetsFlutterBinding.ensureInitialized(); 로 초기화 해주고
await dotenv.load(fileName: '.env'); 로 env파일을 load해줍니다.
그후 appPage로 이동합니다.
bloc을 처음 사용해보신다면
제가 전에 공부하면서 만든 bloc예제 코드 사용해보시면 이해하시기 편할꺼 같습니다.
SimpleBloc github링크
main 진입후 appPage로 이동 합니다
app/app_page.dart
class AppPage extends StatefulWidget {
const AppPage({
Key? key,
}) : super(key: key);
@override
State<AppPage> createState() => _AppPageState();
}
class _AppPageState extends State<AppPage> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LoginBloc(),
),
],
child: const AppView(),
);
}
}
여기에서 bloc을 전역으로 사용하기 위해 main으로 진입할때 bloc을 주입 시켜줍니다.
지금은 logbloc만 추가 했지만 사용하는 bloc이 늘어나면 여기서 추가해주면 전역으로 사용가능합니다.
bloc사용하면서 주입에 대한 스트레스가 어마 무시한데 이렇게 하면 그런 걱정 없이 사용가능합니다.
이어서
appView로 라우팅 되는데 사실상 여기에는 별거 없습니다.
app/app_view.dart
class AppView extends StatefulWidget {
const AppView({super.key});
@override
State<AppView> createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: LoginView(),
);
}
}
그냥 loginView로 이동하는 여기서 중요한건 MaterialApp을 설정해 놓아야 합니다
MaterialApp 설정하는 이유는 Flutter 애플리케이션의 최상위 위젯을 설정 하는것입니다.
나중에 GlobalStyle을 여기서 설정 해놓습니다.
그다음 바로 login view로 이동하는데
코드 설명은 밑에 달아 놓겠습니다.
login/view/login_view.dart
class LoginView extends StatefulWidget {
const LoginView({super.key});
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
TextEditingController tec = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state.status == AuthenticationStatus.authenticated) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomeView(), // 다음 화면으로 이동
),
);
} else if (state.status == AuthenticationStatus.unauthenticated) {
const snackBar = SnackBar(
content: Text(
'닉네임 확인해주세요',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
backgroundColor: Color(0xFF1b1b23),
behavior: SnackBarBehavior.floating,
);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
},
child: BlocBuilder<LoginBloc, LoginState>(builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('WITH TFT'),
),
body: Padding(
padding: const EdgeInsets.all(50),
child: Column(
children: [
const SizedBox(
height: 50,
),
const Column(
children: [
Text(
'TFT 동료 찾기 - 함께 전략전을 즐기세요!',
style: TextStyle(
fontWeight: FontWeight.bold, // 볼드체
fontSize: 18,
),
),
],
),
const SizedBox(
height: 60,
),
TextField(
controller: tec,
decoration: const InputDecoration(
labelText: 'Riot ID',
hintText: '닉네임을 입력해주세요.',
labelStyle: TextStyle(color: Colors.black),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(width: 1, color: Colors.black),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(width: 1, color: Colors.black),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () {
context
.read<LoginBloc>()
.add((RiotSummonerName(nickName: tec.text)));
},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.black),
foregroundColor:
MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // 라운드 없애기
),
),
// 다른 스타일 속성들도 추가 가능
),
child: const SizedBox(
width: double.infinity,
height: 60,
child: Center(
child: Text(
'닉네임 조회',
style: TextStyle(
fontWeight: FontWeight.bold, // 볼드체
fontSize: 16, // 크기 조정
// 다른 스타일 속성들도 추가 가능
),
),
),
),
)
],
),
),
);
}),
);
}
}
간략하게 이런식으로 생각하시면 편할꺼 같습니다.
@override
Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
// Bloc의 상태 변화를 감지하고 그에 따른 처리를 수행
// 예를 들어, 인증 상태에 따라 다른 동작 수행
// 예제에서는 로그인 성공 시 HomeView로 이동하고, 실패 시 스낵바를 표시
},
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
// Bloc의 상태에 따라 UI를 업데이트
// 예제에서는 로그인 화면의 UI를 구성
},
),
);
}
BlocListener사용은 flutter bloc 패키지를 보시면 자세하게 나와있습니다.
일단 패턴을 보여 드리자면 제가 blocEvent 하나 작성할때 순서대로 말씀 드리겠습니다.
총 작성해야하는 경우는 4가지로 생각하시면 편합니다.
bloc,event,state,model
여기 코드에 나와있는 loginEvent를 만든다고 생각하면
일단 닉네임 조회하는 riotApi입니다.
여기에서 DTO를 보시면 id name이런 정보를 represents 받는데 저장하고 싶은 값을 확인합니다 저는 다 저장하려고 model을 작성했습니다
login/model/user_model.dart
class User extends Equatable {
final String id;
final String accountId;
final String puuid;
final String name;
final int profileIconId;
final int revisionDate;
final int summonerLevel;
static const empty = User(
id: '',
accountId: '',
puuid: '',
name: '',
profileIconId: 0,
revisionDate: 0,
summonerLevel: 0,
);
const User({
required this.id,
required this.accountId,
required this.puuid,
required this.name,
required this.profileIconId,
required this.revisionDate,
required this.summonerLevel,
// required this.createdAt,
});
@override
List<Object?> get props => [
id,
accountId,
puuid,
name,
profileIconId,
revisionDate,
summonerLevel,
];
@override
String toString() {
return 'User{id: $id, accountId: $accountId,puuid:$puuid, name: $name, profileIconId: $profileIconId, revisionDate: $revisionDate,summonerLevel: $summonerLevel, }';
}
Map<String, dynamic> toMap() {
return {
'id': id,
'accountId': accountId,
'puuid': puuid,
'name': name,
'profileIconId': profileIconId,
'revisionDate': revisionDate,
'summonerLevel': summonerLevel,
};
}
factory User.fromMap(Map<String, dynamic> map) {
return User(
id: map['id'] ?? "",
accountId: map['accountId'] ?? "",
puuid: map['puuid'] ?? "",
name: map['name'] ?? "",
profileIconId: map['profileIconId'] ?? "" as int,
revisionDate: map['revisionDate'] ?? "" as int,
summonerLevel: map['summonerLevel'] ?? "" as int,
// createdAt: map['createdAt'] ?? "" as int,
);
}
User copyWith({
String? id,
String? accountId,
String? puuid,
String? name,
int? profileIconId,
int? revisionDate,
int? summonerLevel,
}) {
return User(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
puuid: puuid ?? this.puuid,
name: name ?? this.name,
profileIconId: profileIconId ?? this.profileIconId,
revisionDate: revisionDate ?? this.revisionDate,
summonerLevel: summonerLevel ?? this.summonerLevel,
);
}
}
하나하나 보자면
1.Equatable 클래스를 상속하고 있습니다. 이를 통해 객체의 등치 비교를 쉽게 수행할 수 있습니다.
객체 등치 비교
객체의 등치 비교는 두 객체가 서로 동일한지를 확인하는 과정이에요. 예를 들어, A라는 객체와 B라는 객체가 있는데, 두 객체가 모든 속성이 같다면 이 두 객체는 등치(equal)해요. 이를 등치 비교(equality comparison)라고 합니다.
2.속성 정의:
final String id;
final String accountId;
final String puuid;
final String name;
final int profileIconId;
final int revisionDate;
final int summonerLevel;
3.empty 상수 정의: empty라는 상수는 빈 User 객체를 나타냅니다. 이는 초기화된 상태의 User 객체를 쉽게 생성하기 위해 사용될 수 있습니다.
static const empty = User(
id: '',
accountId: '',
puuid: '',
name: '',
profileIconId: 0,
revisionDate: 0,
summonerLevel: 0,
);
4.Equatable을 통한 등치 비교 구현:
@override
List<Object?> get props => [
id,
accountId,
puuid,
name,
profileIconId,
revisionDate,
summonerLevel,
];
5.toString 메서드 구현:toString 메서드를 구현하여 객체를 문자열로 변환할 수 있도록 합니다. 이는 디버깅 및 로그 출력에서 유용합니다
@override
String toString() {
return 'User{id: $id, accountId: $accountId, puuid: $puuid, name: $name, profileIconId: $profileIconId, revisionDate: $revisionDate, summonerLevel: $summonerLevel}';
}
6.toMap 및 fromMap 메서드 구현:toMap 메서드는 User 객체를 Map으로 변환하고, fromMap 메서드는 Map을 User 객체로 변환합니다. 이는 데이터베이스와의 상호 작용 또는 JSON 직렬화와 같은 작업에 유용합니다.
User copyWith({
String? id,
String? accountId,
String? puuid,
String? name,
int? profileIconId,
int? revisionDate,
int? summonerLevel,
}) {
return User(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
puuid: puuid ?? this.puuid,
name: name ?? this.name,
profileIconId: profileIconId ?? this.profileIconId,
revisionDate: revisionDate ?? this.revisionDate,
summonerLevel: summonerLevel ?? this.summonerLevel,
);
}
User copyWith({
String? id,
String? accountId,
String? puuid,
String? name,
int? profileIconId,
int? revisionDate,
int? summonerLevel,
}) {
return User(
id: id ?? this.id,
accountId: accountId ?? this.accountId,
puuid: puuid ?? this.puuid,
name: name ?? this.name,
profileIconId: profileIconId ?? this.profileIconId,
revisionDate: revisionDate ?? this.revisionDate,
summonerLevel: summonerLevel ?? this.summonerLevel,
);
}
이렇게 하면 데이터 모델 캡슐화를 진행 완료 합니다.
생각보다 model 하나만 했는데 이렇게 길어지네요...
너무 길어서 다음 쳅터에 bloc,event,state를 정리하겠습니다.