skip to Main Content

I used BloC pattern to create an application.
There is a data provider layer that is responsible to request a session code from the remote server.

abstract class AppApi{
  /// Logins a user to server and return a [Session].
  ///
  /// Throw [HttpException] if response status code is not 200.Throw
  /// [FetchSessionException] if response message is not "OK" or response data is null.
  Future<Session> fetchSession(String username, String password);
}

There is a repository layer that is responsible to expose the session with a Stream<Session> get session to the application layer.

abstract class LoginRepository {
  factory LoginRepository(AppApi appApi) =>
      LoginRepositoryImpl(appApi);

  Stream<Session> get session;

  Future<void> getSession(String username, String password);
}

There is a cubit in the application layer that subscribes to the repository session stream and with hybricCubit it saves the session.

class SessionCubit extends HydratedCubit<PersistenceSession> {
  final LoginRepository _loginRepository;
  late StreamSubscription<Session> _loginStreamSubscription;

  static final defaultSessionState = PersistenceSession.empty;

  SessionCubit(this._loginRepository) : super(defaultSessionState) {
    _loginStreamSubscription =
        _loginRepository.session.asBroadcastStream().listen((session) {
      if (session == Session.empty) {
        emit(defaultSessionState);
      } else {
        emit(
          state.copyWith(session: session.sessionCode),
        );
      }
    });
  }
}

Each time other cubits want to request to the server they request the session with context.read<PersisSessionCubit>.state.session.code and pass the code as an argument to the repository and data layer.

But I want to persist the session in the repository layer or data layer and then other cubits in the application layer use this saved session with a StreamSubscription.

At which layer can I save the session to prevent tight coupling?

3

Answers


  1. Chosen as BEST ANSWER

    I managed the architecture base on BLoC architecture guide to separate data layer and domain layer and application layer.

    • Application layer :
      • Cubits/ Bloc
      • Flutter widgets
    • Repository layer:
      • Login repository
    • Data layer:
      • Remote server
      • Persist storage with hive

    Then in the repository layer create an algorithm to check if the session in persist data layer does not exist or has expired then call the logout method (since API doesn't support the refresh token endpoint I called the logout method which clears the storage and redirects the user to the login screen) Then when a user request login and we get a 200 response status code with a session, we store the session in persist layer.

    The key to solving this problem is on the repository layer and persist policy to manage calls to the remote server or use the session in persist data layer.


  2. You could just keep the StreamSubscription<Session> in the LoginRepository class. That instance of LoginRepository would get passed into the constructor of any Bloc/Cubit that needs it, and they all would listen to the same stream.

    That being said, I see nothing wrong with how you have it setup now where you just grab session code from the SessionCubit when you need it. You already have loose coupling setup because you don’t have the other cubits directly depending on the SessionCubit. That keeps testing easier without having to stub other Cubits in each blocTest instance.

    Either of these approaches are in line with the Bloc pattern, and maintain loose coupling between Bloc classes.

    Login or Signup to reply.
  3. Step 1: Define the SessionState class

    class SessionState {
      final bool isAuthenticated;
      final String? username;
    
      SessionState({required this.isAuthenticated, this.username});
    
      SessionState copyWith({bool? isAuthenticated, String? username}) {
        return SessionState(
          isAuthenticated: isAuthenticated ?? this.isAuthenticated,
          username: username ?? this.username,
        );
      }
    }
    

    Step 2: Define the S## Heading ##essionEvent class

    abstract class SessionEvent {}
    
    class LoginEvent extends SessionEvent {
      final String username;
      final String password;
    
      LoginEvent({required this.username, required this.password});
    }
    
    class LogoutEvent extends SessionEvent {}
    

    Step 3: Define the SessionBloc class

    class SessionBloc extends Bloc<SessionEvent, SessionState> {
      SessionBloc() : super(SessionState(isAuthenticated: false));
    

    Step 4: Load the initial session state from persistent storage

      @override
      Future<void> onLoad() async {
        super.onLoad();
    
        SharedPreferences prefs = await SharedPreferences.getInstance();
    
        bool isAuthenticated = prefs.getBool('isAuthenticated') ?? false;
        String? username = prefs.getString('username');
    
        SessionState initialState =
            SessionState(isAuthenticated: isAuthenticated, username: username);
    
        emit(initialState);
      }
    
      @override
      Stream<SessionState> mapEventToState(SessionEvent event) async* {
        if (event is LoginEvent) {
      // Step 5: Handle the LoginEvent by updating the session state and saving it to persistent storage
      SharedPreferences prefs = await SharedPreferences.getInstance();
    
      bool isAuthenticated = true;
      String username = event.username;
    
      await prefs.setBool('isAuthenticated', isAuthenticated);
      await prefs.setString('username', username);
    
      yield state.copyWith(isAuthenticated: isAuthenticated, username: username);
    } else if (event is LogoutEvent) {
    

    Step 5: Handle the LogoutEvent by updating the session state and saving it to persistent storage

      SharedPreferences prefs = await SharedPreferences.getInstance();
    
      bool isAuthenticated = false;
      String? username = null;
    
      await prefs.setBool('isAuthenticated', isAuthenticated);
      await prefs.remove('username');
    
      yield state.copyWith(isAuthenticated: isAuthenticated, username: username);
    }}}
    

    Step 6: Use the SessionBloc in your app

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: BlocProvider(
            create: (BuildContext context) => SessionBloc(),
            child: BlocBuilder<SessionBloc, SessionState>(
              builder: (context, state) {
                if (!state.isAuthenticated) {
                  return LoginPage();
                } else {
                  return HomePage(username: state.username!);
                }
              },
            ),
          ),
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search