close
İçeriğe geç

Flutter Giriş

orta seviye

Bu eğitimde, Bloc kütüphanesini kullanarak Flutter ile bir Giriş Akışı oluşturacağız.

demo

  • BlocProvider, alt widget'larına bir bloc sağlayan Flutter widget'ı.
  • context.read ile olay ekleme.
  • Equatable ile gereksiz yeniden oluşturmaları önleme.
  • RepositoryProvider, alt widget'larına bir repository sağlayan Flutter widget'ı.
  • BlocListener, bloc'taki durum değişikliklerine yanıt olarak listener kodunu çağıran bir Flutter widget'ı.
  • context.select ile bir bloc durumunun bir kısmına dayalı olarak kullanıcı arayüzünü güncelleme.

Yepyeni bir Flutter projesi oluşturarak başlayacağız

Terminal window
flutter create flutter_login

Sonrasında tüm bağımlılıklarımızı kurabiliriz

Terminal window
flutter pub get

İlk yapacağımız şey, kimlik doğrulama alanını yönetmekten sorumlu olacak bir authentication_repository paketi oluşturmaktır.

Tüm dahili paketleri içerecek olan projenin kökünde bir packages/authentication_repository dizini oluşturarak başlayacağız.

Yüksek seviyede, dizin yapısı şöyle görünmelidir:

├── android
├── ios
├── lib
├── packages
│ └── authentication_repository
└── test

Sırada, authentication_repository paketi için bir pubspec.yaml oluşturabiliriz:

packages/authentication_repository/pubspec.yaml
name: authentication_repository
description: Dart package which manages the authentication domain.
publish_to: none
environment:
sdk: ^3.12.0

Sırada, packages/authentication_repository/lib/src/authentication_repository.dart içinde yer alacak AuthenticationRepository sınıfının kendisini uygulamamız gerekiyor.

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, bir kullanıcı oturum açtığında veya kapattığında uygulamayı bilgilendirmek için kullanılacak AuthenticationStatus güncellemelerinin bir Stream'ini sunar.

Ayrıca, basitlik adına stub olarak yapılmış ancak örneğin FirebaseAuth veya başka bir kimlik doğrulama sağlayıcısı ile kimlik doğrulama yapacak şekilde kolayca genişletilebilen logIn ve logOut metotları vardır.

Son olarak, dışa açık aktarımları içerecek olan packages/authentication_repository/lib/authentication_repository.dart dosyasını oluşturmamız gerekir:

packages/authentication_repository/lib/authentication_repository.dart
export 'src/authentication_repository.dart';

AuthenticationRepository için bu kadar, sırada UserRepository üzerinde çalışacağız.

AuthenticationRepository'de olduğu gibi, packages dizini içinde bir user_repository paketi oluşturacağız.

├── android
├── ios
├── lib
├── packages
│ ├── authentication_repository
│ └── user_repository
└── test

Sırada, user_repository için pubspec.yaml oluşturacağız:

packages/user_repository/pubspec.yaml
name: user_repository
description: 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.0

user_repository, kullanıcı alanından sorumlu olacak ve mevcut kullanıcıyla etkileşim kurmak için API'ler sunacaktır.

İlk tanımlayacağımız şey, packages/user_repository/lib/src/models/user.dart dosyasındaki kullanıcı modelidir:

packages/user_repository/lib/src/models/user.dart
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('-');
}

Basitlik adına, bir kullanıcının yalnızca bir id özelliği vardır, ancak pratikte firstName, lastName, avatarUrl gibi ek özelliklerimiz olabilir...

Sırada, birden fazla modeli içe aktarmak için tek bir içe aktarma ifadesi kullanabilmemiz adına tüm modelleri dışa aktaracak olan packages/user_repository/lib/src/models içinde bir models.dart oluşturabiliriz.

packages/user_repository/lib/src/models/models.dart
export 'user.dart';

Modeller tanımlandığına göre, UserRepository sınıfını packages/user_repository/lib/src/user_repository.dart içinde uygulayabiliriz.

packages/user_repository/lib/src/user_repository.dart
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()),
);
}
}

Bu basit örnek için, UserRepository mevcut kullanıcıyı alacak tek bir getUser metodu sunar. Bunu stub olarak yapıyoruz, ancak pratikte mevcut kullanıcıyı backend'den sorgulayacağımız yer burasıdır.

user_repository paketi neredeyse bitti -- yapmamız gereken tek şey, dışa açık aktarımları tanımlayan user_repository.dart dosyasını packages/user_repository/lib içinde oluşturmaktır:

packages/user_repository/lib/user_repository.dart
export 'src/models/models.dart';
export 'src/user_repository.dart';

authentication_repository ve user_repository paketleri tamamlandığına göre, Flutter uygulamasına odaklanabiliriz.

Projemizin kökündeki oluşturulmuş pubspec.yaml dosyasını güncelleyerek başlayalım:

pubspec.yaml
name: flutter_login
description: A new Flutter project.
version: 1.0.0+1
publish_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

