Flutter Login
이 튜토리얼에서는 Bloc 라이브러리를 사용해서 Flutter에서 로그인 플로우를 만들어 봅니다.

핵심 주제
섹션 제목: “핵심 주제”- BlocProvider로 하위 위젯에 bloc 제공하기.
- context.read로 이벤트 추가하기.
- Equatable로 불필요한 rebuild 방지하기.
- RepositoryProvider로 하위 위젯에 repository 제공하기.
- BlocListener로 상태 변화에 반응하기.
- context.select로 bloc 상태의 일부에 따라 UI 업데이트하기.
프로젝트 설정
섹션 제목: “프로젝트 설정”새로운 Flutter 프로젝트를 생성합니다.
flutter create flutter_login의존성을 설치합니다.
flutter pub getAuthentication Repository
섹션 제목: “Authentication Repository”먼저 인증 도메인을 관리하는 authentication_repository 패키지를 만듭니다.
프로젝트 루트에 모든 내부 패키지가 들어갈 packages/authentication_repository
디렉토리를 생성합니다.
대략적으로 디렉토리 구조는 다음과 같습니다:
├── android├── ios├── lib├── packages│ └── authentication_repository└── test다음으로 authentication_repository 패키지의 pubspec.yaml을 생성합니다:
name: authentication_repositorydescription: Dart package which manages the authentication domain.publish_to: none
environment: sdk: ^3.12.0다음으로 AuthenticationRepository 클래스 자체를
packages/authentication_repository/lib/src/authentication_repository.dart에
구현합니다.
import 'dart:async';
enum AuthenticationStatus { unknown, authenticated, unauthenticated }
class AuthenticationRepository { final _controller = StreamController<AuthenticationStatus>();
Stream<AuthenticationStatus> get status async* { await Future<void>.delayed(const Duration(seconds: 1)); yield AuthenticationStatus.unauthenticated; yield* _controller.stream; }
Future<void> logIn({ required String username, required String password, }) async { await Future.delayed( const Duration(milliseconds: 300), () => _controller.add(AuthenticationStatus.authenticated), ); }
void logOut() { _controller.add(AuthenticationStatus.unauthenticated); }
void dispose() => _controller.close();}AuthenticationRepository는 사용자가 로그인하거나 로그아웃할 때 앱에 알리는 데
사용할 AuthenticationStatus 업데이트 Stream을 노출합니다.
또한 단순화를 위해 stub으로 처리된 logIn과 logOut 메서드가 있지만,
FirebaseAuth나 다른 인증 프로바이더로 쉽게 확장할 수 있습니다.
마지막으로 public exports를 포함할
packages/authentication_repository/lib/authentication_repository.dart를
생성합니다:
export 'src/authentication_repository.dart';AuthenticationRepository는 여기까지입니다. 다음으로 UserRepository를
만듭니다.
User Repository
섹션 제목: “User Repository”AuthenticationRepository처럼 packages 디렉토리 안에 user_repository
패키지를 만듭니다.
├── android├── ios├── lib├── packages│ ├── authentication_repository│ └── user_repository└── test다음으로 user_repository의 pubspec.yaml을 생성합니다:
name: user_repositorydescription: Dart package which manages the user domain.publish_to: none
environment: sdk: ^3.12.0
dependencies: equatable: ^2.1.0-dev.0 uuid: ^3.0.0user_repository는 사용자 도메인을 담당하고 현재 사용자와 상호작용하기 위한
API를 노출합니다.
먼저 packages/user_repository/lib/src/models/user.dart에 user 모델을
정의합니다:
import 'package:equatable/equatable.dart';
class User extends Equatable { const User(this.id);
final String id;
@override List<Object> get props => [id];
static const empty = User('-');}단순화를 위해 user는 id 속성만 가지지만, 실제로는 firstName, lastName,
avatarUrl 등의 추가 속성이 있을 수 있습니다.
다음으로 packages/user_repository/lib/src/models에 models.dart를 생성해서
단일 import로 여러 모델을 가져올 수 있도록 합니다.
export 'user.dart';이제 모델이 정의됐으니 packages/user_repository/lib/src/user_repository.dart에
UserRepository 클래스를 구현합니다.
import 'dart:async';
import 'package:user_repository/src/models/models.dart';import 'package:uuid/uuid.dart';
class UserRepository { User? _user;
Future<User?> getUser() async { if (_user != null) return _user; return Future.delayed( const Duration(milliseconds: 300), () => _user = User(const Uuid().v4()), ); }}이 간단한 예제에서 UserRepository는 현재 사용자를 가져오는 getUser 메서드
하나만 노출합니다. stub으로 처리됐지만 실제로는 백엔드에서 현재 사용자를
쿼리하는 곳입니다.
user_repository 패키지가 거의 완료됐습니다. 남은 건 public exports를 정의하는
packages/user_repository/lib의 user_repository.dart 파일을 만드는
것뿐입니다:
export 'src/models/models.dart';export 'src/user_repository.dart';이제 authentication_repository와 user_repository 패키지가 완료됐으니 Flutter
앱에 집중할 수 있습니다.
의존성 설치
섹션 제목: “의존성 설치”프로젝트 루트에 생성된 pubspec.yaml을 업데이트합니다:
name: flutter_logindescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ^3.12.0
dependencies: authentication_repository: path: packages/authentication_repository bloc: ^9.0.0 equatable: ^2.1.0-dev.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 formz: ^0.8.0 user_repository: path: packages/user_repository
dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0
flutter: uses-material-design: true다음을 실행해서 의존성을 설치합니다:
flutter pub getAuthentication Bloc
섹션 제목: “Authentication Bloc”AuthenticationBloc은 (AuthenticationRepository가 노출하는) 인증 상태 변화에
반응하고 프레젠테이션 레이어에서 반응할 수 있는 상태를 emit합니다.
AuthenticationBloc 구현은 lib/authentication 안에 있습니다. 인증을 앱
레이어의 기능으로 취급하기 때문입니다.
├── lib│ ├── app.dart│ ├── authentication│ │ ├── authentication.dart│ │ └── bloc│ │ ├── authentication_bloc.dart│ │ ├── authentication_event.dart│ │ └── authentication_state.dart│ ├── main.dartauthentication_event.dart
섹션 제목: “authentication_event.dart”AuthenticationEvent 인스턴스는 AuthenticationBloc의 입력이 되고, 처리되어
새로운 AuthenticationState 인스턴스를 emit하는 데 사용됩니다.
이 앱에서 AuthenticationBloc은 두 가지 이벤트에 반응합니다:
AuthenticationSubscriptionRequested: bloc에게AuthenticationStatus스트림을 구독하라고 알리는 초기 이벤트AuthenticationLogoutPressed: 사용자 로그아웃 액션을 bloc에 알림
part of 'authentication_bloc.dart';
sealed class AuthenticationEvent { const AuthenticationEvent();}
final class AuthenticationSubscriptionRequested extends AuthenticationEvent {}
final class AuthenticationLogoutPressed extends AuthenticationEvent {}다음으로 AuthenticationState를 살펴봅니다.
authentication_state.dart
섹션 제목: “authentication_state.dart”AuthenticationState 인스턴스는 AuthenticationBloc의 출력이 되고 프레젠테이션
레이어에서 사용됩니다.
AuthenticationState 클래스는 세 개의 named constructor가 있습니다:
-
AuthenticationState.unknown(): bloc이 현재 사용자가 인증됐는지 아닌지 아직 모르는 기본 상태. -
AuthenticationState.authenticated(): 사용자가 현재 인증된 상태. -
AuthenticationState.unauthenticated(): 사용자가 현재 인증되지 않은 상태.
part of 'authentication_bloc.dart';
class AuthenticationState extends Equatable { const AuthenticationState._({ this.status = AuthenticationStatus.unknown, this.user = User.empty, });
const AuthenticationState.unknown() : this._();
const AuthenticationState.authenticated(User user) : this._(status: AuthenticationStatus.authenticated, user: user);
const AuthenticationState.unauthenticated() : this._(status: AuthenticationStatus.unauthenticated);
final AuthenticationStatus status; final User user;
@override List<Object> get props => [status, user];}AuthenticationEvent와 AuthenticationState 구현을 봤으니 이제
AuthenticationBloc을 살펴봅니다.
authentication_bloc.dart
섹션 제목: “authentication_bloc.dart”AuthenticationBloc은 사용자를 로그인 페이지에서 시작할지 홈 페이지에서
시작할지 같은 것들을 결정하는 데 사용되는 앱의 인증 상태를 관리합니다.
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';import 'package:bloc/bloc.dart';import 'package:equatable/equatable.dart';import 'package:user_repository/user_repository.dart';
part 'authentication_event.dart';part 'authentication_state.dart';
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> { AuthenticationBloc({ required this._authenticationRepository, required this._userRepository, }) : super(const AuthenticationState.unknown()) { on<AuthenticationSubscriptionRequested>(_onSubscriptionRequested); on<AuthenticationLogoutPressed>(_onLogoutPressed); }
final AuthenticationRepository _authenticationRepository; final UserRepository _userRepository;
Future<void> _onSubscriptionRequested( AuthenticationSubscriptionRequested event, Emitter<AuthenticationState> emit, ) { return emit.onEach( _authenticationRepository.status, onData: (status) async { switch (status) { case AuthenticationStatus.unauthenticated: return emit(const AuthenticationState.unauthenticated()); case AuthenticationStatus.authenticated: final user = await _tryGetUser(); return emit( user != null ? AuthenticationState.authenticated(user) : const AuthenticationState.unauthenticated(), ); case AuthenticationStatus.unknown: return emit(const AuthenticationState.unknown()); } }, onError: addError, ); }
void _onLogoutPressed( AuthenticationLogoutPressed event, Emitter<AuthenticationState> emit, ) { _authenticationRepository.logOut(); }
Future<User?> _tryGetUser() async { try { final user = await _userRepository.getUser(); return user; } catch (_) { return null; } }}AuthenticationBloc은 AuthenticationRepository와 UserRepository 모두에
의존하고 초기 상태를 AuthenticationState.unknown()으로 정의합니다.
생성자 본문에서 AuthenticationEvent 하위 클래스가 해당 이벤트 핸들러에
매핑됩니다.
_onSubscriptionRequested 이벤트 핸들러에서 AuthenticationBloc은
emit.onEach를 사용해서 AuthenticationRepository의 status 스트림을 구독하고
각 AuthenticationStatus에 대한 응답으로 상태를 emit합니다.
emit.onEach는 내부적으로 스트림 구독을 생성하고 AuthenticationBloc이나
status 스트림이 닫히면 취소를 처리합니다.
status 스트림이 에러를 emit하면 addError가 에러와 stackTrace를 listen하고
있는 BlocObserver에 전달합니다.
status 스트림이 AuthenticationStatus.unknown이나 unauthenticated를
emit하면 해당 AuthenticationState가 emit됩니다.
AuthenticationStatus.authenticated가 emit되면 AuthenticationBloc이
UserRepository를 통해 사용자를 쿼리합니다.
main.dart
섹션 제목: “main.dart”기본 main.dart를 다음으로 교체합니다:
import 'package:flutter/widgets.dart';import 'package:flutter_login/app.dart';
void main() => runApp(const App());App
섹션 제목: “App”app.dart는 전체 앱의 루트 App 위젯을 포함합니다.
import 'package:authentication_repository/authentication_repository.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/authentication/authentication.dart';import 'package:flutter_login/home/home.dart';import 'package:flutter_login/login/login.dart';import 'package:flutter_login/splash/splash.dart';import 'package:user_repository/user_repository.dart';
class App extends StatelessWidget { const App({super.key});
@override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (_) => AuthenticationRepository(), dispose: (repository) => repository.dispose(), ), RepositoryProvider(create: (_) => UserRepository()), ], child: BlocProvider( lazy: false, create: (context) => AuthenticationBloc( authenticationRepository: context.read<AuthenticationRepository>(), userRepository: context.read<UserRepository>(), )..add(AuthenticationSubscriptionRequested()), child: const AppView(), ), ); }}
class AppView extends StatefulWidget { const AppView({super.key});
@override State<AppView> createState() => _AppViewState();}
class _AppViewState extends State<AppView> { final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState!;
@override Widget build(BuildContext context) { return MaterialApp( navigatorKey: _navigatorKey, builder: (context, child) { return BlocListener<AuthenticationBloc, AuthenticationState>( listener: (context, state) { switch (state.status) { case AuthenticationStatus.authenticated: _navigator.pushAndRemoveUntil<void>( HomePage.route(), (route) => false, ); case AuthenticationStatus.unauthenticated: _navigator.pushAndRemoveUntil<void>( LoginPage.route(), (route) => false, ); case AuthenticationStatus.unknown: break; } }, child: child, ); }, onGenerateRoute: (_) => SplashPage.route(), ); }}기본적으로 BlocProvider는 lazy라서 Bloc이 처음 접근될 때까지 create를
호출하지 않습니다. AuthenticationBloc은 항상
(AuthenticationSubscriptionRequested 이벤트를 통해) AuthenticationStatus
스트림을 즉시 구독해야 하므로 lazy: false를 설정해서 이 동작을 명시적으로 opt
out합니다.
AppView는 NavigatorState에 접근하는 데 사용되는 GlobalKey를 유지하기
때문에 StatefulWidget입니다. 기본적으로 AppView는 SplashPage(나중에
살펴봄)를 렌더링하고 BlocListener를 사용해서 AuthenticationState 변화에 따라
다른 페이지로 이동합니다.
Splash
섹션 제목: “Splash”splash 기능은 앱이 시작될 때 사용자가 인증됐는지 판단하는 동안 렌더링될 간단한 뷰만 포함합니다.
lib└── splash ├── splash.dart └── view └── splash_page.dartimport 'package:flutter/material.dart';
class SplashPage extends StatelessWidget { const SplashPage({super.key});
static Route<void> route() { return MaterialPageRoute<void>(builder: (_) => const SplashPage()); }
@override Widget build(BuildContext context) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); }}Login
섹션 제목: “Login”login 기능은 LoginPage, LoginForm, LoginBloc을 포함하고 사용자가 앱에
로그인하기 위해 username과 password를 입력할 수 있게 합니다.
├── lib│ ├── login│ │ ├── bloc│ │ │ ├── login_bloc.dart│ │ │ ├── login_event.dart│ │ │ └── login_state.dart│ │ ├── login.dart│ │ ├── models│ │ │ ├── models.dart│ │ │ ├── password.dart│ │ │ └── username.dart│ │ └── view│ │ ├── login_form.dart│ │ ├── login_page.dart│ │ └── view.dartLogin Models
섹션 제목: “Login Models”package:formz를 사용해서 username과
password에 대한 재사용 가능하고 표준적인 모델을 만듭니다.
Username
섹션 제목: “Username”import 'package:formz/formz.dart';
enum UsernameValidationError { empty }
class Username extends FormzInput<String, UsernameValidationError> { const Username.pure() : super.pure(''); const Username.dirty([super.value = '']) : super.dirty();
@override UsernameValidationError? validator(String value) { if (value.isEmpty) return UsernameValidationError.empty; return null; }}단순화를 위해 username이 비어있지 않은지만 검증하지만, 실제로는 특수 문자 사용, 길이 등을 적용할 수 있습니다.
Password
섹션 제목: “Password”import 'package:formz/formz.dart';
enum PasswordValidationError { empty }
class Password extends FormzInput<String, PasswordValidationError> { const Password.pure() : super.pure(''); const Password.dirty([super.value = '']) : super.dirty();
@override PasswordValidationError? validator(String value) { if (value.isEmpty) return PasswordValidationError.empty; return null; }}마찬가지로 password가 비어있지 않은지 간단히 확인합니다.
Models Barrel
섹션 제목: “Models Barrel”이전처럼 단일 import로 Username과 Password 모델을 쉽게 가져올 수 있도록
models.dart barrel이 있습니다.
export 'password.dart';export 'username.dart';Login Bloc
섹션 제목: “Login Bloc”LoginBloc은 LoginForm의 상태를 관리하고 username과 password 입력 유효성
검사와 폼 상태를 처리합니다.
login_event.dart
섹션 제목: “login_event.dart”이 앱에는 세 가지 LoginEvent 타입이 있습니다:
LoginUsernameChanged: username이 수정됐음을 bloc에 알림.LoginPasswordChanged: password가 수정됐음을 bloc에 알림.LoginSubmitted: 폼이 submit됐음을 bloc에 알림.
part of 'login_bloc.dart';
sealed class LoginEvent extends Equatable { const LoginEvent();
@override List<Object> get props => [];}
final class LoginUsernameChanged extends LoginEvent { const LoginUsernameChanged(this.username);
final String username;
@override List<Object> get props => [username];}
final class LoginPasswordChanged extends LoginEvent { const LoginPasswordChanged(this.password);
final String password;
@override List<Object> get props => [password];}
final class LoginSubmitted extends LoginEvent { const LoginSubmitted();}login_state.dart
섹션 제목: “login_state.dart”LoginState는 폼의 상태와 username, password 입력 상태를 포함합니다.
part of 'login_bloc.dart';
final class LoginState extends Equatable { const LoginState({ this.status = FormzSubmissionStatus.initial, this.username = const Username.pure(), this.password = const Password.pure(), this.isValid = false, });
final FormzSubmissionStatus status; final Username username; final Password password; final bool isValid;
LoginState copyWith({ FormzSubmissionStatus? status, Username? username, Password? password, bool? isValid, }) { return LoginState( status: status ?? this.status, username: username ?? this.username, password: password ?? this.password, isValid: isValid ?? this.isValid, ); }
@override List<Object> get props => [status, username, password];}login_bloc.dart
섹션 제목: “login_bloc.dart”LoginBloc은 LoginForm의 사용자 상호작용에 반응하고 폼의 유효성 검사와
submit을 처리합니다.
import 'package:authentication_repository/authentication_repository.dart';import 'package:bloc/bloc.dart';import 'package:equatable/equatable.dart';import 'package:flutter_login/login/login.dart';import 'package:formz/formz.dart';
part 'login_event.dart';part 'login_state.dart';
class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc({required this._authenticationRepository}) : super(const LoginState()) { on<LoginUsernameChanged>(_onUsernameChanged); on<LoginPasswordChanged>(_onPasswordChanged); on<LoginSubmitted>(_onSubmitted); }
final AuthenticationRepository _authenticationRepository;
void _onUsernameChanged( LoginUsernameChanged event, Emitter<LoginState> emit, ) { final username = Username.dirty(event.username); emit( state.copyWith( username: username, isValid: Formz.validate([state.password, username]), ), ); }
void _onPasswordChanged( LoginPasswordChanged event, Emitter<LoginState> emit, ) { final password = Password.dirty(event.password); emit( state.copyWith( password: password, isValid: Formz.validate([password, state.username]), ), ); }
Future<void> _onSubmitted( LoginSubmitted event, Emitter<LoginState> emit, ) async { if (state.isValid) { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { await _authenticationRepository.logIn( username: state.username.value, password: state.password.value, ); emit(state.copyWith(status: FormzSubmissionStatus.success)); } catch (_) { emit(state.copyWith(status: FormzSubmissionStatus.failure)); } } }}LoginBloc은 폼이 submit될 때 logIn을 호출하므로 AuthenticationRepository에
의존합니다. bloc의 초기 상태는 pure로, 입력이나 폼이 아직 터치되거나
상호작용되지 않은 상태입니다.
username이나 password가 바뀔 때마다 bloc은 Username/Password 모델의
dirty variant를 생성하고 Formz.validate API를 통해 폼 상태를 업데이트합니다.
LoginSubmitted 이벤트가 추가되면 폼의 현재 상태가 valid인 경우 bloc이
logIn을 호출하고 요청 결과에 따라 상태를 업데이트합니다.
다음으로 LoginPage와 LoginForm을 살펴봅니다.
Login Page
섹션 제목: “Login Page”LoginPage는 Route를 노출하고 LoginBloc을 생성해서 LoginForm에 제공하는
역할을 합니다.
import 'package:authentication_repository/authentication_repository.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/login/login.dart';
class LoginPage extends StatelessWidget { const LoginPage({super.key});
static Route<void> route() { return MaterialPageRoute<void>(builder: (_) => const LoginPage()); }
@override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(12), child: BlocProvider( create: (context) => LoginBloc( authenticationRepository: context.read<AuthenticationRepository>(), ), child: const LoginForm(), ), ), ); }}Login Form
섹션 제목: “Login Form”LoginForm은 사용자 이벤트를 LoginBloc에 알리고 BlocBuilder와
BlocListener를 사용해서 상태 변화에 반응합니다.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/login/login.dart';import 'package:formz/formz.dart';
class LoginForm extends StatelessWidget { const LoginForm({super.key});
@override Widget build(BuildContext context) { return BlocListener<LoginBloc, LoginState>( listener: (context, state) { if (state.status.isFailure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( const SnackBar(content: Text('Authentication Failure')), ); } }, child: Align( alignment: const Alignment(0, -1 / 3), child: Column( mainAxisSize: MainAxisSize.min, children: [ _UsernameInput(), const Padding(padding: EdgeInsets.all(12)), _PasswordInput(), const Padding(padding: EdgeInsets.all(12)), _LoginButton(), ], ), ), ); }}
class _UsernameInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginBloc bloc) => bloc.state.username.displayError, );
return TextField( key: const Key('loginForm_usernameInput_textField'), onChanged: (username) { context.read<LoginBloc>().add(LoginUsernameChanged(username)); }, decoration: InputDecoration( labelText: 'username', errorText: displayError != null ? 'invalid username' : null, ), ); }}
class _PasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginBloc bloc) => bloc.state.password.displayError, );
return TextField( key: const Key('loginForm_passwordInput_textField'), onChanged: (password) { context.read<LoginBloc>().add(LoginPasswordChanged(password)); }, obscureText: true, decoration: InputDecoration( labelText: 'password', errorText: displayError != null ? 'invalid password' : null, ), ); }}
class _LoginButton extends StatelessWidget { @override Widget build(BuildContext context) { final isInProgressOrSuccess = context.select( (LoginBloc bloc) => bloc.state.status.isInProgressOrSuccess, );
if (isInProgressOrSuccess) return const CircularProgressIndicator();
final isValid = context.select((LoginBloc bloc) => bloc.state.isValid);
return ElevatedButton( key: const Key('loginForm_continue_raisedButton'), onPressed: isValid ? () => context.read<LoginBloc>().add(const LoginSubmitted()) : null, child: const Text('Login'), ); }}BlocListener는 로그인 submit이 실패하면 SnackBar를 표시하는 데 사용됩니다.
또한 context.select를 사용해서 각 위젯이 LoginState의 특정 부분에 효율적으로
접근하여 불필요한 rebuild를 방지합니다. onChanged 콜백은 username/password
변경을 LoginBloc에 알리는 데 사용됩니다.
_LoginButton 위젯은 폼의 상태가 valid인 경우에만 활성화되고, 폼이 submit되는
동안에는 CircularProgressIndicator가 대신 표시됩니다.
Home
섹션 제목: “Home”성공적인 logIn 요청 시 AuthenticationBloc의 상태가 authenticated로 바뀌고
사용자는 user의 id와 로그아웃 버튼이 표시되는 HomePage로 이동합니다.
├── lib│ ├── home│ │ ├── home.dart│ │ └── view│ │ └── home_page.dartHome Page
섹션 제목: “Home Page”HomePage는 context.select((AuthenticationBloc bloc) => bloc.state.user.id)를
통해 현재 user id에 접근하고 Text 위젯을 통해 표시합니다. 또한 logout 버튼이
탭되면 AuthenticationBloc에 AuthenticationLogoutPressed 이벤트가 추가됩니다.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/authentication/authentication.dart';
class HomePage extends StatelessWidget { const HomePage({super.key});
static Route<void> route() { return MaterialPageRoute<void>(builder: (_) => const HomePage()); }
@override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [_UserId(), _LogoutButton()], ), ), ); }}
class _LogoutButton extends StatelessWidget { const _LogoutButton();
@override Widget build(BuildContext context) { return ElevatedButton( child: const Text('Logout'), onPressed: () { context.read<AuthenticationBloc>().add(AuthenticationLogoutPressed()); }, ); }}
class _UserId extends StatelessWidget { const _UserId();
@override Widget build(BuildContext context) { final userId = context.select( (AuthenticationBloc bloc) => bloc.state.user.id, );
return Text('UserID: $userId'); }}이 시점에서 꽤 괜찮은 로그인 구현이 있고, Bloc을 사용해서 프레젠테이션 레이어와 비즈니스 로직 레이어를 분리했습니다.
이 예제의 전체 소스 코드(단위 테스트와 위젯 테스트 포함)는 여기에서 확인할 수 있습니다.