1
\$\begingroup\$

I'm new to Flutter and BloC and it just seems quite different than what I've been using until now. In terms of state, I'm coming from the JS world of Vuex, Pinia and Redux and I've also worked a with Kotlin Jetpack Compose.

I'm trying to build a simple login/authentication where a request is made with HTTP request. I have one bloc for the login form and another auth bloc to keep the JWT token and the authentication state.

If you can make any suggestions to the code or just generally see something thats not following the correct pattern I would appreciate it if you can drop me a comment. Thank you for taking the time.

Login Form

import 'package:bloc_auth/src/components/forms/email_field.dart'; import 'package:bloc_auth/src/components/forms/password_field.dart'; import 'package:bloc_auth/src/components/spacing.dart' show spacingVOne; import 'package:bloc_auth/src/feature/authentication/bloc/authentication_bloc.dart'; import 'package:bloc_auth/src/feature/authentication/bloc/login_bloc.dart'; import 'package:bloc_auth/src/screen/home.dart' show HomeScreen; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class LoginFormFields extends StatefulWidget { const LoginFormFields({super.key}); @override State<LoginFormFields> createState() => _LoginFormFieldsState(); } class _LoginFormFieldsState extends State<LoginFormFields> { final _formKey = GlobalKey<FormState>(); final emailFocusNode = FocusNode(); final passwordFocusNode = FocusNode(); String? _emailErrorText; String? _passwordErrorText; void _login(LoginBloc loginBloc) { setState(() { _emailErrorText = null; _passwordErrorText = null; }); if (!_formKey.currentState!.validate()) { return; } _formKey.currentState!.save(); loginBloc.add(const LoginSubmitted()); } @override Widget build(BuildContext context) { return BlocListener<LoginBloc, LoginState>( listener: (context, state) { if (LoginStatus.failure == state.status) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( const SnackBar(content: Text('Authentication Failure')), ); setState(() { _emailErrorText = state.emailErrorText; _passwordErrorText = state.passwordErrorText; }); context.read<LoginBloc>().add(const LoginStatusReset()); } else if (LoginStatus.success == state.status) { context.read<AuthenticationBloc>().add( AuthenticationSucceeded(state.loginResponseToken ?? ''), ); Navigator.push( context, MaterialPageRoute(builder: (context) => const HomeScreen()), ); } }, child: Form( key: _formKey, child: Column( children: [ emailField( focusNode: emailFocusNode, emailErrorText: _emailErrorText, onChanged: (value) { context.read<LoginBloc>().add( LoginUsernameChanged(value ?? ''), ); }, ), passwordField( focusNode: passwordFocusNode, errorText: _passwordErrorText, onChanged: (value) { context.read<LoginBloc>().add( LoginPasswordChanged(value ?? ''), ); }, ), spacingVOne(), ElevatedButton( onPressed: () { _login(context.read<LoginBloc>()); }, child: Text('Sign in'), ), ], ), ), ); } } 

Login Bloc

import 'dart:convert'; import 'package:bloc_auth/src/feature/authentication/repository/authentication_repository.dart' show AuthenticationRepository; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart' show Bloc, Emitter; part 'login_event.dart'; part 'login_state.dart'; class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc({required AuthenticationRepository authenticationRepository}) : _authenticationRepository = authenticationRepository, super(const LoginState(username: '', password: '')) { on<LoginUsernameChanged>(_onUsernameChanged); on<LoginPasswordChanged>(_onPasswordChanged); on<LoginSubmitted>(_onSubmitted); on<LoginStatusReset>(_loginStatusReset); } final AuthenticationRepository _authenticationRepository; void _onUsernameChanged( LoginUsernameChanged event, Emitter<LoginState> emit, ) { emit(state.copyWith(username: event.username)); } void _onPasswordChanged( LoginPasswordChanged event, Emitter<LoginState> emit, ) { emit(state.copyWith(password: event.password)); } void _loginStatusReset(LoginStatusReset event, Emitter<LoginState> emit) { emit(state.copyWith(status: LoginStatus.unknown)); } Future<void> _onSubmitted( LoginSubmitted event, Emitter<LoginState> emit, ) async { emit(state.copyWith(status: LoginStatus.inProgress)); await _authenticationRepository .logIn(username: state.username, password: state.password) .then((response) { if (200 == response.statusCode) { final json = jsonDecode(response.body); emit( state.copyWith( status: LoginStatus.success, loginResponseToken: json.containsKey('access_token') ? json['access_token'] : null, ), ); } else { emit( state.copyWith( status: LoginStatus.failure, emailErrorText: 'Login Failed', ), ); } }) .catchError((error) { emit( state.copyWith( status: LoginStatus.failure, emailErrorText: 'Something went wrong. Please try again.', ), ); }); } } 

