Andres Eloy
Andres Eloy

Reputation: 133

Proper separation of layers and ChangeNotifier in Clean Architecture for Flutter

I am currently implementing Clean Architecture in a Flutter app, dividing it into three layers: domain, data, and presentation. The domain layer consists of use cases, entities, and repository interfaces, the data layer has repository implementations and API interfaces & implementations (data sources), while the presentation layer contains the UI and Bloc.

The dependency flow is as follows: Blocs depend on use cases; use cases depend on repositories, and repositories rely on data sources.

I am facing a specific scenario where I want to ensure independent pages can listen to changes made in different parts of the app. One common situation is having a page displaying a list of T items, from which users can navigate to an item registration page in a deeper route. After returning to the list page, I need to refresh the list dynamically.

Another use case involves authentication, where I use a UserRepository to store the authenticated user. I want my app's UI to update dynamically when the user's authentication status changes. For instance, if an authentication function succeeds and returns a user, I update the current user variable and expect the index page to listen to this change and log the user in instantly. To achieve this, I made the UserRepository class a ChangeNotifier, and I notify listeners whenever the current user changes.

However, I am concerned that this approach might be in conflict with the principles of Clean Architecture. Specifically, I am required to declare a getter for the repository in the use case for the bloc to subscribe to changes.

I want to ensure my implementation respects Clean Architecture principles and is not just a workaround. Am I correctly applying Clean Architecture or am I inadvertently breaking it? Any suggestions or alternatives on handling these scenarios within the Clean Architecture structure would be greatly appreciated. Thank you!

Here is a snipped for one of the examples stated above.

class UserRepository extends ChangeNotifier implements IUserRepository {
  const UserRepository({
    required IUserApi userApi,
  }) : _userApi = userApi;

  final IUserApi _userApi;

  User? currentUser;

  Future<User> logIn(String token) {
    try{
      final user = await _userApi.logIn(token);
      currentUser = user;
      
      notifyListeners();

      return user;
    } catch(e){
      
      currentUser = null;

      notifyListeners();
    }
  }
}

class GetUserUseCases {
  GetUserUseCases({
    required IUserRepository userRepository,
  }) : _user = userRepository;

  final IUserRepository _user;

  IUserRepository get userRepository => _user;

  Future<User> execute(String token) => _user.logIn(token);
}

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc({
    required GetUserUseCases getUserUseCases,
  })  : _getUserUseCases = getUserUseCases,
        {
          on<AuthEventLoggedIn>(_onLogIn);
          on<AuthEventLoggedOut>(_onLogOut);
          // Other event subscriptions ... 

          // Listen to the user repository changes and update the state 
          // in real time.
          _getUserUseCases.userRepository.listen((repository) {
            if (repository.currentUser != null) {
              add(AuthEventLoggedIn(user));
            } else {
              add(const AuthEventLoggedOut());
            }
          });

        }

  final GetUserUseCases _getUserUseCases;

  // Event methods ...
}

Upvotes: 0

Views: 794

Answers (1)

Charles
Charles

Reputation: 1231

The first thing that I noticed is your UserRepository extends ChangeNotifier which contains an internal state of type User? currentUser. A state should be stored and handled by a Bloc, in your case, AuthBloc. Your AuthBloc should be holding an internal state AuthState that can tell other parts of the app what the currentUser is, and it should be responsible to mutate its state, i.e. handle the login and log out event.

Secondly, I would say UserRepository should not need to implement IUserRepository most of the time, unless there are 2 different implementations of UserRepository. It makes code navigating in the IDE much harder and does not bring too much value. If you want to mock UserRepository in tests, you can already do that by extending UserRepository.

Thirdly, UseCase's act as an abstraction layer between the intention and the actual implementation. You should not need to define a UseCase class since Bloc already did that for you, you can use the AuthEvent instead.

Upvotes: 0

Related Questions