App Module
Dependency Injection Package
🎯 Provide a dependency injection container.
🎯 Provide platform annotations for dependency injection.
This package uses get_it to offer the applications central dependency injection container. Other packages will retrieve dependencies from it and use injectable to register them beforehand. In addition to providing the dependency container, this package introduces annotations that enable the registration of specific dependencies for particular platforms.
Logging Package
🎯 Provide a logger.
The primary goal of this package is to offer a single, dedicated logger that can be effortlessly employed throughout the entire application. By default, an implementation is provided, yet developers have the freedom to either create their own customized logger or use existing solutions like for example logger or logging.
Platform Root Package
🎯 Provide entrypoints to run the application in different environments.
🎯 Setup Dependency Injection.
🎯 Setup Routing.
🎯 Provide a BlocObserver.
🎯 Provide a RouterObserver.
🎯 Integration Testing.
This package provides entry points for different environments via main_development.dart
, main_test.dart
, and main_prod.dart
, where the application gets set up depending on the needs of each environment. It initializes dependency injection, routing, logging and hosts the native application. The package is also the location where integration tests take place.
More information about testing a Platform Root Package can be found here.
Platform Navigation Package
🎯 Provide interfaces to navigate between feature packages.
The primary goal of this package is to decouple Platform Feature Packages and allow navigation between them. This is achieved by defining Navigator Interfaces for each feature package that will be navigated to from within another feature package. The interface will then be implemented in the associated feature package using a Navigator Implementation which is registred to the dependency injection container. Feature packages that want to navigate to the feature package can then use the injected implementation instead of depending on the feature package directly.
Navigator Interface
A navigator interface is a component which defines an interface to navigate to a feature package.
This can be common navigation methods like push
, replace
and more.
// i_home_page_navigator.dart
abstract class IHomePageNavigator {
// add navigation methods (e.g push, replace, ...) here
}
Platform Localization Package
🎯 Provide translations for different languages.
The main purpose of this package is to provide localization for the application. This is done using .arb
files via flutter_localizations and intl.
Platform Feature Package
There are several diffrent types of feature packages. See below.
Platform App Feature Package
🎯 Provide the root widget of the application.
🎯 Provide global state of the application.
The app feature package must be present as it provides the root App
widget.
It is also the location where global state is implemented.
Platform Page Feature Package
🎯 Provide a page widget of the application.
🎯 Provide state of the page.
A page feature package is used when adding a simple page to the application.
Platform Flow Feature Package
🎯 Provide a flow widget of the application.
🎯 Provide state of the flow.
A flow feature package is used when adding nested navigation to the application.
Platform Tab Flow Feature Package
🎯 Provide a tab flow widget of the application.
🎯 Provide state of the tab flow.
A tab flow feature package is used when adding nested tab navigation to the application.
Platform Widget Feature Package
🎯 Provide a widget.
🎯 Provide state of the widget.
A widget feature package is used for parts of UI that are not abstract enough to be part of the design language but is shared accross multiple other feature packages.
Bloc
A bloc is a component which manages the state of a feature package. Events can be added to the bloc and states are emitted by the bloc. The UI of a feature package can then listen to these state changes and rebuild properly. See bloc and flutter_bloc for more information.
// counter_bloc.dart
@android
@injectable
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc()
: super(
// Set initial state
0,
) {
// Register handlers
on<CounterStarted>(_onIncrement);
}
/// Handle incoming [CounterIncrement] event.
void _onIncrement(
CounterIncrement event,
Emitter<int> emit,
) {
emit(state + 1);
}
}
// counter_event.dart
@freezed
sealed class CounterEvent with _$CounterEvent {
const factory CounterEvent.increment() = CounterIncrement;
}
// counter_state.dart
// (not needed in this example)
Cubit
Cubit is a less complex version of a bloc where events are not represented by a seperate class but using methods instead. It has the same purpose and can be used interchangeably.
// counter_cubit.dart
@android
@injectable
class CounterCubit extends Cubit<int> {
CounterCubit()
: super(
// Set initial state
0,
);
void started() {
emit(state + 1);
}
}
// counter_state.dart
// (not needed in this example)
Navigator Implementation
A navigator implementation is a component which implements its associated Navigator Interface. By implementing the interface from within the feature package and registering it in the dependency injection container decoupling between feature packages is achieved.
// navigator.dart
@android
@LazySingleton(as: IHomePageNavigator)
class HomePageNavigator implements IHomePageNavigator {
// implement navigation methods (e.g push, replace, ...) here
}
More information about testing a Platform Feature Package can be found here.
Domain Package
🎯 Represent a domain using Entities, Service Interfaces and Value Objects.
A domain package models a specific domain using domain components. It serves as the boundary to the external world and is implemented by its associated Infrastructure Package. Objects exposed by this package are immutable, and exceptions are represented by seperate failure classes (see Value Object, Service Interface) to achieve domain safety.
Domain safety refers to the domain being designed in an safe way, mitigating possible errors introduced by mutable models and uncaught exceptions.
Entity
An entity is a component which represents a immutable unit which is part of a domain.
// user.dart
@freezed
class User with _$User {
const factory User({
required String id,
// add more fields here
}) = _User;
factory User.random() {
final faker = Faker();
return User(
id: faker.randomGenerator.string(16, min: 16),
);
}
}
Service Interface
A service interface component defines an interface for actions the application needs to perform its business logic. Every method of a service interface defines its own result union type and map possbile exceptions thrown by the method to failure cases. (Hint errors should not be mapped only exceptions should as they are intended to be handled by clients using the domain package)
// i_authentication_service.dart
abstract class IAuthenticationService {
SingInResult signIn({
required EmailAddress email,
required Password password,
});
// add more service methods
}
sealed class SingInResult {
const SingInResult();
}
@freezed
class SignInSuccess extends SingInResult with _$SignInSuccess {
const factory SignInSuccess(String value) = _SignInSuccess;
const SignInSuccess._();
}
@freezed
sealed class SignInFailure extends SignInResult with _$SignInFailure {
const SignInFailure._();
const factory SignInFailure.invalidEmail() = SignInFailureInvalidEmail;
const factory SignInFailure.invalidPassword() = SignInFailureInvalidPassword;
const factory SignInFailure.serverNotReachable() = SignInFailureServerNotReachable;
// add more failure cases here
}
// add more service method results here
Value Object
A value object is a component which wraps an other type (primitives, collections or other entities) and validates its value. Based on the provided value the value object union is either mapped to a valid or failure state. This allows to build a more expressive domain language and write safer code.
// email_address.dart
sealed class EmailAddress {
factory EmailAddress(String raw) {
return _validate(raw);
}
factory EmailAddress.random({bool valid = true}) {
final faker = Faker();
if (valid) {
return faker.randomGenerator.element([
// insert random valid instances here
]);
} else {
return faker.randomGenerator.element([
// insert random invalid instances here
]);
}
}
const EmailAddress._();
static EmailAddress _validate(String raw) {
// implement validation here
}
String getOrCrash() {
return switch (this) {
final ValidEmailAddress valid => valid.value,
final EmailAddressFailure failure =>
throw StateError('Unexpected $failure at unrecoverable point.'),
};
}
bool isValid() => this is ValidEmailAddress;
}
@freezed
class ValidEmailAddress extends EmailAddress with _$ValidEmailAddress {
const factory ValidEmailAddress(String value) = _ValidEmailAddress;
const ValidEmailAddress._() : super._();
}
@freezed
sealed class EmailAddressFailure extends EmailAddress
with _$EmailAddressFailure {
const EmailAddressFailure._() : super._();
const factory EmailAddressFailure.missingLocalPart() = EmailAddressFailureFoo;
const factory EmailAddressFailure.missingDomainPart() = EmailAddressFailureFoo;
// add more failures here
}
More information about testing a Domain Package can be found here.
Infrastructure Package
🎯 Implement its associated Domain Package.
An infrastructure package implements the interface defined in its associated Domain Package. This is done interacting with external or local data sources and services, such as databases or APIs, to fetch or store data as needed. Every infrastructure package can provide multiple Service Implementations per Service Interface which can be easily injected depending on environment.
Data Transfer Object
A data transfer object is a component which is associated to an Entity. Its only purpose is to transform data from the outside world (mostly json) to an Entity and back.
// user_dto.dart
@freezed
class UserDto with _$UserDto {
const factory UserDto({
required String id,
// add more fields here
}) = _UserDto;
factory UserDto.fromDomain(User domain) {
return UserDto(
id: domain.id,
);
}
factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
const UserDto._();
User toDomain() {
return User(
id: id,
);
}
}
Service Implementation
A service implementation implements the interface specified by its corresponding Service Interface, handling all the technical details, using SDKS, Rest APIs or other ways to communicated with the external world or the device. When implementing the interface it is important to map exceptions to the correct failures of the respective result and let errors bubble up to be handled at the root level.
// fake_authentication_service.dart
@dev
@LazySingleton(as: IAuthenticationService)
class FakeAuthenticationService implements IAuthenticationService {
@override
SingInResult signIn({
required EmailAddress email,
required Password password,
}){
// implement
}
}
More information about testing an Infrastructure Package can be found here.