아직 진행중이지만 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,
  );
}
  1. copyWith 메서드 구현:copyWith 메서드는 현재 객체를 복사하고, 지정된 속성들을 변경하여 새로운 객체를 생성합니다. 이는 불변성을 유지하면서 객체를 갱신할 때 사용됩니다.
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를 정리하겠습니다.

profile
크로스플랫폼 클라이언트 개발자(Flutter) 1년차

0개의 댓글