Login Event

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]; @override String toString() => 'LoginUsernameChanged(username: $username)'; } final class LoginPasswordChanged extends LoginEvent { const LoginPasswordChanged(this.password); final String password; @override List<Object> get props => [password]; @override String toString() => 'LoginPasswordChanged(password: $password)'; } final class LoginSubmitted extends LoginEvent { const LoginSubmitted(); @override String toString() => 'LoginSubmitted()'; } final class LoginStatusReset extends LoginEvent { const LoginStatusReset(); @override String toString() => 'LoginStatusReset()'; } 

Login State

part of 'login_bloc.dart'; enum LoginStatus { unknown, inProgress, success, failure } final class LoginState extends Equatable { const LoginState({ required this.username, required this.password, this.status = LoginStatus.unknown, this.emailErrorText = '', this.passwordErrorText = '', this.loginResponseToken, }); final String username; final String password; final LoginStatus status; final String emailErrorText; final String passwordErrorText; final String? loginResponseToken; LoginState copyWith({ String? username, String? password, LoginStatus? status, String? emailErrorText, String? passwordErrorText, String? loginResponseToken, }) { return LoginState( username: username ?? this.username, password: password ?? this.password, status: status ?? this.status, emailErrorText: emailErrorText ?? this.emailErrorText, passwordErrorText: passwordErrorText ?? this.passwordErrorText, loginResponseToken: loginResponseToken ?? this.loginResponseToken, ); } @override List<Object?> get props => [ username, password, status, emailErrorText, passwordErrorText, loginResponseToken, ]; @override String toString() => """ LoginState { username: $username, password: $password, status: $status, emailErrorText: $emailErrorText, passwordErrorText: $passwordErrorText, loginResponseToken: $loginResponseToken } """; } 
\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

Instead of writing toString() everywhere just to see what’s happening in your bloc, you can plug in the logger package with a BlocObserver. This way you don’t need to manually override toString() in every event or state the observer will automatically log everything for you in a nice, readable way. It keeps your code much cleaner.

Also, if you are already using Bloc, try to avoid using setState. With bloc, your UI should rebuild itself based on the emitted states. Mixing setState and bloc usually means you’re duplicating state management, which makes the code harder to maintain if it grows big.

Login UI

 import 'package:bloc_auth/src/components/forms/email_field.dart'; import 'package:bloc_auth/src/components/forms/password_field.dart'; import 'package:bloc_auth/src/components/spacing.dart' show spacingVOne; import 'package:bloc_auth/src/feature/authentication/bloc/authentication_bloc.dart'; import 'package:bloc_auth/src/feature/authentication/bloc/login_bloc.dart'; import 'package:bloc_auth/src/screen/home.dart' show HomeScreen; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class LoginFormFields extends StatefulWidget { const LoginFormFields({super.key}); @override State<LoginFormFields> createState() => _LoginFormFieldsState(); } class _LoginFormFieldsState extends State<LoginFormFields> { final _formKey = GlobalKey<FormState>(); final emailFocusNode = FocusNode(); final passwordFocusNode = FocusNode(); void _login(BuildContext context) { if (!_formKey.currentState!.validate()) { return; } _formKey.currentState!.save(); context.read<LoginBloc>().add(const LoginSubmitted()); } @override Widget build(BuildContext context) { return BlocConsumer<LoginBloc, LoginState>( listenWhen: (prev, curr) => prev.status != curr.status, listener: (context, state) { if (state.status == LoginStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text( state.emailErrorText.isNotEmpty ? state.emailErrorText : 'Authentication Failed', ), ), ); context.read<LoginBloc>().add(const LoginStatusReset()); } else if (state.status == LoginStatus.success) { context.read<AuthenticationBloc>().add( AuthenticationSucceeded(state.loginResponseToken ?? ''), ); Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const HomeScreen()), ); } }, builder: (context, state) { return Form( key: _formKey, child: Column( children: [ emailField( focusNode: emailFocusNode, emailErrorText: state.emailErrorText.isNotEmpty ? state.emailErrorText : null, onChanged: (value) { context.read<LoginBloc>().add( LoginUsernameChanged(value ?? ''), ); }, ), passwordField( focusNode: passwordFocusNode, errorText: state.passwordErrorText.isNotEmpty ? state.passwordErrorText : null, onChanged: (value) { context.read<LoginBloc>().add( LoginPasswordChanged(value ?? ''), ); }, ), spacingVOne(), ElevatedButton( onPressed: state.status == LoginStatus.inProgress ? null : () => _login(context), child: state.status == LoginStatus.inProgress ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation<Color>( Colors.white, ), ), ) : const Text('Sign in'), ), ], ), ); }, ); } } 

