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 anyGateway
s orExternalInterface
s 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 thePresenter
,UI
, andViewModel
s for the feature. All UI-related logic needed by the feature should reside here, chiefly within thePresenter
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!