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,
      ],
    ),
  );
}