Bağımlılıkları şu komutu çalıştırarak kurabiliriz:

Terminal window
flutter pub get

AuthenticationBloc, kimlik doğrulama durumundaki değişikliklere (AuthenticationRepository tarafından sunulan) tepki vermekten ve sunum katmanında tepki verebileceğimiz durumları yaymaktan sorumlu olacaktır.

AuthenticationBloc'un uygulaması lib/authentication içindedir çünkü kimlik doğrulamayı uygulama katmanımızda bir özellik olarak ele alıyoruz.

├── lib
│ ├── app.dart
│ ├── authentication
│ │ ├── authentication.dart
│ │ └── bloc
│ │ ├── authentication_bloc.dart
│ │ ├── authentication_event.dart
│ │ └── authentication_state.dart
│ ├── main.dart

AuthenticationEvent örnekleri, AuthenticationBloc'un girdisi olacak ve yeni AuthenticationState örnekleri yaymak için işlenip kullanılacaktır.

Bu uygulamada, AuthenticationBloc iki farklı olaya tepki verecektir:

  • AuthenticationSubscriptionRequested: bloc'a AuthenticationStatus stream'ine abone olmasını bildiren başlangıç olayı
  • AuthenticationLogoutPressed: bloc'a kullanıcının çıkış yapma eylemini bildirir
lib/authentication/bloc/authentication_event.dart
part of 'authentication_bloc.dart';
sealed class AuthenticationEvent {
const AuthenticationEvent();
}
final class AuthenticationSubscriptionRequested extends AuthenticationEvent {}
final class AuthenticationLogoutPressed extends AuthenticationEvent {}

Sırada, AuthenticationState'e bir göz atalım.

AuthenticationState örnekleri, AuthenticationBloc'un çıktısı olacak ve sunum katmanı tarafından tüketilecektir.

AuthenticationState sınıfının üç adlandırılmış kurucu (constructor) vardır:

  • AuthenticationState.unknown(): bloc'un mevcut kullanıcının kimlik doğrulamasının yapılıp yapılmadığını henüz bilmediğini belirten varsayılan durum.

  • AuthenticationState.authenticated(): kullanıcının şu anda kimlik doğrulamasının yapıldığını belirten durum.

  • AuthenticationState.unauthenticated(): kullanıcının şu anda kimlik doğrulamasının yapılmadığını belirten durum.

lib/authentication/bloc/authentication_state.dart
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 ve AuthenticationState uygulamalarını gördüğümüze göre, şimdi AuthenticationBloc'a bir göz atalım.

AuthenticationBloc, kullanıcının bir giriş sayfasında mı yoksa bir ana sayfada mı başlatılacağı gibi şeyleri belirlemek için kullanılan uygulamanın kimlik doğrulama durumunu yönetir.

lib/authentication/bloc/authentication_bloc.dart
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, hem AuthenticationRepository hem de UserRepository'e bir bağımlılığa sahiptir ve başlangıç durumunu AuthenticationState.unknown() olarak tanımlar.

Kurucu gövdesinde, AuthenticationEvent alt sınıfları, ilgili olay işleyicilerine eşlenir.

_onSubscriptionRequested olay işleyicisinde, AuthenticationBloc, AuthenticationRepository'nin status stream'ine abone olmak ve her bir AuthenticationStatus'a yanıt olarak bir durum yaymak için emit.onEach kullanır.

emit.onEach, dahili olarak bir stream subscription oluşturur ve hem AuthenticationBloc hem de status stream'i kapatıldığında onu iptal etmeyi üstlenir.

status stream'i bir hata yayarsa, addError, hatayı ve stackTrace'i dinleyen herhangi bir BlocObserver'a iletir.

status stream'i AuthenticationStatus.unknown veya unauthenticated yaydığında, ilgili AuthenticationState yayılır.

AuthenticationStatus.authenticated yayıldığında, AuthentictionBloc, UserRepository aracılığıyla kullanıcıyı sorgular.

Sırada, varsayılan main.dart'ı şununla değiştirebiliriz:

lib/main.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_login/app.dart';
void main() => runApp(const App());

app.dart, tüm uygulama için kök App widget'ını içerecektir.

lib/app.dart
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(),
);
}
}

Varsayılan olarak, BlocProvider lazy'dir ve Bloc'a ilk kez erişilene kadar create'i çağırmaz. AuthenticationBloc'un her zaman AuthenticationStatus stream'ine hemen abone olması gerektiğinden (AuthenticationSubscriptionRequested olayı aracılığıyla), lazy: false ayarlayarak bu davranışı açıkça devre dışı bırakabiliriz.

AppView, NavigatorState'e erişmek için kullanılan bir GlobalKey tuttuğundan bir StatefulWidget'tır. Varsayılan olarak, AppView, SplashPage'i (daha sonra göreceğimiz) render edecek ve AuthenticationState'teki değişikliklere göre farklı sayfalara yönlendirmek için BlocListener kullanır.

