Search code examples
flutterblocservice-locator

Flutter Bloc state change is not updated UI with get_it


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...

  1. Code of UserAuthenticationBloc:
    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.
        }
      }
    }

Solution

  • 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()));
    }