I've been building a login/authentication feature using a combination of this login tutorial and the resocoder clean architecture tutorials. It's 99% working perfectly, but it is not responding properly to the LoginButton
being pressed.
For some reason when LoginBloc
calls AuthenticationBloc.add(loggedin())
, the AuthenticationBloc yields the AuthenticationAuthenticated()
state just fine, but the BlocBuilder
in Main.dart doesn't receive the state change. Even the OnTransition
inside SimpleBlocDelegate is triggered when AuthenticationAuthenticated
is yielded, but BlocBuilder
does nothing.
Main.dart
looks like this:
import 'package:bloc/bloc.dart';
import 'package:flutter_app/dependency_injector.dart' as di;
import 'package:flutter_app/features/login/presentation/pages/login_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'features/login/presentation/bloc/user_login_bloc.dart';
import 'features/login/presentation/bloc/user_login_events.dart';
import 'features/login/presentation/bloc/user_login_states.dart';
class SimpleBlocDelegate extends BlocDelegate {
@override
void onEvent(Bloc bloc, Object event) {
print(event);
super.onEvent(bloc, event);
}
@override
void onTransition(Bloc bloc, Transition transition) {
print(transition);
super.onTransition(bloc, transition);
}
@override
void onError(Bloc bloc, Object error, StackTrace stackTrace) {
print(error);
super.onError(bloc, error, stackTrace);
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await di.init(); //Dependency Injection using get_it
BlocSupervisor.delegate = SimpleBlocDelegate();
runApp(
BlocProvider<UserAuthenticationBloc>(
create: (_) => sl<UserAuthenticationBloc>()..add(AppStarted()),
child: App(),
),
);
}
class App extends StatelessWidget {
App({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocBuilder<UserAuthenticationBloc, AuthenticationState>(
builder: (context, state) {
if (state is AuthenticationAuthenticated) {
return Container(
child: HomePage(); // THIS NEVER HAPPENS, even though AuthBloc yields the State
}
if (state is AuthenticationUnauthenticated) {
return LoginScreen(); // THIS yeilds fine when AppStarted in passed on init.
}
if (state is AuthenticationLoading) {
return LoadingIndicator();
}
return Scaffold(
body: SplashPage();
)
},
),
);
}
}
I can only think it has something to do with get_it
. The Dependency Injection looks like this:
final sl = GetIt.instance;
Future<void> init() async {
sl.registerFactory(
() => UserAuthenticationBloc(
getCachedUser: sl(),
),
);
sl.registerFactory(
() => LoginBloc(authenticationBloc: sl(), getUserFromEmailAndPassword: sl()),
);
...
}
and then in the widget tree for the loginscreen
the LoginBloc
gets created, so it is available to the login form.
class LoginScreen extends StatelessWidget {
LoginScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider<LoginBloc>(
create: (_) => sl<LoginBloc>(),
child: LoginForm(), //login form
),
);
}
}
TWO EDITS:
1. I changed UserAuthenticationBloc
in the dependency-injection file from a factory to a lazysingleton... now it works. However, I heard that using singletons for classes with Streams can cause memory leaks?? I guess it means that LoginBloc
is not talking to the same instance of AuthBloc
that Main.dart is listening to? I've no idea how to ensure that without the singleton...
class UserAuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
final GetCachedUser getCachedUser;
UserAuthenticationBloc({
@required GetCachedUser getCachedUser,
}) : assert(getCachedUser != null),
getCachedUser = getCachedUser;
@override
AuthenticationState get initialState => AuthenticationUninitialized();
@override
Stream<AuthenticationState> mapEventToState(AuthenticationEvent event) async* {
if (event is AppStarted) {
yield AuthenticationUnauthenticated();
}
}
if (event is LoggedIn) {
yield AuthenticationAuthenticated(); //this fires.
}
}
}
Flutter bloc it's a great option, as you can see it's not complicated to use it and it's easy to understand the main concept of how can you use it. Also, It gives you a lot of ways to manage your views or widgets.
If you just need to rebuild the screen, calling setState() should trigger a rebuild. BlocBuilder on the other hand rebuilds on state changes. If there's a state change that you'd like to observe in your bloc, calling something like context.
you can go to ScreenB and ScreenC through ScreenA. ScreenB and ScreenC use the same bloc, and bloc is initialized sepearately in each screens.
EDIT
Use bloc provided methods for dependency injection rather than get_it. Creating a singleton can be a issue as it won't be disposed automatically.BlocProvider
handles and disposes the created bloc as mentioned in docs
In most cases, BlocProvider should be used to create new blocs which will be made available to the rest of the subtree. In this case, since BlocProvider is responsible for creating the bloc, it will automatically handle closing the bloc.
And use BlocProvider.value
to pass the value as suggested in bloc official documentation.
BlocProvider(
create: (BuildContext context) => BlocA(service1: sl<Service1>()),
child: ChildA(),
);
This is how I use BlocProvider and get_it together. I use get_it for everything other than Bloc. And the parameters for bloc are provided by get_it's dependency injection.
If you want to use get_it, read TLDR; section.
TLDR;
Use Singleton
only when necessary (AuthenticationBloc
). And keep using Factory
for all the other Blocs (LoginBloc
, etc).
final sl = GetIt.instance;
final Environment _env = Environment();
Future<void> init() async {
//! Core
..load some singletons
//! Bloc
sl.registerLazySingleton(() => AuthenticationBloc(secureStorage: sl()));
sl.registerFactory(() => LoginBloc(authenticationBloc: sl(), authService: sl()));
sl.registerFactory(() => SignupBloc(authenticationBloc: sl(), authService: sl()));
}
Concepts
I use the same approach when using bloc. The most common case we encounter where we need to two blocs to communicate is AuthenticationBloc communicates to almost all the other blocs.
Why registerFactory
do not work. But registerLazySingleton
does
The definition by getit for registerFactory
You have to pass a factory function func that returns an NEW instance of an implementation of T. Each time you call get() you will get a new instance returned
As per the get_it documentation. registerFactory
generates a new instance of Bloc object every time we call the sl<AuthenticationBloc>()
method.
Now when LoginBloc
Constructor asks for a parameter and we pass sl()
in our dependecy injection file, we are creating a new instance and passing it to our LoginBloc
. Hence the AuthenticationBloc
instance which is in use throughout our app is not equal to the AuthenticationBloc
that we have provided to our LoginBloc
constructor. And as a result your AuthenticationBloc
won't listen to changes that are communicated by LoginBloc
as it added event to some other instance of AuthenticationBloc
.
registerLazySingleton
is defined as
You have to pass a factory function func that returns an instance of an implementation of T. Only the first time you call get() this factory function will be called to create a new instance.
And as explained above the simple solution would be to change the dependency injection from registerFactory
to registerLazySingleton
. By doing this you will be providing a single instance of AuthenticationBloc
throughout the application. Hence events added to AuthenticationBloc
from LoginBloc
will start working.
Proposed Solution
There can be two solution. One is that is proposed in this question i.e. change every Bloc to lazySingleton
. But it won't create new Bloc when needed. By using that method you will be using the same Bloc instance throughout the application. It is suitable for most the situations.
Another way is to make Singleton
only when necessary (AuthenticationBloc
). And keep using Factory
for all the other Blocs (LoginBloc
, etc).
Authentication Bloc
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
final SecureStorage secureStorage;
AuthenticationBloc({required this.secureStorage}) : super(AppInitial());
@override
Stream<AuthenticationState> mapEventToState(AuthenticationEvent event) async* {
if (event is AppStarted) {
AuthModel? authModel = await secureStorage.getAuthUser();
if (authModel != null && authModel.jwtToken.isNotEmpty && authModel.userId.isNotEmpty) {
yield AuthenticationUserKnown(authModel: authModel);
} else {
yield AuthenticationUserUnknown();
}
} else if (event is UserAuthenticated) {
yield AuthenticationUserKnown(authModel: event.authModel);
} else if (event is UserLoggedOut) {
yield AuthenticationUserUnknown();
}
}
}
Login Bloc
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc({required this.authenticationBloc, required this.validationHelper, required this.authService})
: super(LoginInitial());
final AuthenticationBloc authenticationBloc;
final AuthService authService;
final ValidationHelper validationHelper;
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is EmailAuthenticationRequested) {
yield* _mapEmailAuthencationRequestedEventToState(event);
}
}
Stream<LoginState> _mapEmailAuthencationRequestedEventToState(EmailAuthenticationRequested event) async* {
yield AuthenticationInProgress();
final authEither = await authService.loginWithEmail(email: event.email, password: event.password);
yield authEither.fold(
(failure) => LoginAuthenticationFailure(failureMessage: failure.errorMessage),
(authModel) {
authenticationBloc.add(UserAuthenticated(authModel: authModel));
return LoginAuthenticationSuccess(authModel: authModel, authenticationMethod: AuthenticationMethod.EMAIL);
},
);
}
@override
Future<void> close() {
authenticationBloc.close();
return super.close();
}
}
Dependency injector
final sl = GetIt.instance;
final Environment _env = Environment();
Future<void> init() async {
//! Core
..load some singletons
//! Bloc
sl.registerLazySingleton(() => AuthenticationBloc(secureStorage: sl()));
sl.registerFactory(() => LoginBloc(authenticationBloc: sl(), validationHelper: sl(), authService: sl()));
sl.registerFactory(() => SignupBloc(validationHelper: sl(), authService: sl()));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With