Project Structure#
We suggest you organize your app into Features, with the assumption that features don't depend on each other. The goal should be to be able to delete a feature completely and don't break any code.
Each feature could be organized in this way:
lib
providers.dart
features
feature
domain
feature_entity.dart
feature_ui_output.dart
feature_use_case.dart
feature_input.dart
external_interface
feature_gateway.dart
presentation
feature_presenter.dart
feature_ui.dart
feature_view_model.dart
Notice that the name of the feature is a prefix for all the files inside. We prefer this naming convention so they are easier to identify on searches, but you are free to follow any convention that suits your need.
The folder structure is also a suggestion, you can add multiple layers if the feature begins to grow and have multiple screens and interactions.
The Providers#
Use Cases, Gateways and External Interfaces are instances of classes that are not Flutter Widgets, so they are not dependant on the Flutter Context. To have access to them, you can "publish" them using the Providers pattern.
If you notice on the files list shown above, outside the features folder we have a file where we list all the providers used on the app. For large projects this is probably not the best idea, since this file can be long and bloated, so probably splitting the providers by feature could work better.
This is an example on how this file can be coded:
final featureUseCaseProvider = UseCaseProvider(FeatureUseCase.new);
final featureGatewayProvider = GatewayProvider(
FeatureGateway.new
useCases: [featureUseCaseProvider],
);
final graphQLExternalInterfaceProvider = ExternalInterfaceProvider(
GraphQLExternalInterface.new
gateways: [featureGatewayProvider],
);
Clean Framework uses Riverpod for the Providers behavior, so you can understand why the providers are global instances. For anyone not familiar to how Riverpod works, this might seem inappropriate, specially coming from a strict OO formation. Justifying why this is useful and desirable, please refer to the Riverpod documentation, since the creator already did a great job explaining this approach.
Providers create instances lazily, but some of the listeners need to be connected before use cases make any request. That is why we need to "touch" all gateway and external interfaces providers to ensure they are created when the app starts.
Adding external interface providers to the externalInterfaceProviders
in AppProviderScope
will ensure that all external interfaces are created.
void main() {
runApp(
AppProviderScope(
externalInterfaceProviders: [
graphQLExternalInterfaceProvider,
],
),
);
}