Reputation: 119
I've been working on a project in Flutter trying to follow Clean Architecture principles. I feel like I have a solid understanding of the various layers (data, domain, and presentation), but I am struggling to understand how features are supposed to interact with one another. From the tutorials I've watched, I've understood that each feature is supposed to be decoupled from one another, and if there are common states that multiple features need to interact with (such as an authenticated user), it should be placed in the /common folder for each feature to interact with.
Now let's get to my problem. I am trying to create a menu page where the page will be pulling in menu information from my backend API. I decided to create a feature called menu and create specific uses cases in the domain layer such as GetMenuItems and GetSpecificMenuItem. I've followed TDD and have created "contracts" (i.e. abstract interfaces) for the data layer to interact with the domain layer. The problem is within my state management (BLOC). Here is my menu_bloc.dart file
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:starterbyte/core/usecase/usecase.dart';
import 'package:starterbyte/features/menu/domain/entities/menu_item.dart';
import 'package:starterbyte/features/menu/domain/usecases/get_menu_items.dart';
import 'package:starterbyte/features/menu/domain/usecases/get_specific_menu_item.dart';
part 'menu_event.dart';
part 'menu_state.dart';
class MenuBloc extends Bloc<MenuEvent, MenuState> {
final GetMenuItems _getMenuItem;
final GetSpecificMenuItem _getSpecificMenuItem;
MenuBloc({
required GetMenuItems getMenuItem,
required GetSpecificMenuItem getSpecificMenuItem,
}) : _getMenuItem = getMenuItem,
_getSpecificMenuItem = getSpecificMenuItem,
super(MenuInitial()) {
on<MenuEvent>((_, emit) => emit(
MenuLoading())); //Catches all the Menu Events and emits loading state
on<RetrieveMenuItems>(_onGetMenuItems);
on<RetrieveMenuItem>(_onGetMenuItem);
}
void _onGetMenuItems(
RetrieveMenuItems event,
Emitter<MenuState> emit,
) async {
final response = await _getMenuItem.call(NoParams());
response.fold((failure) => emit(MenuFailure(failure.message)),
(menuItems) => emit(MenuSuccess(menuItems)));
}
void _onGetMenuItem(
RetrieveMenuItem event,
Emitter<MenuState> emit,
) async {
final response = await _getSpecificMenuItem
.call(GetSpecificMenuItemParams(name: event.name));
response.fold((failure) => emit(MenuFailure(failure.message)),
(menuItem) => emit(MenuSuccess([menuItem])));
}
}
As you can see, I am using two use cases to emit various states of the Menu.
The problem is that I am trying to add another use case to the menu feature called 'getCategories', and this is supposed to get a new entity type called Categories (which takes an ID and a name). The problem is, is that I am not sure to create a new feature called Categories and go through all the steps I took for the Menu feature because the entity that I am returning is of a different type. My MenuSuccess state only returns a list of MenuItems -
part of 'menu_bloc.dart';
@immutable
sealed class MenuState {}
final class MenuInitial extends MenuState {}
final class MenuLoading extends MenuState {}
final class MenuSuccess extends MenuState {
final List<MenuItem> menuItems;
MenuSuccess(this.menuItems);
}
final class MenuFailure extends MenuState {
final String message;
MenuFailure(this.message);
}
I am not sure whether I should separate the two functionalities or not. My solution that I have come up with so far is to put categories into it's own feature and create a Cubit in a common folder called CategoryCubit and have the CategoryBloc update this cubit when it's state changes and have the presentation layer of the Menu feature look at the CategoryCubit and updates it's UI from there.
I am using GetIt for dependency injection:
Future<void> initDependencies() async {
_initAuth();
_initMenu();
_initCategories();
final firebase = await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
final FirebaseAuth firebaseAuth = FirebaseAuth.instanceFor(app: firebase);
final FirebaseFirestore firebaseFirestore = FirebaseFirestore.instanceFor(
app: firebase,
);
serviceLocator.registerLazySingleton(() => firebaseAuth);
serviceLocator.registerLazySingleton(() => firebaseFirestore);
serviceLocator.registerFactory(() => InternetConnection());
// core
serviceLocator.registerLazySingleton(() => AppUserCubit());
serviceLocator.registerLazySingleton(() => CategoryCubit());
serviceLocator.registerFactory<ConnectionChecker>(
() => ConnectionCheckerImpl(InternetConnection()));
// Register http.Client
serviceLocator.registerLazySingleton(() => http.Client());
}
//Note: The reason why we can just pass serviceLocator as a parameter is because the GetIt package automatically infers the type stored in the serviceLocator object.
void _initAuth() {
serviceLocator
..registerFactory<AuthRemoteDataSource>(() => AuthRemoteDataSourceImpl(
serviceLocator(),
serviceLocator(),
))
..registerFactory<AuthRepository>(
() => AuthRepositoryImpl(serviceLocator(), serviceLocator()))
..registerFactory(() => UserSignUp(serviceLocator()))
..registerFactory(() => UserLogin(serviceLocator()))
..registerFactory(() => CurrentUser(serviceLocator()))
..registerFactory(() => UserLogout(serviceLocator()))
..registerLazySingleton(() => AuthBloc(
userSignUp: serviceLocator(),
userLogin: serviceLocator(),
currentUser: serviceLocator(),
appUserCubit: serviceLocator(),
userLogout: serviceLocator(),
));
}
void _initMenu() {
serviceLocator
..registerFactory<MenuItemRemoteDataSource>(
() => MenuItemRemoteDataSourceImpl(
serviceLocator(),
))
..registerFactory<MenuItemRepository>(
() => MenuItemRepositoryImpl(serviceLocator(), serviceLocator()))
..registerFactory(() => GetMenuItems(serviceLocator()))
..registerFactory(() => GetSpecificMenuItem(serviceLocator()))
..registerLazySingleton(() => MenuBloc(
getMenuItem: serviceLocator(),
getSpecificMenuItem: serviceLocator(),
));
}
void _initCategories() {
serviceLocator
..registerFactory<CategoryRemoteDataSource>(
() => CategoryRemoteDataSourceImpl(
serviceLocator(),
))
..registerFactory<CategoryRepository>(
() => CategoryRepositoryImpl(serviceLocator(), serviceLocator()))
..registerFactory(() => GetCategories(serviceLocator()))
..registerLazySingleton(() => CategoryBloc(
getCategories: serviceLocator(),
categoryCubit: serviceLocator(),
));
and using BlocProvider in my Main.dart to register to BLOCS.
Am I doing this right? If not, can someone please explain what I am supposed to do. I feel a little lost and not sure if the solution I came up with is following Clean Architecture.
Upvotes: 0
Views: 80