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 } """; }