Let's dive into the typical structure of a Clean Framework project.

Organizing Your App into Features

We encourage you to organize your app into separate, independent features. The key is that these features should not depend on each other; ideally, a feature should be able to be deleted entirely without breaking any of the code.

Here's a suggested layout for each feature:

lib
    providers.dart
    features
        your_feature
            domain
                your_feature_entity.dart
                your_feature_domain_models.dart
                your_feature_use_case.dart
            external_interface
                your_feature_gateway.dart
            presentation
                your_feature_presenter.dart
                your_feature_ui.dart
                your_feature_view_model.dart

In this structure, each file within a feature begins with the feature's name, making it easier to locate feature-specific files in searches. This naming convention is just a recommendation, and you're welcome to use any system that suits your project.

As can be seen, we typically organize the different components of a given feature around the layer they belong to in Clean Architecture:

  • domain: Contains the feature's Entity, Use Case, and DomainInputs/DomainModels; all of the core business logic for the feature.
  • external_interface: Contains any Gateways or ExternalInterfaces required by the Domain layer to interact with external services or dependencies.
  • presentation: Covers the UI layer, which will be explained in the next section, containing the Presenter, UI, and ViewModels for the feature. All UI-related logic needed by the feature should reside here, chiefly within the Presenter class.

As your feature grows, feel free to expand the folder structure to accommodate multiple screens and interactions.

Understanding the Providers

In Clean Framework, components like Use Cases, Gateways, and External Interfaces are not Flutter Widgets. Consequently, they don’t depend on Flutter’s Context. To access them, they are "published" using the Providers pattern.

In the project structure, we have a file called lib/providers.dart outside the features folder. This file is where all the providers used in the app reside. For larger projects, it's advised to organize the providers according feature to prevent the file from becoming too lengthy.

Here's an example of what the lib/providers.dart file might contain:

final featureUseCaseProvider = UseCaseProvider(FeatureUseCase.new);

final featureGatewayProvider = GatewayProvider(
  FeatureGateway.new
  useCases: [featureUseCaseProvider],
);

final graphQLExternalInterfaceProvider = ExternalInterfaceProvider(
  GraphQLExternalInterface.new
  gateways: [featureGatewayProvider],
);

Clean Framework uses Riverpod for its provider behavior, which means providers are declared globally. If you're new to Riverpod, this approach may seem unusual. To learn more about why this is desirable, the Riverpod documentation does a great job of explaining this approach.

Providers in Clean Framework create instances lazily. However, some listeners need to be connected before use cases can make requests. Therefore, it’s necessary to "touch" all gateway and external interface providers to ensure they're created when the app starts.

To guarantee all external interfaces are created, add external interface providers to the externalInterfaceProviders in the AppProviderScope of your main function:

void main() {
  runApp(
    AppProviderScope(
      externalInterfaceProviders: [
        graphQLExternalInterfaceProvider,
      ],
    ),
  );
}

Now that we've gone over some of the basics, lets move on to the UI layer and begin writing a simple app!