Hello! This document contains my personal recommendations for policies, procedures, and standards regarding how development should be conducted.
The project code should be structured with a mix of folder-by-feature and folder-by-type.
Meaning at the root level you have folders defining features. For example:
|-- common/
|-- authentication/
|-- settings/
Common/Core feature contains classes, functions or widgets that by default should be available in any features.
So the idea is to have features of your application divided by top level folders.
Think of every top level folders as a separate dart/flutter package, dependencies or access boundary of your application.
So if you add something to the common folder, you have to ask yourself, will 99.% features of our application need that ?
If you think of it as a dependency, the common dependency is always declared in your pubspec.yaml.
So before adding a class, function or a widget to the common folder, ask yourself:
Inside each feature folder you have folders defined by type.
Here are the folder types that would be part of a feature folder.
|-- ui/
|-- cubits/
|-- domain_objects/
|-- exceptions/
|-- use_cases
|-- services/
|-- repositories/
|-- data_sources/
|-- dtos/
If there's more than one file related to a single file, you can group them in a folder, for example generated code for a class in a file. For example:
|-- authentication/
|---- cubits/
|------ login/
|-------- login_cubit.dart
|-------- login_cubit.freezed.dart
|-------- login_cubit.g.dart
Code that is related to the user's device interface, for example: UI PageBuilders, UI Screens, UI View, UI Components.
A screen is a user interface component that fill the whole device display and is the container of user interface view or component (Button, Checkboxes, Images and ect).
Also, a screen is a specific application navigation destination.
Guideline:
class LoginScreen extends StatelessWidget {
//...fields and constructor...
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_LoginLogo,
_LoginForm,
_LoginActions,
_LoginFooter
],
);
}
}
// extracted by use case.
class _LoginLogo extends StatelessWidget {}
// extracted by update area.
class _LoginForm extends StatefullWidget {}
// extracted by update area.
class _LoginActions extends StatelesssWidget {}
// extracted by update area.
class _LoginFooter extends StatelessWidget {}login_screen.dart
part 'login_screen_components.dart';
class LoginScreen extends StatelessWidget {
//...fields and constructor...
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_LoginLogo,
_LoginForm,
_LoginActions,
_LoginFooter
],
);
}
}
// Is kept here because it's does not break the 400 max line rule.
class _LoginLogo extends StatelessWidget {}login_screen_components.dart
part of 'login_screen.dart';
/// [LoginScreen]'s fields.
class _LoginForm extends StatelessWidget {}
// ************************ Footer ************************
/// [LoginScreen]'s footer.
class _LoginFooter extends StatelessWidget {}
// ************************* ACTIONS *********************************
/// [LoginScreen]'s actions.
class _LoginActions extends StatelessWidget {}class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginCubitState>(
// Here cubit is not specified either.
builder: (BuildContext context, LoginCubitState state) {},
);
}
}class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
//...
}
}
class _SubTree1 extends StatelessWidget {}
class _SubTree2 extends StatelessWidget {}
class _SubTree3 extends StatelessWidget {}
class _SubTree4 extends StatelessWidget {}class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
childreen: <Widget>[
Button1(onClick: _openRegisterUser),
Button2(onClick: _openLogin),
Field(onTextChanged: _onUserNameTextChanged),
],
);
}
void _openRegisterUser() {}
void _openLogin() {}
void _onUserNameTextChanged(String newText) {}
}A view is a user interface component that does not fill the whole device display and is the container of user interface view or component (Button, Checkboxes, Images and ect).
Also, a view is not an application navigation destination.
Guideline:
Cubits are classes that contains business logic for your UI, cubit is bound to a state, that represent the UI state.
Guideline Recommendations:
Future<String?> bookAppointment(BookingData booking);
Future<bool> login(String username, String password);A cubit state class represent the state of ui bounded to it a given time.
Guideline Recommendations:
class LoginState {
bool logginSuceeded // avoid this.
}Classes that used for making a single business operation/goal/job/task in your domain.
Guidelines:
class GetCurrentUserUseCase, class SignInUseCase;UseCase;call(), where return type is the result of executing the use case;Repositories, Services or any other high level coordinators;DataSource or Cubit or interact with dtos or any other low level object;Cubit or in other UseCases.Examples:
class GetCurrentUserUseCase {
final UserRepository _repository;
const GetCurrentUserUseCase(this._repository);
Future<User?> call() async {
await _repository.getCurrentUser();
}
}class AuthenticateMemberUseCase {
/// Create a [AuthenticateMemberUseCase].
const AuthenticateMemberUseCase(
/* constructor arguments */
);
/* ... fields ...*/
/// Execute the use case.
Future<TegTask<void>> call(MemberAuthenticationCredentials credentials) {
return runTaskSafelyAsync<void>(() async {
final bool isEmailValid = credentials.email.isEmail;
if (!isEmailValid) {
throw const TegEmailInvalidException();
}
final bool isConnected = await _hostDeviceInternetService.isConnected();
if (!isConnected) {
throw const TegInternetUnavailableException();
}
final String? deviceId = await _hostDeviceInfoRepository.getDeviceId();
if (deviceId == null) {
throw const TegDeviceIdUnavailableException();
}
final PublicPrivateKeyPair keyPair = await _keyGenerator.generate();
final Member member = await _authService.signIn(
email: credentials.email,
password: credentials.password,
deviceId: deviceId,
publicKey: keyPair.publicKey,
);
await _updateCurrentMemberUseCase(
member: member,
memberPrivateKey: keyPair.privateKey,
deviceId: deviceId,
);
await _saveMemberAuthenticationCredentialsUseCase(credentials);
final TegTask<List<Account>> memberAccountsTask = await _getMemberAccountsUseCase();
if (memberAccountsTask.failed) {
throw memberAccountsTask.exception;
}
await _updateMemberCurrentAccountUseCase(
member: member,
deviceId: deviceId,
memberPrivateKey: keyPair.privateKey,
account: memberAccountsTask.result.first,
);
});
}
}Classes that provide access to data using only CRUD operations for a specific feature or a scope of a feature, for example:
abstract class BookingRepository {
Futute<Booking> getBookingById(String id);
Future<List<Booking>> getBookings();
Future<void> deleteBookingById(String id);
Future<void> saveBooking(Booking booking);
}Guideline Recommendations:
class DefaultBookingRepository implements BookingRepository {
final LocalBookingDataSource local;
final RemoteBookingDataSource remote;
Future<Booking?> getBookingById(String id) async {
Booking? savedBooking = await local.getBookingById(id);
if (savedBooking == null) {
Booking? remoteBooking = await remote.getBookingById(id);
if (remoteBooking != null) {
await local.saveBooking(remoteBooking);
}
return remoteBooking;
}
return savedBooking;
}
//...other operations...
}Interface
abstract class BookingRepository {}Implementations
class DefaultBookingRepository implements BookingRepository {}
class AnonymousBookingRepository implements BookingRepository {}
class PremiumBookingRepository implements BookingRepository {}Classes that provide access to functionalities for a specific feature or a scope of a feature, for example:
abstract class AuthenticationService {
Future<void> authenticate(String username, String password);
//...other functionalities...
}
abstract class AppointmentService {
Future<void> register(Appointment appointment);
//...other functionalities...
}Guideline Recommendations.
Classes that implement access to data located locally or remotely. Example
abstract class LocalBookingDataSource {
Futture<void> saveBooking(Booking booking);
//...other functionalities...
}
abstract class RemoteBookingDataSource {
Future<booking> saveBooking();
}Guideline Recommendations:
class SQLiteBookingDataSource implements LocalBookingDataSource {
//...............Implementation................
}
class RestApiBookingDataSource implements RemoteBookingDataSource {
//...............Implementation................
}
class MemoryBookingDataSource implements LocalBookingDataSource {
//...............Implementation................
}Data transfer objects - used to transfer data from one system to another. Example
class ApiRequest {
//...fields...
}
class ApiResponse {
//...fields...
}class SQLiteTableDefinition {
//...fields...
}Guidelines:
class ApiRequest {
//...fields...
factory ApiRequest.fromDO(DOObject doObject) {
//...mapper code...
}
DOObject toDO() {
//...mapper code...
}
}Domain Object are data classes representation of a part of the business or an item within it, they are used to execute business logic independently of platform, library or tools.
A domain object may represent, for example, a person, place, event, business process, or concept and invoice, a product, a transaction or even details of a person.
Guideline:
The same as for Project code structure.
First the feature name, then the assets folder containing files.
Example:
|-- authentication/
|---- assets/
|------ password_hidden_icon.svg
|------ forget_password_icon.svg
|------ background.png