Login Bloc

 import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart' show Bloc, Emitter; part 'login_event.dart'; part 'login_state.dart'; class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc({required AuthenticationRepository authenticationRepository}) : _authenticationRepository = authenticationRepository, super(const LoginState(username: '', password: '')) { on<LoginUsernameChanged>(_onUsernameChanged); on<LoginPasswordChanged>(_onPasswordChanged); on<LoginSubmitted>(_onSubmitted); on<LoginStatusReset>(_loginStatusReset); } final AuthenticationRepository _authenticationRepository; void _onUsernameChanged( LoginUsernameChanged event, Emitter<LoginState> emit, ) { emit(state.copyWith(username: event.username)); } void _onPasswordChanged( LoginPasswordChanged event, Emitter<LoginState> emit, ) { emit(state.copyWith(password: event.password)); } void _loginStatusReset(LoginStatusReset event, Emitter<LoginState> emit) { emit(state.copyWith(status: LoginStatus.unknown)); } Future<void> _onSubmitted( LoginSubmitted event, Emitter<LoginState> emit, ) async { emit(state.copyWith(status: LoginStatus.inProgress)); try { // Mock API delay final response = await Future.delayed( const Duration(seconds: 2), () => {"statusCode": 200, "access_token": "mock_token_123"}, ); if (response['statusCode'] == 200) { emit( state.copyWith(status: LoginStatus.success, loginResponseToken: ""), ); } else { emit( state.copyWith( status: LoginStatus.failure, emailErrorText: 'Login Failed', ), ); } } catch (e) { emit( state.copyWith( status: LoginStatus.failure, emailErrorText: 'Something went wrong. Please try again.', ), ); } } } 

Login State

 part of 'login_bloc.dart'; enum LoginStatus { unknown, inProgress, success, failure } final class LoginState extends Equatable { const LoginState({ required this.username, required this.password, this.status = LoginStatus.unknown, this.emailErrorText = '', this.passwordErrorText = '', this.loginResponseToken, }); final String username; final String password; final LoginStatus status; final String emailErrorText; final String passwordErrorText; final String? loginResponseToken; LoginState copyWith({ String? username, String? password, LoginStatus? status, String? emailErrorText, String? passwordErrorText, String? loginResponseToken, }) { return LoginState( username: username ?? this.username, password: password ?? this.password, status: status ?? this.status, emailErrorText: emailErrorText ?? this.emailErrorText, passwordErrorText: passwordErrorText ?? this.passwordErrorText, loginResponseToken: loginResponseToken ?? this.loginResponseToken, ); } @override List<Object?> get props => [ username, password, status, emailErrorText, passwordErrorText, loginResponseToken, ]; @override bool get stringify => true; } 

Login Event

 part of 'demo_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(); } final class LoginStatusReset extends LoginEvent { const LoginStatusReset(); } 

User a Bloc Observer with combination of any Logger Package

 flutter_bloc: ^8.1.3 logger: ^2.0.2+1 

Simple Bloc Observer

 import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logger/logger.dart'; /// Global logger instance final logger = Logger( printer: PrettyPrinter( methodCount: 2, // number of method calls to show errorMethodCount: 5, // number of stacktrace lines for errors lineLength: 80, colors: true, printEmojis: true, printTime: true, ), ); class SimpleBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); logger.i('[EVENT] ${bloc.runtimeType}, Event: $event'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); logger.d('[CHANGE] ${bloc.runtimeType}, Change: $change'); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); logger.t('[TRANSITION] ${bloc.runtimeType}, Transition: $transition'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { logger.e('[ERROR] ${bloc.runtimeType}, Error: $error', error, stackTrace); super.onError(bloc, error, stackTrace); } } 

Main

 import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'simple_bloc_observer.dart'; void main() { Bloc.observer = SimpleBlocObserver(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: // add your class here ), ); } } 
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.