Splash özelliği, kullanıcının kimliğinin doğrulanıp doğrulanmadığını belirlerken uygulama başlatılır başlatılmaz render edilecek basit bir görünüm içerecektir.

lib
└── splash
├── splash.dart
└── view
└── splash_page.dart
lib/splash/view/splash_page.dart
import '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 özelliği bir LoginPage, LoginForm ve LoginBloc içerir ve kullanıcıların uygulamaya giriş yapmak için bir kullanıcı adı ve parola girmesine izin verir.

├── 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.dart

username ve password için yeniden kullanılabilir ve standart modeller oluşturmak amacıyla package:formz kullanıyoruz.

lib/login/models/username.dart
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;
}
}

Basitlik adına, kullanıcı adının boş olmadığından emin olmak için yalnızca doğrulama yapıyoruz, ancak pratikte özel karakter kullanımı, uzunluk vb. zorunlu kılabilirsiniz...

lib/login/models/password.dart
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;
}
}

Yine, parolanın boş olmadığından emin olmak için basit bir kontrol yapıyoruz.

Daha önce olduğu gibi, Username ve Password modellerini tek bir içe aktarma ile içe aktarmayı kolaylaştıran bir models.dart barrel'ı vardır.

lib/login/models/models.dart
export 'password.dart';
export 'username.dart';

LoginBloc, LoginForm'un durumunu yönetir ve kullanıcı adı ile parola girdisinin yanı sıra formun durumunun da doğrulanmasını üstlenir.

Bu uygulamada üç farklı LoginEvent türü vardır:

  • LoginUsernameChanged: bloc'a kullanıcı adının değiştirildiğini bildirir.
  • LoginPasswordChanged: bloc'a parolanın değiştirildiğini bildirir.
  • LoginSubmitted: bloc'a formun gönderildiğini bildirir.
lib/login/bloc/login_event.dart
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();
}

LoginState, formun durumunu ve kullanıcı adı ile parola girdi durumlarını içerecektir.

lib/login/bloc/login_state.dart
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];
}

LoginBloc, LoginForm'daki kullanıcı etkileşimlerine tepki vermekten ve formun doğrulanmasını ve gönderilmesini ele almaktan sorumludur.

lib/login/bloc/login_bloc.dart
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, form gönderildiğinde logIn'i çağırdığı için AuthenticationRepository'e bir bağımlılığa sahiptir. Bloc'un başlangıç durumu pure'dur, yani ne girdilere ne de forma dokunulmuş veya etkileşime geçilmiştir.

username veya password her değiştiğinde, bloc, Username/Password modelinin dirty bir varyantını oluşturur ve form durumunu Formz.validate API'si aracılığıyla günceller.

LoginSubmitted olayı eklendiğinde, formun mevcut durumu geçerliyse, bloc logIn'e bir çağrı yapar ve isteğin sonucuna göre durumu günceller.

Sırada, LoginPage ve LoginForm'a bir göz atalım.

LoginPage, Route'u sunmaktan ve ayrıca LoginBloc'u oluşturup LoginForm'a sağlamaktan sorumludur.

lib/login/view/login_page.dart
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(),
),
),
);
}
}

LoginForm, LoginBloc'u kullanıcı olayları hakkında bilgilendirmeyi yönetir ve ayrıca BlocBuilder ve BlocListener kullanarak durum değişikliklerine yanıt verir.

lib/login/view/login_form.dart
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, giriş gönderimi başarısız olursa bir SnackBar göstermek için kullanılır. Ek olarak, context.select, gereksiz yeniden oluşturmaları önleyerek her widget için LoginState'in belirli bölümlerine verimli bir şekilde erişmek için kullanılır. onChanged geri çağrısı, kullanıcı adı/parola değişikliklerini LoginBloc'a bildirmek için kullanılır.

_LoginButton widget'ı yalnızca formun durumu geçerliyse etkindir ve form gönderilirken yerinde bir CircularProgressIndicator gösterilir.

Başarılı bir logIn isteği üzerine, AuthenticationBloc'un durumu authenticated'a değişecek ve kullanıcı, kullanıcının id'sini ve çıkış yapmak için bir düğmeyi görüntülediğimiz HomePage'e yönlendirilecektir.

├── lib
│ ├── home
│ │ ├── home.dart
│ │ └── view
│ │ └── home_page.dart

HomePage, mevcut kullanıcı id'sine context.select((AuthenticationBloc bloc) => bloc.state.user.id) aracılığıyla erişebilir ve bunu bir Text widget'ı aracılığıyla görüntüler. Ek olarak, çıkış düğmesine dokunulduğunda, AuthenticationBloc'a bir AuthenticationLogoutPressed olayı eklenir.

lib/home/view/home_page.dart
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');
}
}

Bu noktada oldukça sağlam bir giriş uygulamamız var ve Bloc kullanarak sunum katmanımızı iş mantığı katmanından ayırdık.

Bu örneğin tam kaynak kodu (birim ve widget testleri dahil) buradan ulaşılabilir.