Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.
        }
      }
    }
like image 902
Gary Frewin Avatar asked May 17 '20 12:05

Gary Frewin


People also ask

Is Bloc good for Flutter?

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.

How do you rebuild bloc in Flutter?

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.

Where do I initialize bloc Flutter?

you can go to ScreenB and ScreenC through ScreenA. ScreenB and ScreenC use the same bloc, and bloc is initialized sepearately in each screens.


1 Answers

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

like image 106
Ritesh Singh Avatar answered Sep 18 '22 13:09

Ritesh Singh