diff --git a/.gitignore b/.gitignore
index 4350c4b2..e3ebbb4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,6 +81,27 @@ Podfile.lock
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
+# macos/XCode related
+**/macos/**/*.mode1v3
+**/macos/**/*.mode2v3
+**/macos/**/*.moved-aside
+**/macos/**/*.pbxuser
+**/macos/**/*.perspectivev3
+**/macos/**/*sync/
+**/macos/**/.sconsign.dblite
+**/macos/**/.tags*
+**/macos/**/.vagrant/
+**/macos/**/DerivedData/
+**/macos/**/Icon?
+**/macos/**/Pods/
+**/macos/**/.symlinks/
+**/macos/**/profile
+**/macos/**/xcuserdata
+**/macos/.generated/
+**/macos/Flutter/
+**/macos/ServiceDefinitions.json
+**/macos/Runner/GeneratedPluginRegistrant.*
+
# Coverage
coverage/
coverage_badge.svg
diff --git a/docs.json b/docs.json
index 9276568e..426a47a6 100644
--- a/docs.json
+++ b/docs.json
@@ -11,7 +11,7 @@
["Setup","/codelabs/clean-framework/setup"],
["UI Layer","/codelabs/clean-framework/ui-layer"],
["Domain Layer","/codelabs/clean-framework/domain-layer"],
- ["Adaptive Layer","/codelabs/clean-framework/adaptive-layer"],
+ ["Adapter Layer","/codelabs/clean-framework/adapter-layer"],
["External Interface Layer","/codelabs/clean-framework/external-interface-layer"]
]
],
@@ -29,6 +29,5 @@
]
],
["Other Resources", "/resources"]
-
]
}
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/adaptive-layer.mdx b/docs/codelabs/clean-framework/adapter-layer.mdx
similarity index 82%
rename from docs/codelabs/clean-framework/adaptive-layer.mdx
rename to docs/codelabs/clean-framework/adapter-layer.mdx
index 7f85d381..58ec9367 100644
--- a/docs/codelabs/clean-framework/adaptive-layer.mdx
+++ b/docs/codelabs/clean-framework/adapter-layer.mdx
@@ -1,16 +1,17 @@
# Adapter Layer: Gateways
-We already learned part of this layer componets with the Presenter and View Model. The only thing left to learn here are the Gateways, which handle Outputs used as requests, and create Inputs to be processed by the Use Case.
+We already learned part of this layer components with the Presenter and View Model.
+The only thing left to learn here are the Gateways, which handle Outputs used as requests, and create Inputs to be processed by the Use Case.
Let's look at a simple example first:
```dart
-class MyGateway extends Gateway {
+class MyGateway extends Gateway {
LastLoginDateGateway({ProvidersContext? context, UseCaseProvider? provider})
: super(
- context: context ?? providersContext,
- provider: provider ?? lastLoginUseCaseProvider);
+ context: context ?? providersContext,
+ provider: provider ?? lastLoginUseCaseProvider,
+ );
@override
MyRequest buildRequest(MyOutput output) {
@@ -18,7 +19,7 @@ class MyGateway extends Gateway AddMachineUI(provider: addMachineUseCaseProvider),
setup: () {
- final gateway = AddMachineGetTotalGateway(
- context: context, provider: addMachineUseCaseProvider);
- gateway.transport =
- (request) async => Right(AddMachineGetTotalResponse(740));
+ final gateway = AddMachineGetTotalGateway(context: context, provider: addMachineUseCaseProvider);
+ gateway.transport = (request) async => Right(AddMachineGetTotalResponse(740));
final gatewayProvider = GatewayProvider((_) => gateway);
gatewayProvider.getGateway(context);
},
verify: (tester) async {
- expect(find.descendant(of: sumTotalWidget, matching: find.text('740')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('740')), findsOneWidget);
},
);
@@ -135,7 +133,7 @@ class AddMachineGetTotalGateway extends Gateway<
}
@override
- onSuccess(covariant AddMachineGetTotalResponse response) {
+ onSuccess(AddMachineGetTotalResponse response) {
return AddMachineGetTotalInput(response.number);
}
}
@@ -159,23 +157,28 @@ And with this code, the only thing we need to do is make the UseCase do a reques
```dart
class AddMachineUseCase extends UseCase {
AddMachineUseCase()
- : super(entity: AddMachineEntity(0), outputFilters: {
- AddMachineUIOutput: (AddMachineEntity e) =>
- AddMachineUIOutput(total: e.total),
- }, inputFilters: {
- AddMachineAddNumberInput:
- (AddMachineAddNumberInput i, AddMachineEntity e) =>
- AddMachineEntity(i.number + e.total),
- }) {
+ : super(
+ entity: AddMachineEntity(0),
+ transformers: [
+ OutputTransformer.from(
+ (entity) => AddMachineUIOutput(total: entity.total),
+ ),
+ InputTransformer.from(
+ (entity, input) => AddMachineEntity(entity.total + input.number),
+ ),
+ ],
+ ) {
onCreate();
}
void onCreate() {
- request(AddMachineGetTotalOutput(),
- onSuccess: (AddMachineGetTotalInput input) {
- return AddMachineEntity(input.number);
- },
- onFailure: (_) => entity);
+ request(
+ AddMachineGetTotalOutput(),
+ onSuccess: (input) {
+ return AddMachineEntity(input.number);
+ },
+ onFailure: (_) => entity,
+ );
}
}
```
@@ -187,18 +190,18 @@ Here we are adding a way to trigger a request. This **onCreate** method will be
class AddMachinePresenter extends Presenter {
AddMachinePresenter({
- required UseCaseProvider provider,
- required PresenterBuilder builder,
- }) : super(provider: provider, builder: builder);
+ required super.provider,
+ required super.builder,
+ });
@override
AddMachineViewModel createViewModel(useCase, output) => AddMachineViewModel(
total: output.total.toString(),
- onAddNumber: (number) => _onAddNumber(useCase, number));
+ onAddNumber: (number) => _onAddNumber(useCase, number),
+ );
void _onAddNumber(useCase, String number) {
- useCase.setInput(
- AddMachineAddNumberInput(int.parse(number)));
+ useCase.setInput(AddMachineAddNumberInput(int.parse(number)));
}
@override
diff --git a/docs/codelabs/clean-framework/domain-layer.mdx b/docs/codelabs/clean-framework/domain-layer.mdx
index 0c7d7c65..7fd90c0b 100644
--- a/docs/codelabs/clean-framework/domain-layer.mdx
+++ b/docs/codelabs/clean-framework/domain-layer.mdx
@@ -7,7 +7,7 @@ Let's start by understanding the Entities. If you are familiar with Domain Drive
The sum of all your Entities is the state of the whole feature. This state will be kept alive as long as its Use Case exists. Since we create it when the app is executed (using a provider), this reference is alive until the app is removed from memory.
-So it is important to understand that this state needs initial values and rules governing how those values chage. When writing an Entity, try to follow these rules:
+So it is important to understand that this state needs initial values and rules governing how those values change. When writing an Entity, try to follow these rules:
1. Entities don't depend on other files or libraries except for the clean framework import. This is the most central layer, so it should not need anything, not even from other features. Shared enums are even problematic, since feature requirements could change, forcing you to refactor the affected features.
@@ -17,25 +17,25 @@ So it is important to understand that this state needs initial values and rules
1. It is OK to create a hierarchy of entities, but keep a single ancestor that the Use Case can create easily. Composition is much better than inheritance. Functional constructs like Either and Unions are useful here as well.
-1. Add generators like **copyWith** or **merge** to create instances based on current values. This simplifies the Use Case code.
+1. Add generators like **copyWith** to create instances based on current values. This simplifies the Use Case code.
It is OK to add methods to validate the consistency of the data. For example:
```dart
class AccountEntity extends Entity{
+ AccountEntity({required this.isRegistered, this.userName});
+
final bool isRegistered;
final UserNameEntity userName;
-
- AccountEntity({required this.isRegistered, this.userName});
}
class UserNameEntity extends Entity{
+ UserNameEntity({required this.firstName, this.lastName}) : assert(firstName.isNotEmpty() && lastName.isNotEmpty);
+
final String firstName;
final String lastName;
- UserNameEntity({required this.firstName, this.lastName}) : assert(firstName.isNotEmpty() && lastName.isNotEmpty);
-
String get fullName => firstName + ' ' + lastName;
}
```
@@ -56,16 +56,24 @@ Be careful to not add logic that doesn't belong to the Entities, or you will be
### Use Case
-Use Cases live outside the Entities, on its own layer. Use Cases will create and manipulate Entities internally, while transfering data from Inputs and into Outputs. Lets look at one simple example to understand the class:
+Use Cases live outside the Entities, on its own layer.
+Use Cases will create and manipulate Entities internally, while transferring data from Inputs and into Outputs.
+Lets look at one simple example to understand the class:
```dart
class MyUseCase extends UseCase {
MyUseCase()
- : super(entity: MyEntity(), outputFilters: {
- MyUIOutput: (MyEntity e) => MyUIOutput(data: e.data),
- }, inputFilters: {
- MyInput: (MyInput i, MyEntity e) => e.copyWith(data: i.data),
- });
+ : super(
+ entity: MyEntity(),
+ transformers: [
+ OutputTransformer.from(
+ (entity) => MyUIOutput(data: entity.data),
+ ),
+ InputTransformer.from(
+ (entity, input) => entity.copyWith(data: input.data),
+ ),
+ ],
+ );
}
```
@@ -75,11 +83,11 @@ Here, MyUseCase has only one output, so the Presenter only needs to listen to My
Notice that the filter is a Map of the type of the Output and a function that receives the current Entity instance. It is intended to do it this way so its easier to isolate the code and help the developer think on simple terms and avoid having complex method calls.
-Outputs are meant to only hold a subset of the data available in the Entity, and the way the Presenter and UseCase communication works internally, a new Output is **only** generated if the fields used for its construction chage. In this example, the Use Case can alter the Entity, but if the **data** field remains the same, no new Output is created.
+Outputs are meant to only hold a subset of the data available in the Entity, and the way the Presenter and UseCase communication works internally, a new Output is **only** generated if the fields used for its construction change. In this example, the Use Case can alter the Entity, but if the **data** field remains the same, no new Output is created.
Input filters work in a similar way. If a Gateway is attached to a Use Case, it produces a specific type of Input. This class allows a Gateway to send a MyInput instance, which will be used by the input filter anonymous method to create a new version of the Entity based on the data received.
-So this means that a MyInput instance is received, it will trigger a Entity change on the data field, and thus generate a new MyUIOuput.
+So this means that a MyInput instance is received, it will trigger a Entity change on the data field, and thus generate a new MyUIOutput.
Entities can be changed at any time in other methods inside the Use Case, as in here:
@@ -87,10 +95,11 @@ Entities can be changed at any time in other methods inside the Use Case, as in
// Method inside the Use Case
void updateAmount(double newAmount){
- if (entity.isAmountValid(newAmount))
- entity = entity.merge(amount: newAmount);
- else
- entity = entity.merge(error: Errors.invalidAmount);
+ if (entity.isAmountValid(newAmount)) {
+ entity = entity.copyWith(amount: newAmount);
+ } else {
+ entity = entity.copyWith(error: Errors.invalidAmount);
+ }
}
```
@@ -98,7 +107,7 @@ The **entity** attribute is available in any UseCase. Each time we need to chang
### Outputs for Presenters and Gateways
-Use Cases have no knowledge of the world of the ouside layers. They only create Outputs that can be listened by anything. That is why you have to keep the implementation independant from any assumption about the data.
+Use Cases have no knowledge of the world of the outside layers. They only create Outputs that can be listened by anything. That is why you have to keep the implementation independent from any assumption about the data.
For example, an Output can contain data that will be stored in a database, visualized on a screen, or sent to a service. Only the external layers will determine where the data goes and how it is used.
@@ -108,12 +117,15 @@ But to create outputs on demand and wait for some kind of response from the outs
```dart
void fetchUserData(){
- await request(FetchUserDataOutput(), onSuccess: (UserDataInput input) {
- return entity.merge(
- name: input.name);
- }, onFailure: (_) {
- return entity.merge(error: Error.dataFetchError);
- });
+ await request(
+ FetchUserDataOutput(),
+ onSuccess: (UserDataInput input) {
+ return entity.copyWith(name: input.name);
+ },
+ onFailure: (_) {
+ return entity.copyWith(error: Error.dataFetchError);
+ },
+ );
}
```
@@ -177,24 +189,24 @@ Now we are ready to continue the feature implementation we started on the previo
final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('15')), findsOneWidget);
await tester.enterText(numberField, '7');
await tester.tap(addButton);
await tester.pumpAndSettle();
- expect(find.descendant(of: sumTotalWidget, matching: find.text('22')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('22')), findsOneWidget);
},
);
// Replace the provider with these lines:
- final addMachineUseCaseProvider = UseCaseProvider((_) => StaticUseCase([
+ final addMachineUseCaseProvider = UseCaseProvider(
+ (_) => StaticUseCase([
AddMachineUIOutput(total: 0),
AddMachineUIOutput(total: 15),
AddMachineUIOutput(total: 22),
- ]));
+ ]),
+ );
```
This is basically a copy/paste of the previous test, the only needed change is the use case fake now returning an additional output.
@@ -204,10 +216,10 @@ Once we have this test coded and passing, its time for some major refactoring on
#### lib/features/add_machine/domain/add_machine_add_entity.dart
```dart
class AddMachineEntity extends Entity {
- final int total;
-
AddMachineEntity(this.total);
+ final int total;
+
@override
List get props => [total];
}
@@ -217,14 +229,17 @@ class AddMachineEntity extends Entity {
```dart
class AddMachineUseCase extends UseCase {
AddMachineUseCase()
- : super(entity: AddMachineEntity(0), outputFilters: {
- AddMachineUIOutput: (AddMachineEntity e) =>
- AddMachineUIOutput(total: e.total),
- }, inputFilters: {
- AddMachineAddNumberInput:
- (AddMachineAddNumberInput i, AddMachineEntity e) =>
- AddMachineEntity(i.number + e.total),
- });
+ : super(
+ entity: AddMachineEntity(0),
+ transformers: [
+ OutputTransformer.from(
+ (entity) => AddMachineUIOutput(total: entity.total),
+ ),
+ InputTransformer.from(
+ (entity, input) => AddMachineEntity(entity.total + input.number),
+ ),
+ ],
+ );
}
```
diff --git a/docs/codelabs/clean-framework/external-interface-layer.mdx b/docs/codelabs/clean-framework/external-interface-layer.mdx
index 060230de..fc3f392a 100644
--- a/docs/codelabs/clean-framework/external-interface-layer.mdx
+++ b/docs/codelabs/clean-framework/external-interface-layer.mdx
@@ -1,5 +1,7 @@
# External Interface Layer
-The final piece of the Framework is the most flexible one, since it work as a wrapper for any external dependency code from libraries and modules. If coded properly, they will protect you from dependencies migrations and version upgrades.
+The final piece of the Framework is the most flexible one, since it work as a wrapper for any external dependency code from libraries and modules.
+If coded properly, they will protect you from dependencies migrations and version upgrades.
+
As usual, let's study the example first:
@@ -56,8 +58,7 @@ This is the remaining code:
#### lib/features/add_machine/external_interface/add_machine_external_interface.dart
```dart
-class AddMachineExternalInterface
- extends ExternalInterface {
+class AddMachineExternalInterface extends ExternalInterface {
int _savedNumber;
AddMachineExternalInterface({
@@ -131,28 +132,35 @@ And here are the changes for the rest of components:
```dart
class AddMachineUseCase extends UseCase {
AddMachineUseCase()
- : super(entity: AddMachineEntity(0), outputFilters: {
- AddMachineUIOutput: (AddMachineEntity e) =>
- AddMachineUIOutput(total: e.total),
- }) {
+ : super(
+ entity: AddMachineEntity(0),
+ transformers: [
+ OutputTransformer.from(
+ (entity) => AddMachineUIOutput(total: entity.total),
+ ),
+ ],
+ ) {
onCreate();
}
void onAddNumber(int number) async {
- await request(AddMachineSetTotalOutput(number + entity.total),
- onSuccess: (AddMachineGetTotalInput input) {
- return AddMachineEntity(input.number);
- }, onFailure: (_) {
- return entity;
- });
+ await request(
+ AddMachineSetTotalOutput(number + entity.total),
+ onSuccess: (AddMachineGetTotalInput input) {
+ return AddMachineEntity(input.number);
+ },
+ onFailure: (_) => entity,
+ );
}
void onCreate() async {
- await request(AddMachineGetTotalOutput(),
- onSuccess: (AddMachineGetTotalInput input) {
- return AddMachineEntity(input.number);
- },
- onFailure: (_) => entity);
+ await request(
+ AddMachineGetTotalOutput(),
+ onSuccess: (AddMachineGetTotalInput input) {
+ return AddMachineEntity(input.number);
+ },
+ onFailure: (_) => entity,
+ );
}
}
```
@@ -169,7 +177,8 @@ class AddMachinePresenter extends Presenter AddMachineViewModel(
total: output.total.toString(),
- onAddNumber: (number) => _onAddNumber(useCase, number));
+ onAddNumber: (number) => _onAddNumber(useCase, number)
+ );
void _onAddNumber(AddMachineUseCase useCase, String number) {
useCase.onAddNumber(int.parse(number));
@@ -197,11 +206,9 @@ void main() {
void setup() {
addMachineUseCaseProvider = UseCaseProvider((_) => AddMachineUseCase());
getTotalGatewayProvider = GatewayProvider((_) =>
- AddMachineGetTotalGateway(
- context: context, provider: addMachineUseCaseProvider));
+ AddMachineGetTotalGateway(context: context, provider: addMachineUseCaseProvider));
setTotalGatewayProvider = GatewayProvider((_) =>
- AddMachineSetTotalGateway(
- context: context, provider: addMachineUseCaseProvider));
+ AddMachineSetTotalGateway(context: context, provider: addMachineUseCaseProvider));
externalInterfaceProvider = ExternalInterfaceProvider((_) =>
AddMachineExternalInterface(
@@ -227,8 +234,7 @@ void main() {
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('0')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('0')), findsOneWidget);
},
);
@@ -256,8 +262,7 @@ void main() {
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('15')), findsOneWidget);
},
);
@@ -283,15 +288,13 @@ void main() {
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('15')), findsOneWidget);
await tester.enterText(numberField, '7');
await tester.tap(addButton);
await tester.pumpAndSettle();
- expect(find.descendant(of: sumTotalWidget, matching: find.text('22')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('22')), findsOneWidget);
},
);
@@ -312,8 +315,7 @@ void main() {
gateway.transport(AddMachineSetTotalRequest(740));
},
verify: (tester) async {
- expect(find.descendant(of: sumTotalWidget, matching: find.text('740')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('740')), findsOneWidget);
},
);
}
diff --git a/docs/codelabs/clean-framework/intro.mdx b/docs/codelabs/clean-framework/intro.mdx
index c3cb1c48..35f70a91 100644
--- a/docs/codelabs/clean-framework/intro.mdx
+++ b/docs/codelabs/clean-framework/intro.mdx
@@ -1,45 +1,73 @@
# OverView
-Clean Framework is a toolkit of classes and implementations that help any developer create a layered architecture on any app, following the principles of Clean Architecture from Uncle Bob (Robert Martin).
+Clean Framework is a toolkit of classes and implementations that help any developer create a layered architecture on any app,
+following the principles of Clean Architecture from Uncle Bob (Robert Martin).
## The Layers
-To understand the components, first we have to talk about the layers, which are just a way to group your code to avoid interdependencies and to separate concerns.
+To understand the components,
+first we have to talk about the layers, which are just a way to group your code to avoid interdependencies and to separate concerns.
The following diagram explains how the Clean Architecture proposes the implementation of the layers.
-.|.
----|---
-The idea of layering the architecture to separate the domain logic from the implementation details is not recent, and some other approaches have also been proposed (like the Hexagonal Architecture). Bob Martin took good ideas from the existing proposals, so some of the terms may seem familiar.
+The idea of layering the architecture to separate the domain logic from the implementation details is not recent,
+and some other approaches have also been proposed (like the Hexagonal Architecture).
+Bob Martin took good ideas from the existing proposals, so some of the terms may seem familiar.
### Entities Layer
-The core of your app should exist within this layer. Here we have Entity instances that hold the state of all your features. These entities are immutable and should be free of any external code, they should not care about databases, UI, or services. If you are familiar with Domain Driven Design, this is considered your Domain data.
+The core of your app should exist within this layer.
+Here we have Entity instances that hold the state of your features.
+These entities are immutable and should be free of any external code, they should not care about databases, UI, or services.
+If you are familiar with Domain Driven Design, this is considered your Domain data.
### Use Cases Layer
-The Use Case is an object that handles the data in the Entities and redirects the flows of data. Use Cases will hold most of the business logic of your features.
+The Use Case is an object that handles the data in the Entities and redirects the flows of data.
+Use Cases will hold most of the business logic of your features.
-Use Cases handle two classes, Input and Output, which move data inside or outside respectively, they are very similar to DDD Events. The next layer can only use these components to send and receive data from the Entities. Since they are simple PODOs (Plain Old Dart Objects), they are completely agnostic from the implementation of the outside layer, and this means the Use Case will usually interact with any type of object without worrying about the details.
+Use Cases handle two classes, Input and Output, which move data inside or outside respectively, they are very similar to DDD Events.
+The next layer can only use these components to send and receive data from the Entities.
+Since they are simple PODOs (Plain Old Dart Objects), they are completely agnostic from the implementation of the outside layer,
+and this means the Use Case will usually interact with any type of object without worrying about the details.
-To interact with the Outputs and Inputs, Use Cases use requests and filters, and these interactions can be synchronous or subscriptions.
+To interact with the Outputs and Inputs, Use Cases use `requests` and `transformers`,
+and these interactions can be synchronous or subscriptions.
### Adapters Layer
-The goal of this layer is to translate the Inputs and Outputs from the Use Case into more specific messages for specific destinations. These components have a similar function than the BDD Adapter. We have to main components, the Presenter and the Gateway
+The goal of this layer is to translate the `Input`s and `Output`s from the Use Case into more specific messages for specific destinations.
+These components have a similar function as the BDD Adapter. We have two main components, the `Presenter` and the `Gateway`:
-### Presenter
-It's job is to translate Outputs into ViewModels, which are contain data and behavior (in the form of callbacks). This class will hold most of your UI logic that is not business related, like navigation.
+#### Presenters
+It's job is to translate `Output`s into `ViewModel`s, which are contain data and behavior _(in the form of callbacks)_.
+This class will hold most of your UI logic that is not business related, like navigation.
-Presenters will interact with providers of Use Cases to subscribe to a specific Output, so when that output gets updated, we can schedule a refresh on the UI side. Once the Presenter receives the updated Output, it will create a new View Model to be processed by the UI.
+Presenters will interact with providers of Use Cases to subscribe to a specific `Output`,
+so when that output gets updated, we can schedule a refresh on the UI side.
+Once the Presenter receives the updated Output, it will create a new View Model to be processed by the UI.
-### Gateway
-When you need external data from sources like REST servers, databases, hardware, cache, etc. Use Cases will send requests with an specific Output. This message will be listened by a Gateway, which translates the Output data into a request that can be processed by the next layer.
+#### Gateways
+When you need external data from sources like REST servers, databases, hardware, cache, etc.
+Use Cases will send requests with an specific `Output`.
+This message will be listened by a `Gateway`,
+which translates the `Output` data into a request that can be processed by the next layer.
-There are two types of Gateway, depending on how you need the response to be delivered. The base Gateway class handles requests and waits for a response on the same interaction, blocking the execution until a response or an error is received.
+There are two types of `Gateway`, depending on how you need the response to be delivered.
+The base `Gateway` class handles requests and waits for a response on the same interaction,
+blocking the execution until a response or an error is received.
-The other type is the WatcherGateway, which will create a subscription. Once the result is received and sent back to the UseCase, it will keep listening for subsequent responses, which are sent to the Use Case through the Input listener.
+The other type is the `WatcherGateway`,which will create a subscription.
+Once the result is received and sent back to the UseCase, it will keep listening for subsequent responses,
+which are sent to the Use Case through the Input listener.
### External Interfaces Layer
-This is where code from libraries and dependencies interacts with your features. Waits for Requests to happen and then process them depending on its type. Clean Framework include some ready-to-use default implementations to work with Firebase, GraphQL and REST services.
+This is where code from libraries and dependencies interacts with your features.
+Waits for Requests to happen and then process them depending on its type.
-The UI layer is considered a type of External Interface layer since it also relies on messages to an adapter (the Presenter) to send and receive state changes from the entities.
\ No newline at end of file
+Clean Framework include some ready-to-use default implementations to work with `GraphQL`, `REST` & `Cloud FireStore` services in the form of sub-packages.
+- [clean_framework_graphql](https://pub.dev/packages/clean_framework_graphql)
+- [clean_framework_rest](https://pub.dev/packages/clean_framework_rest)
+- [clean_framework_firestore](https://pub.dev/packages/clean_framework_firestore)
+
+The UI layer is considered a type of External Interface layer,
+since it also relies on messages to an adapter (the Presenter) to send and receive state changes from the entities.
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/setup.mdx b/docs/codelabs/clean-framework/setup.mdx
index 073fd4ee..75dfec68 100644
--- a/docs/codelabs/clean-framework/setup.mdx
+++ b/docs/codelabs/clean-framework/setup.mdx
@@ -1,13 +1,13 @@
# Setup
-To start using the Clean Framework components, you need to add the library on the pubspec.yaml of the project. Use the latest version available.
+To start using the Clean Framework components, you need to add the library on the pubspec.yaml of the project.
+Use the latest version available.
### Adding `clean_framework` package as a dependency
The project depends on the `clean_framework package`.
+
In the `pubspec.yaml`, add the following to `dependencies` section:
-```yaml
-clean_framework: any
-```
+
Or _you can just add the package directly using the following command:_
```
@@ -15,7 +15,8 @@ flutter pub add clean_framework
```
### 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.
+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:
@@ -23,35 +24,42 @@ Each feature could be organized in this way:
lib
providers_loader.dart
features
- my_new_feature
+ feature
domain
- my_new_feature_usecase.dart
- my_new_feature_entity.dart
- my_new_feature_outputs.dart
- my_new_feature_inputs.
+ feature_usecase.dart
+ feature_entity.dart
+ feature_outputs.dart
+ feature_inputs.dart
presentation
- my_new_feature_presenter.dart
- my_new_feature_view_model.dart
- my_new_feature_ui.dart
- external_interfaces
- my_new_feature_gateway.dart
+ feature_presenter.dart
+ feature_view_model.dart
+ feature_ui.dart
+ external_interface
+ feature_gateway.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 idenfiy on searches, but you are free to follow any convention that suits your need.
+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 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.
+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.
+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:
```dart
-final myNewFeatureUseCaseProvider =
- UseCaseProvider(
+final myNewFeatureUseCaseProvider = UseCaseProvider(
(_) => LastLoginUseCase(),
);
@@ -62,15 +70,21 @@ final myNewFeatureGatewayProvider = GatewayProvider(
void loadProviders() {
myNewFeatureUseCaseProvider.getUseCaseFromContext(providersContext);
- MyNewFeatureGatewayProvider.getGateway(providersContext);
+ myNewFeatureGatewayProvider.getGateway(providersContext);
restExternalInterface.getExternalInterface(providersContext);
}
```
-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 innapropiate, specially comming from a strict OO formation. Justifying why this is useful and desirable, please refer to the [Riverpod documentation](https://riverpod.dev/docs/concepts/providers), since the creator already did a great job explaining this approach.
+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](https://riverpod.dev/docs/concepts/providers),
+since the creator already did a great job explaining this approach.
-Providers create instances lazyly, but some of the listeners need to be connected before use cases make any request. That is why we use a global function to "touch" all gateway and external interfaces providers to ensure they are created when the app starts.
+Providers create instances lazyly, but some of the listeners need to be connected before use cases make any request.
+That is why we use a global function to "touch" all gateway and external interfaces providers to ensure they are created when the app starts.
The last consideration is to remember to use the function on the main function:
@@ -81,5 +95,6 @@ void main() {
}
```
-While working on this codelab, it won't be necessary to have this file from the beginning, you will see a box like this one to let you know when it will be needed.
+While working on this codelab, it won't be necessary to have this file from the beginning,
+you will see a box like this one to let you know when it will be needed.
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/ui-layer.mdx b/docs/codelabs/clean-framework/ui-layer.mdx
index 2a23c032..3b905e6c 100644
--- a/docs/codelabs/clean-framework/ui-layer.mdx
+++ b/docs/codelabs/clean-framework/ui-layer.mdx
@@ -1,31 +1,56 @@
# The UI Layer: UI, Presenter and View Model
+
Lets discuss in more detail the components of the UI Layer
+
-As mentioned on the previous topic, the UI component lives on the most external layer of the architecture. It means that it is related to specific libraries that conform the frontend of the application, in our case, the Flutter widgets libraries.
-When building an app using the Clean Framework classes, we try to separate as much as possible any code that is not related to pure UI logic and put that on the Presenter (to send and receive data from internal layers) and the Use Case (the normal location for business logic).
+As mentioned on the previous topic,
+the UI component lives on the most external layer of the architecture.
+It means that it is related to specific libraries that conform the frontend of the application,
+in our case, the Flutter widgets libraries.
+
+When building an app using the Clean Framework classes,
+we try to separate as much as possible any code that is not related to pure UI logic and put that on the Presenter (to send and receive data from internal layers)
+and the Use Case (the normal location for business logic).
-UI is a class that behaves like a Stateless Widget. It will be very rare that a Stateful Widget is needed, since the state usage for important data breaks the layer rules. Try to always think on ways the UI widgets without the need for Stateful Widgets.
+UI is a class that behaves like a Stateless Widget.
+It will be very rare that a Stateful Widget is needed, since the state usage for important data breaks the layer rules.
+Try to always think on ways the UI widgets without the need for Stateful Widgets.
-All UI implementations require at least one View Model to fetch data from the entities. This data comes from Use Case Outputs, which Presenters receive and translate as needed.
+All UI implementations require at least one View Model to fetch data from the entities.
+This data comes from Use Case Outputs, which Presenters receive and translate as needed.
-The feature you code can be expresed into multiple screens presented to the user, and even include small widgets that are inserted in other screens. These are your entry points to the feature, and as such, will require for the UI to listen to the state changes of the feature's Use Case through its Outputs. In other words, Use Cases can have multiple Outputs, that can have relationships with many View Models through the Presenters.
+The feature you code can be expressed into multiple screens presented to the user,
+and even include small widgets that are inserted in other screens.
+These are your entry points to the feature, and as such,
+will require for the UI to listen to the state changes of the feature's Use Case through its Outputs.
+In other words, Use Cases can have multiple Outputs, that can have relationships with many View Models through the Presenters.
-View Models are immutable classes, almost pure PODO's (Plain Old Dart Objects). We try to make them as lean as possible, because its only responsibility is the passing of digested data fields into the UI object.
+View Models are immutable classes, almost pure PODOs (Plain Old Dart Objects).
+We try to make them as lean as possible, because its only responsibility is the passing of digested data fields into the UI object.
-They tend to have only Strings. This is intentional since the Presenter has the responsibility of any formating and parsing done to the data.
+They tend to have only Strings.
+This is intentional since the Presenter has the responsibility of any formatting and parsing done to the data.
-Finally, the Presenters purpose is to connect and listen to Use Case Providers to interact with the Use Case instance and pass messages for user actions done on the UI (through callbacks on the View Model) and also to trigger rebuilds on the UI when the state changes causes a new Output to be generated. This will be explained in detail on the following sessions, so for now just asume the Presenters associate with only one type of Output.
+Finally, the Presenters purpose is to connect and listen to Use Case Providers to interact with the Use Case instance
+and pass messages for user actions done on the UI (through callbacks on the View Model)
+and also to trigger rebuilds on the UI when the state changes causes a new Output to be generated.
+This will be explained in detail on the following sessions, so for now just assume the Presenters associate with only one type of Output.
+
+The most important job of the Presenter is to translate an Output instance
+and create a new View Model everytime the Output is received.
-The most important job of the Presenter is to translate an Output instance and create a new View Model everytime the Output is received.
### Testing and Coding the UI Layer
-After a feature folder is created, any developer will probably try to start adding Flutter Widgets to build up the code requirements. This framework is flexible enough to allow you to start coding components that don't require to have any access or even knowledge of any possible dependency (databases, services, cache, etc), because those concerns belong to other layers.
+After a feature folder is created, any developer will probably try to start adding Flutter Widgets to build up the code requirements.
+This framework is flexible enough to allow you to start coding components that don't require to have any access
+or even knowledge of any possible dependency (databases, services, cache, etc), because those concerns belong to other layers.
-The simplest way to start working on a new feature is to first decide how many UI elements will be required to complete the implementation of the feature. For the sake of simplicity we are going to considering only one widget for the single screen of the new feature.
+The simplest way to start working on a new feature is to first decide how many UI elements will be required to complete the implementation of the feature.
+For the sake of simplicity we are going to considering only one widget for the single screen of the new feature.
-While working on this codelab, we will be creating the code by using TDD so we can focus on stablishing the desired outcome before explaining the code that produces it.
+While working on this codelab, we will be creating the code by using TDD so we can focus on establishing the desired outcome before explaining the code that produces it.
### The feature requirements
@@ -33,7 +58,6 @@ While working on this codelab, we will be creating the code by using TDD so we c
We are going to code a very simple feature which can be explained in a few Gherkin scenarios:
-
```gherkin
Given I have navigated to the Add Machine feature
Then I will see the Add Machine screen
@@ -88,7 +112,8 @@ void main() {
}
```
-After creating the initial blank project (using 'flutter create' for instance), you can add this test under the suggested path (features/add_machine/presentation).
+After creating the initial blank project (using 'flutter create' for instance),
+you can add this test under the suggested path (features/add_machine/presentation).
Be aware that TDD rules should cause the developer to not write more code than what is needed in order to make the test pass, but recreating the actual process will be lengthy for this codelab. We are oversimplifying the process here.
@@ -108,18 +133,19 @@ But in practice, we not only need that. UI is coupled to a valid ViewModel, whic
#### lib/features/add_machine/presentation/add_machine_ui.dart
```dart
class AddMachineUI extends UI {
- AddMachineUI({required PresenterCreator create})
- : super(create: create);
+ AddMachineUI({required super.create});
@override
Widget build(BuildContext context, AddMachineViewModel viewModel) {
- return Column(children: [
- Text('Add Machine'),
- Container(
- key: Key('SumTotalWidget'),
- child: Text(viewModel.total),
- ),
- ]);
+ return Column(
+ children: [
+ Text('Add Machine'),
+ Container(
+ key: Key('SumTotalWidget'),
+ child: Text(viewModel.total),
+ ),
+ ],
+ );
}
@override
@@ -132,10 +158,10 @@ class AddMachineUI extends UI {
#### lib/features/add_machine/presentation/add_machine_view_model.dart
```dart
class AddMachineViewModel extends ViewModel {
- final String total;
-
AddMachineViewModel({required this.total});
+ final String total;
+
@override
List get props => [total];
}
@@ -145,7 +171,7 @@ Let's review the code so far:
* The UI class specifies by generics the usage of a AddMachineViewModel. This way the class can have access to any field of the model.
-* A constructor is provided to accept a creator function. This is normally not needed. The "normal" implementation instanciates the proper presenter on the create override. But to make the test pass we can have a presenter that doesn't use a Use Case provider, but builds a static view model instead. This is useful for unit tests that use fake presenters.
+* A constructor is provided to accept a creator function. This is normally not needed. The "normal" implementation instantiates the proper presenter on the create override. But to make the test pass we can have a presenter that doesn't use a Use Case provider, but builds a static view model instead. This is useful for unit tests that use fake presenters.
* Since the presenter has a mocked behavior, the actual class is defined on the test file, and the create override is left as is (it will never be called on execution).
@@ -167,30 +193,29 @@ void main() {
final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('0')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('0')), findsOneWidget);
},
);
}
-class AddMachinePresenter
- extends Presenter {
+class AddMachinePresenter extends Presenter {
AddMachinePresenter({
- required PresenterBuilder builder,
- }) : super(provider: addMachineUseCaseProvider, builder: builder);
+ required super.builder,
+ }) : super(provider: addMachineUseCaseProvider);
@override
- AddMachineViewModel createViewModel(UseCase useCase, output) =>
- AddMachineViewModel(total: output.total.toString());
+ AddMachineViewModel createViewModel(UseCase useCase, output) {
+ return AddMachineViewModel(total: output.total.toString());
+ }
AddMachineUIOutput subscribe(_) => AddMachineUIOutput(total: 0);
}
class AddMachineUIOutput extends Output {
- final int total;
-
AddMachineUIOutput({required this.total});
+ final int total;
+
@override
List get props => [total];
}
@@ -210,10 +235,10 @@ We use to write any mocks and fakes in the test file that uses them, and try to
Now lets evolve our current code so we can test the second scenario. This is the test for it:
```dart
-/// Given I opened the Add Machine feature
- /// When I write a number on the number field
- /// And I press the "Add" button
- /// Then the total shown will be the entered number.
+ // Given I opened the Add Machine feature
+ // When I write a number on the number field
+ // And I press the "Add" button
+ // Then the total shown will be the entered number.
uiTest(
'AddMachineUI unit test - Scenario 2',
context: ProvidersContext(),
@@ -234,8 +259,7 @@ Now lets evolve our current code so we can test the second scenario. This is the
final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('15')), findsOneWidget);
},
);
```
@@ -248,8 +272,7 @@ To make this test work, we will need to first move the Presenter code into its c
#### lib/features/add_machine/presentation/add_machine_presenter.dart
```dart
-class AddMachinePresenter
- extends Presenter {
+class AddMachinePresenter extends Presenter {
AddMachinePresenter({
required UseCaseProvider provider,
required PresenterBuilder builder,
@@ -261,8 +284,7 @@ class AddMachinePresenter
onAddNumber: (number) => _onAddNumber(useCase, number));
void _onAddNumber(useCase, String number) {
- useCase.setInput(
- AddMachineAddNumberInput(int.parse(number)));
+ useCase.setInput(AddMachineAddNumberInput(int.parse(number)));
}
}
```
@@ -334,6 +356,7 @@ class AddMachineUI extends UI {
@override
Widget build(BuildContext context, AddMachineViewModel viewModel) {
final fieldController = TextEditingController();
+
return Scaffold(
body: Column(children: [
Text('Add Machine'),
@@ -357,8 +380,7 @@ class AddMachineUI extends UI {
}
@override
- create(PresenterBuilder builder) =>
- AddMachinePresenter(provider: provider, builder: builder);
+ create(PresenterBuilder builder) => AddMachinePresenter(provider: provider, builder: builder);
}
```
@@ -390,8 +412,7 @@ void main() {
final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
expect(sumTotalWidget, findsOneWidget);
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
+ expect(find.descendant(of: sumTotalWidget, matching: find.text('15')), findsOneWidget);
},
);
}
@@ -430,6 +451,6 @@ class EmptyEntity extends Entity {
Remember that as part of the TDD methodology, you will be constantly refactoring and updating the tests the more production code you complete, at this point you can see that the UseCaseFake is basically a minimal functioning Use Case. This is done intentionally to exemplify how fakes can be used to avoid writing production code before it's actually needed. For a normal real-case project, this is probably a step you can skip.
-Hopefuly by now you can appreciate the capacity of the Clean Framework components to help developers work with the UI layer without the need to first finish the Domain Layer code. You can even work in paralel with another developer that is doing it, while also having a high coverage on your code.
+Hopefully by now you can appreciate the capacity of the Clean Framework components to help developers work with the UI layer without the need to first finish the Domain Layer code. You can even work in parallel with another developer that is doing it, while also having a high coverage on your code.
It has to be noted that this is very helpful to create MVP builds and have a working prototype that can be reviewed by stakeholders and QA teams, saving the development team a lot of headaches, since the feedback can be received sooner.
diff --git a/packages/clean_framework/example/lib/features/country/domain/country_use_case.dart b/packages/clean_framework/example/lib/features/country/domain/country_use_case.dart
index eb56d91c..24baa8a7 100644
--- a/packages/clean_framework/example/lib/features/country/domain/country_use_case.dart
+++ b/packages/clean_framework/example/lib/features/country/domain/country_use_case.dart
@@ -17,17 +17,19 @@ class CountryUseCase extends UseCase {
'South America': 'SA',
},
),
- outputFilters: {
- CountryUIOutput: (CountryEntity e) {
- return CountryUIOutput(
- isLoading: e.isLoading,
- countries: e.countries,
- continents: e.continents,
- selectedContinentId: e.selectedContinentId,
- errorMessage: e.errorMessage,
- );
- },
- },
+ transformers: [
+ OutputTransformer.from(
+ (e) {
+ return CountryUIOutput(
+ isLoading: e.isLoading,
+ countries: e.countries,
+ continents: e.continents,
+ selectedContinentId: e.selectedContinentId,
+ errorMessage: e.errorMessage,
+ );
+ },
+ ),
+ ],
);
Future fetchCountries({
diff --git a/packages/clean_framework/example/lib/features/last_login/domain/last_login_use_case.dart b/packages/clean_framework/example/lib/features/last_login/domain/last_login_use_case.dart
index 29dd733d..029ce3b3 100644
--- a/packages/clean_framework/example/lib/features/last_login/domain/last_login_use_case.dart
+++ b/packages/clean_framework/example/lib/features/last_login/domain/last_login_use_case.dart
@@ -3,30 +3,31 @@ import 'last_login_entity.dart';
class LastLoginUseCase extends UseCase {
LastLoginUseCase()
- : super(entity: LastLoginEntity(), outputFilters: {
- LastLoginUIOutput: _lastLoginUIOutput,
- LastLoginCTAUIOutput: _lastLoginCTAUIOutput,
- });
-
- static LastLoginUIOutput _lastLoginUIOutput(LastLoginEntity entity) =>
- LastLoginUIOutput(
- lastLogin: entity.lastLogin,
- );
-
- static LastLoginCTAUIOutput _lastLoginCTAUIOutput(LastLoginEntity entity) =>
- LastLoginCTAUIOutput(
- isLoading: entity.state == LastLoginState.loading,
- );
+ : super(
+ entity: LastLoginEntity(),
+ transformers: [
+ OutputTransformer.from(
+ (entity) => LastLoginCTAUIOutput(
+ isLoading: entity.state == LastLoginState.loading,
+ ),
+ ),
+ LastLoginUIOutputTransformer(),
+ ],
+ );
Future fetchCurrentDate() async {
entity = entity.merge(state: LastLoginState.loading);
- await request(LastLoginDateOutput(), onSuccess: (LastLoginDateInput input) {
- return entity.merge(
- state: LastLoginState.idle, lastLogin: input.lastLogin);
- }, onFailure: (_) {
- return entity;
- });
+ await request(
+ LastLoginDateOutput(),
+ onSuccess: (LastLoginDateInput input) {
+ return entity.merge(
+ state: LastLoginState.idle,
+ lastLogin: input.lastLogin,
+ );
+ },
+ onFailure: (_) => entity,
+ );
}
}
@@ -57,3 +58,11 @@ class LastLoginDateInput extends SuccessInput {
LastLoginDateInput(this.lastLogin);
}
+
+class LastLoginUIOutputTransformer
+ extends OutputTransformer {
+ @override
+ LastLoginUIOutput transform(LastLoginEntity entity) {
+ return LastLoginUIOutput(lastLogin: entity.lastLogin);
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/random_cat/domain/random_cat_use_case.dart b/packages/clean_framework/example/lib/features/random_cat/domain/random_cat_use_case.dart
index fe55309f..02aa9e1c 100644
--- a/packages/clean_framework/example/lib/features/random_cat/domain/random_cat_use_case.dart
+++ b/packages/clean_framework/example/lib/features/random_cat/domain/random_cat_use_case.dart
@@ -6,15 +6,17 @@ class RandomCatUseCase extends UseCase {
RandomCatUseCase()
: super(
entity: RandomCatEntity(),
- outputFilters: {
- RandomCatUIOutput: (RandomCatEntity e) {
- return RandomCatUIOutput(
- isLoading: e.isLoading,
- id: e.id,
- url: e.url,
- );
- },
- },
+ transformers: [
+ OutputTransformer.from(
+ (e) {
+ return RandomCatUIOutput(
+ isLoading: e.isLoading,
+ id: e.id,
+ url: e.url,
+ );
+ },
+ ),
+ ],
);
Future fetch() async {
diff --git a/packages/clean_framework/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/clean_framework/example/macos/Flutter/Flutter-Debug.xcconfig
deleted file mode 100644
index 4b81f9b2..00000000
--- a/packages/clean_framework/example/macos/Flutter/Flutter-Debug.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
-#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/packages/clean_framework/example/macos/Flutter/Flutter-Release.xcconfig b/packages/clean_framework/example/macos/Flutter/Flutter-Release.xcconfig
deleted file mode 100644
index 5caa9d15..00000000
--- a/packages/clean_framework/example/macos/Flutter/Flutter-Release.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
-#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/packages/clean_framework/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/clean_framework/example/macos/Flutter/GeneratedPluginRegistrant.swift
deleted file mode 100644
index 1102414a..00000000
--- a/packages/clean_framework/example/macos/Flutter/GeneratedPluginRegistrant.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// Generated file. Do not edit.
-//
-
-import FlutterMacOS
-import Foundation
-
-import cloud_firestore
-import firebase_core
-
-func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
- FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
- FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
-}
diff --git a/packages/clean_framework/example/macos/Flutter/ephemeral/.symlinks/plugins/cloud_firestore b/packages/clean_framework/example/macos/Flutter/ephemeral/.symlinks/plugins/cloud_firestore
deleted file mode 120000
index dbf4e2d4..00000000
--- a/packages/clean_framework/example/macos/Flutter/ephemeral/.symlinks/plugins/cloud_firestore
+++ /dev/null
@@ -1 +0,0 @@
-/Users/sarbagya/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-4.2.0/
\ No newline at end of file
diff --git a/packages/clean_framework/example/macos/Flutter/ephemeral/.symlinks/plugins/firebase_core b/packages/clean_framework/example/macos/Flutter/ephemeral/.symlinks/plugins/firebase_core
deleted file mode 120000
index e52fa372..00000000
--- a/packages/clean_framework/example/macos/Flutter/ephemeral/.symlinks/plugins/firebase_core
+++ /dev/null
@@ -1 +0,0 @@
-/Users/sarbagya/.pub-cache/hosted/pub.dartlang.org/firebase_core-2.4.0/
\ No newline at end of file
diff --git a/packages/clean_framework/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/packages/clean_framework/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig
deleted file mode 100644
index 8b895615..00000000
--- a/packages/clean_framework/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig
+++ /dev/null
@@ -1,13 +0,0 @@
-// This is a generated file; do not edit or check into version control.
-FLUTTER_ROOT=/Users/sarbagya/Development/flutter
-FLUTTER_APPLICATION_PATH=/Users/sarbagya/Development/projects/clean_framework/packages/clean_framework/example
-COCOAPODS_PARALLEL_CODE_SIGN=true
-FLUTTER_TARGET=/Users/sarbagya/Development/projects/clean_framework/packages/clean_framework/example/lib/main.dart
-FLUTTER_BUILD_DIR=build
-FLUTTER_BUILD_NAME=1.5.0
-FLUTTER_BUILD_NUMBER=1.5.0
-DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==
-DART_OBFUSCATION=false
-TRACK_WIDGET_CREATION=true
-TREE_SHAKE_ICONS=false
-PACKAGE_CONFIG=/Users/sarbagya/Development/projects/clean_framework/packages/clean_framework/example/.dart_tool/package_config.json
diff --git a/packages/clean_framework/example/macos/Flutter/ephemeral/FlutterMacOS.podspec b/packages/clean_framework/example/macos/Flutter/ephemeral/FlutterMacOS.podspec
deleted file mode 100644
index 0bac3e84..00000000
--- a/packages/clean_framework/example/macos/Flutter/ephemeral/FlutterMacOS.podspec
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# NOTE: This podspec is NOT to be published. It is only used as a local source!
-# This is a generated file; do not edit or check into version control.
-#
-
-Pod::Spec.new do |s|
- s.name = 'FlutterMacOS'
- s.version = '1.0.0'
- s.summary = 'A UI toolkit for beautiful and fast apps.'
- s.homepage = 'https://flutter.dev'
- s.license = { :type => 'BSD' }
- s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
- s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
- s.osx.deployment_target = '10.11'
- # Framework linking is handled by Flutter tooling, not CocoaPods.
- # Add a placeholder to satisfy `s.dependency 'FlutterMacOS'` plugin podspecs.
- s.vendored_frameworks = 'path/to/nothing'
-end
diff --git a/packages/clean_framework/example/macos/Flutter/ephemeral/flutter_export_environment.sh b/packages/clean_framework/example/macos/Flutter/ephemeral/flutter_export_environment.sh
deleted file mode 100755
index 49f87080..00000000
--- a/packages/clean_framework/example/macos/Flutter/ephemeral/flutter_export_environment.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/sh
-# This is a generated file; do not edit or check into version control.
-export "FLUTTER_ROOT=/Users/sarbagya/Development/flutter"
-export "FLUTTER_APPLICATION_PATH=/Users/sarbagya/Development/projects/clean_framework/packages/clean_framework/example"
-export "COCOAPODS_PARALLEL_CODE_SIGN=true"
-export "FLUTTER_TARGET=/Users/sarbagya/Development/projects/clean_framework/packages/clean_framework/example/lib/main.dart"
-export "FLUTTER_BUILD_DIR=build"
-export "FLUTTER_BUILD_NAME=1.5.0"
-export "FLUTTER_BUILD_NUMBER=1.5.0"
-export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ=="
-export "DART_OBFUSCATION=false"
-export "TRACK_WIDGET_CREATION=true"
-export "TREE_SHAKE_ICONS=false"
-export "PACKAGE_CONFIG=/Users/sarbagya/Development/projects/clean_framework/packages/clean_framework/example/.dart_tool/package_config.json"
diff --git a/packages/clean_framework/example/macos/Pods/Local Podspecs/FlutterMacOS.podspec.json b/packages/clean_framework/example/macos/Pods/Local Podspecs/FlutterMacOS.podspec.json
deleted file mode 100644
index 0ef94a8f..00000000
--- a/packages/clean_framework/example/macos/Pods/Local Podspecs/FlutterMacOS.podspec.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "FlutterMacOS",
- "version": "1.0.0",
- "summary": "A UI toolkit for beautiful and fast apps.",
- "homepage": "https://flutter.dev",
- "license": {
- "type": "BSD"
- },
- "authors": {
- "Flutter Dev Team": "flutter-dev@googlegroups.com"
- },
- "source": {
- "git": "https://github.com/flutter/engine",
- "tag": "1.0.0"
- },
- "platforms": {
- "osx": "10.11"
- },
- "vendored_frameworks": "path/to/nothing"
-}
diff --git a/packages/clean_framework/example/macos/Pods/Local Podspecs/cloud_firestore.podspec.json b/packages/clean_framework/example/macos/Pods/Local Podspecs/cloud_firestore.podspec.json
deleted file mode 100644
index 0043e4dc..00000000
--- a/packages/clean_framework/example/macos/Pods/Local Podspecs/cloud_firestore.podspec.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "name": "cloud_firestore",
- "version": "4.2.0",
- "summary": "Flutter plugin for Cloud Firestore, a cloud-hosted, noSQL database with live synchronization and offline support on Android and iOS.",
- "description": "Flutter plugin for Cloud Firestore, a cloud-hosted, noSQL database with live synchronization and offline support on Android and iOS.",
- "homepage": "https://firebase.google.com/docs/firestore",
- "license": {
- "file": "../LICENSE"
- },
- "authors": "The Chromium Authors",
- "source": {
- "path": "."
- },
- "source_files": "Classes/**/*.{h,m}",
- "public_header_files": "Classes/Public/*.h",
- "private_header_files": "Classes/Private/*.h",
- "platforms": {
- "osx": "10.13"
- },
- "dependencies": {
- "FlutterMacOS": [
-
- ],
- "firebase_core": [
-
- ],
- "Firebase/CoreOnly": [
- "~> 10.3.0"
- ],
- "Firebase/Firestore": [
- "~> 10.3.0"
- ],
- "nanopb": [
- ">= 2.30908.0",
- "< 2.30910.0"
- ]
- },
- "static_framework": true,
- "pod_target_xcconfig": {
- "GCC_PREPROCESSOR_DEFINITIONS": "LIBRARY_VERSION=\\@\\\"4.2.0\\\" LIBRARY_NAME=\\@\\\"flutter-fire-fst\\\"",
- "DEFINES_MODULE": "YES"
- }
-}
diff --git a/packages/clean_framework/example/macos/Pods/Local Podspecs/firebase_core.podspec.json b/packages/clean_framework/example/macos/Pods/Local Podspecs/firebase_core.podspec.json
deleted file mode 100644
index 12e61936..00000000
--- a/packages/clean_framework/example/macos/Pods/Local Podspecs/firebase_core.podspec.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "name": "firebase_core",
- "version": "2.4.0",
- "summary": "Flutter plugin for Firebase Core, enabling connecting to multiple Firebase apps.",
- "description": "Flutter plugin for Firebase Core, enabling connecting to multiple Firebase apps.",
- "homepage": "https://firebase.flutter.dev/docs/core/usage/",
- "license": {
- "file": "../LICENSE"
- },
- "authors": "The Chromium Authors",
- "source": {
- "path": "."
- },
- "source_files": "Classes/**/*",
- "platforms": {
- "osx": "10.13"
- },
- "dependencies": {
- "FlutterMacOS": [
-
- ],
- "Firebase/CoreOnly": [
- "~> 10.3.0"
- ]
- },
- "static_framework": true,
- "pod_target_xcconfig": {
- "GCC_PREPROCESSOR_DEFINITIONS": "LIBRARY_VERSION=\\@\\\"2.4.0\\\" LIBRARY_NAME=\\@\\\"flutter-fire-core\\\"",
- "DEFINES_MODULE": "YES"
- }
-}
diff --git a/packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart b/packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart
index 9701c941..6bb542de 100644
--- a/packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart
+++ b/packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart
@@ -11,10 +11,11 @@ void main() {
// Subscription shortcut to mock a successful response from a Gateway
- useCase.subscribe(
- LastLoginDateOutput,
- (_) => Right(
- LastLoginDateInput(currentDate)));
+ useCase.subscribe(
+ (_) => Right(
+ LastLoginDateInput(currentDate),
+ ),
+ );
var output = useCase.getOutput();
expect(output, LastLoginUIOutput(lastLogin: DateTime.parse('1900-01-01')));
@@ -35,10 +36,12 @@ void main() {
final useCase = LastLoginUseCase();
// Subscription shortcut to mock a failure in the response from a Gateway
- useCase.subscribe(LastLoginDateOutput, (output) {
- expect(output, LastLoginDateOutput());
- return Left(FailureInput());
- });
+ useCase.subscribe(
+ (output) {
+ expect(output, LastLoginDateOutput());
+ return Left(FailureInput());
+ },
+ );
await useCase.fetchCurrentDate();
diff --git a/packages/clean_framework/lib/src/providers/gateway.dart b/packages/clean_framework/lib/src/providers/gateway.dart
index ab5f4200..9e9cf35d 100644
--- a/packages/clean_framework/lib/src/providers/gateway.dart
+++ b/packages/clean_framework/lib/src/providers/gateway.dart
@@ -18,9 +18,8 @@ abstract class Gateway _processRequest(buildRequest(output)),
+ _useCase.subscribe(
+ (output) => _processRequest(buildRequest(output as O)),
);
}
@@ -54,9 +53,8 @@ abstract class BridgeGateway(
+ (output) {
return Right(
onResponse(
_publisherUseCase.getOutput(),
diff --git a/packages/clean_framework/lib/src/providers/use_case.dart b/packages/clean_framework/lib/src/providers/use_case.dart
index 73fe8ad2..194c78aa 100644
--- a/packages/clean_framework/lib/src/providers/use_case.dart
+++ b/packages/clean_framework/lib/src/providers/use_case.dart
@@ -1,37 +1,35 @@
import 'dart:async';
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:either_dart/either.dart';
-import 'package:equatable/equatable.dart';
+import 'package:clean_framework/src/providers/entity.dart';
+import 'package:clean_framework/src/providers/use_case/use_case_helpers.dart';
+import 'package:clean_framework/src/providers/use_case_debounce_mixin.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:meta/meta.dart';
-typedef OutputBuilder = Output Function(T);
+export 'package:clean_framework/src/providers/use_case/use_case_helpers.dart';
-abstract class UseCase extends StateNotifier {
+typedef InputCallback = E Function(I);
+
+abstract class UseCase extends StateNotifier
+ with UseCaseDebounceMixin {
UseCase({
required E entity,
- Map>? outputFilters,
- Map? inputFilters,
- }) : _outputFilters = outputFilters ?? const {},
- _inputFilters = inputFilters ?? const {},
- super(entity);
-
- final Map> _outputFilters;
- final Map _inputFilters;
-
- final Map _requestSubscriptions = {};
- final Map _debounceTimers = {};
-
- @override
- void dispose() {
- for (final debounceTimer in _debounceTimers.values) {
- debounceTimer.cancel();
+ @Deprecated('Use transformers instead') OutputFilterMap? outputFilters,
+ @Deprecated('Use transformers instead') InputFilterMap? inputFilters,
+ List>? transformers,
+ }) : _outputFilters = Map.of(outputFilters ?? const {}),
+ _inputFilters = Map.of(inputFilters ?? const {}),
+ super(entity) {
+ if (transformers != null && transformers.isNotEmpty) {
+ _outputFilters.addTransformers(transformers);
+ _inputFilters.addTransformers(transformers);
}
- _debounceTimers.clear();
- super.dispose();
}
+ final OutputFilterMap _outputFilters;
+ final InputFilterMap _inputFilters;
+ final RequestSubscriptionMap _requestSubscriptions = {};
+
@visibleForTesting
@protected
E get entity => super.state;
@@ -39,103 +37,32 @@ abstract class UseCase extends StateNotifier {
@protected
set entity(E newEntity) => super.state = newEntity;
- /// Executes the [action] so that it will only be executed
- /// when there is no further repeated actions with same [tag]
- /// in a given frame of [duration].
- ///
- /// If [immediate] is false, then then first action will also be debounced.
- @protected
- void debounce({
- required void Function() action,
- required String tag,
- Duration duration = const Duration(milliseconds: 300),
- bool immediate = true,
- }) {
- final timer = _debounceTimers[tag];
-
- final timerPending = timer?.isActive ?? false;
- final canExecute = immediate && !timerPending;
-
- timer?.cancel();
- _debounceTimers[tag] = Timer(
- duration,
- () {
- _debounceTimers.remove(tag);
- if (!immediate) action();
- },
- );
-
- if (canExecute) action();
- }
-
- O getOutput() {
- final filter = _outputFilters[O];
- if (filter == null) {
- throw StateError('Output filter not defined for $O');
- }
- return filter(entity) as O;
- }
+ O getOutput() => _outputFilters(entity);
void setInput(I input) {
- if (_inputFilters[I] == null) {
- throw StateError('Input processor not defined for $I');
- }
- final processor = _inputFilters[I]! as InputProcessor;
- entity = processor(input, entity);
+ entity = _inputFilters(entity, input);
}
- void subscribe(Type outputType, Function callback) {
- if (_requestSubscriptions[outputType] != null) {
- throw StateError('A subscription for $outputType already exists');
- }
- _requestSubscriptions[outputType] = callback;
+ void subscribe(
+ RequestSubscription subscription,
+ ) {
+ _requestSubscriptions.add(subscription);
}
@protected
Future request(
O output, {
- required E Function(S successInput) onSuccess,
- required E Function(FailureInput failureInput) onFailure,
+ required InputCallback onSuccess,
+ required InputCallback onFailure,
}) async {
- final callback = _requestSubscriptions[O] ??
- (_) => Left(
- NoSubscriptionFailureInput(O),
- );
+ final input = await _requestSubscriptions(output);
- // ignore: avoid_dynamic_calls
- final either = await callback(output) as Either;
- entity = either.fold(onFailure, onSuccess);
+ entity = input.fold(onFailure, onSuccess);
}
-}
-
-typedef InputCallback = void Function(I input);
-typedef InputProcessor = E Function(
- I input,
- E entity,
-);
-
-typedef SubscriptionFilter = V Function(
- E entity,
-);
-
-@immutable
-abstract class Output extends Equatable {
@override
- bool get stringify => true;
-}
-
-@immutable
-abstract class Input {}
-
-class SuccessInput extends Input {}
-
-class FailureInput extends Input {
- FailureInput({this.message = ''});
- final String message;
-}
-
-class NoSubscriptionFailureInput extends FailureInput {
- NoSubscriptionFailureInput(Type t)
- : super(message: 'No subscription exists for this request of $t');
+ void dispose() {
+ clearDebounce();
+ super.dispose();
+ }
}
diff --git a/packages/clean_framework/lib/src/providers/use_case/helpers/input.dart b/packages/clean_framework/lib/src/providers/use_case/helpers/input.dart
new file mode 100644
index 00000000..e9846ef4
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/helpers/input.dart
@@ -0,0 +1,18 @@
+import 'package:clean_framework/src/providers/use_case/helpers/output.dart';
+import 'package:meta/meta.dart';
+
+@immutable
+abstract class Input {}
+
+class SuccessInput extends Input {}
+
+class FailureInput extends Input {
+ FailureInput({this.message = ''});
+
+ final String message;
+}
+
+class NoSubscriptionFailureInput extends FailureInput {
+ NoSubscriptionFailureInput()
+ : super(message: 'No subscription exists for this request of $O');
+}
diff --git a/packages/clean_framework/lib/src/providers/use_case/helpers/input_filter_map.dart b/packages/clean_framework/lib/src/providers/use_case/helpers/input_filter_map.dart
new file mode 100644
index 00000000..f19235e7
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/helpers/input_filter_map.dart
@@ -0,0 +1,23 @@
+part of 'use_case_transformer.dart';
+
+typedef InputProcessor = E Function(dynamic, E);
+
+typedef InputFilterMap = Map>;
+
+extension InputFilterMapExtension on InputFilterMap {
+ E call(E entity, I input) {
+ final processor = this[I];
+
+ if (processor == null) {
+ throw StateError('Input processor not defined for $I');
+ }
+
+ return processor(input, entity);
+ }
+
+ void addTransformers(List transformers) {
+ addEntries(
+ transformers.whereType>().map((f) => f._entry),
+ );
+ }
+}
diff --git a/packages/clean_framework/lib/src/providers/use_case/helpers/output.dart b/packages/clean_framework/lib/src/providers/use_case/helpers/output.dart
new file mode 100644
index 00000000..def1f00b
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/helpers/output.dart
@@ -0,0 +1,8 @@
+import 'package:equatable/equatable.dart';
+import 'package:meta/meta.dart';
+
+@immutable
+abstract class Output extends Equatable {
+ @override
+ bool get stringify => true;
+}
diff --git a/packages/clean_framework/lib/src/providers/use_case/helpers/output_filter_map.dart b/packages/clean_framework/lib/src/providers/use_case/helpers/output_filter_map.dart
new file mode 100644
index 00000000..3fb56808
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/helpers/output_filter_map.dart
@@ -0,0 +1,28 @@
+part of 'use_case_transformer.dart';
+
+typedef OutputBuilder = Output Function(E);
+
+typedef OutputFilterMap = Map>;
+
+extension OutputFilterMapExtension on OutputFilterMap {
+ O call(E entity) {
+ final builder = this[O];
+
+ if (builder == null) {
+ throw StateError(
+ 'Output filter not defined for "$O".\n'
+ 'Filters available for: ${keys.join(', ')}',
+ );
+ }
+
+ return builder(entity) as O;
+ }
+
+ void addTransformers(List transformers) {
+ addEntries(
+ transformers
+ .whereType>()
+ .map((f) => f._entry),
+ );
+ }
+}
diff --git a/packages/clean_framework/lib/src/providers/use_case/helpers/request_subscription_map.dart b/packages/clean_framework/lib/src/providers/use_case/helpers/request_subscription_map.dart
new file mode 100644
index 00000000..fa70eb27
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/helpers/request_subscription_map.dart
@@ -0,0 +1,38 @@
+import 'dart:async';
+
+import 'package:clean_framework/src/providers/use_case/helpers/input.dart';
+import 'package:clean_framework/src/providers/use_case/helpers/output.dart';
+import 'package:either_dart/either.dart';
+
+typedef RequestSubscriptionMap
+ = Map>;
+
+typedef Result = FutureOr>;
+
+typedef RequestSubscription = Result Function(dynamic);
+
+extension RequestSubscriptionMapExtension
+ on RequestSubscriptionMap {
+ void add(RequestSubscription subscription) {
+ if (this[O] != null) {
+ throw StateError('A subscription for $O already exists');
+ }
+
+ this[O] = subscription;
+ }
+
+ Result call(
+ O output,
+ ) async {
+ final subscription = this[O];
+
+ if (subscription == null) {
+ return Left(
+ NoSubscriptionFailureInput(),
+ );
+ }
+
+ final result = await subscription(output);
+ return result as Either;
+ }
+}
diff --git a/packages/clean_framework/lib/src/providers/use_case/helpers/use_case_transformer.dart b/packages/clean_framework/lib/src/providers/use_case/helpers/use_case_transformer.dart
new file mode 100644
index 00000000..55b482e3
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/helpers/use_case_transformer.dart
@@ -0,0 +1,61 @@
+import 'package:clean_framework/src/providers/entity.dart';
+import 'package:clean_framework/src/providers/use_case/helpers/input.dart';
+import 'package:clean_framework/src/providers/use_case/helpers/output.dart';
+import 'package:meta/meta.dart';
+
+part 'input_filter_map.dart';
+part 'output_filter_map.dart';
+
+abstract class UseCaseTransformer {}
+
+abstract class OutputTransformer
+ implements UseCaseTransformer {
+ const OutputTransformer() : _transformer = null;
+
+ factory OutputTransformer.from(O Function(E) transformer) =
+ _OutputFilter;
+
+ const OutputTransformer._(this._transformer);
+
+ final O Function(E)? _transformer;
+
+ MapEntry> get _entry => MapEntry(O, transform);
+
+ @protected
+ O transform(E entity);
+}
+
+abstract class InputTransformer
+ implements UseCaseTransformer {
+ const InputTransformer() : _transformer = null;
+
+ factory InputTransformer.from(E Function(E, I) transformer) =
+ _InputFilter;
+
+ const InputTransformer._(this._transformer);
+
+ final E Function(E, I)? _transformer;
+
+ MapEntry> get _entry {
+ return MapEntry(I, (i, e) => transform(e, i as I));
+ }
+
+ @protected
+ E transform(E entity, I input);
+}
+
+class _OutputFilter
+ extends OutputTransformer {
+ const _OutputFilter(super.transformer) : super._();
+
+ @override
+ O transform(E entity) => _transformer!(entity);
+}
+
+class _InputFilter
+ extends InputTransformer {
+ const _InputFilter(super.transformer) : super._();
+
+ @override
+ E transform(E entity, I input) => _transformer!(entity, input);
+}
diff --git a/packages/clean_framework/lib/src/providers/use_case/use_case_helpers.dart b/packages/clean_framework/lib/src/providers/use_case/use_case_helpers.dart
new file mode 100644
index 00000000..59875296
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case/use_case_helpers.dart
@@ -0,0 +1,4 @@
+export 'helpers/input.dart';
+export 'helpers/output.dart';
+export 'helpers/request_subscription_map.dart';
+export 'helpers/use_case_transformer.dart';
diff --git a/packages/clean_framework/lib/src/providers/use_case_debounce_mixin.dart b/packages/clean_framework/lib/src/providers/use_case_debounce_mixin.dart
new file mode 100644
index 00000000..5a22ef2a
--- /dev/null
+++ b/packages/clean_framework/lib/src/providers/use_case_debounce_mixin.dart
@@ -0,0 +1,44 @@
+import 'dart:async';
+
+import 'package:meta/meta.dart';
+
+mixin UseCaseDebounceMixin {
+ final Map _debounceTimers = {};
+
+ /// Executes the [action] so that it will only be executed
+ /// when there is no further repeated actions with same [tag]
+ /// in a given frame of [duration].
+ ///
+ /// If [immediate] is false, then then first action will also be debounced.
+ @protected
+ void debounce({
+ required void Function() action,
+ required String tag,
+ Duration duration = const Duration(milliseconds: 300),
+ bool immediate = true,
+ }) {
+ final timer = _debounceTimers[tag];
+
+ final timerPending = timer?.isActive ?? false;
+ final canExecute = immediate && !timerPending;
+
+ timer?.cancel();
+ _debounceTimers[tag] = Timer(
+ duration,
+ () {
+ _debounceTimers.remove(tag);
+ if (!immediate) action();
+ },
+ );
+
+ if (canExecute) action();
+ }
+
+ @protected
+ void clearDebounce() {
+ for (final debounceTimer in _debounceTimers.values) {
+ debounceTimer.cancel();
+ }
+ _debounceTimers.clear();
+ }
+}
diff --git a/packages/clean_framework/test/providers/external_interface_integration_test.dart b/packages/clean_framework/test/providers/external_interface_integration_test.dart
index 09557af2..ab7f3ceb 100644
--- a/packages/clean_framework/test/providers/external_interface_integration_test.dart
+++ b/packages/clean_framework/test/providers/external_interface_integration_test.dart
@@ -158,13 +158,12 @@ class TestUseCase extends UseCase {
TestUseCase(TestEntity entity)
: super(
entity: entity,
- outputFilters: {
- TestOutput: (entity) => TestOutput(entity.foo),
- },
- inputFilters: {
- TestSuccessInput: (TestSuccessInput input, TestEntity entity) =>
- entity.merge(foo: input.foo),
- },
+ transformers: [
+ OutputTransformer.from((entity) => TestOutput(entity.foo)),
+ InputTransformer.from(
+ (entity, input) => entity.merge(foo: input.foo),
+ ),
+ ],
);
Future fetchDataImmediately() async {
diff --git a/packages/clean_framework/test/providers/gateway_integration_test.dart b/packages/clean_framework/test/providers/gateway_integration_test.dart
index b62b1154..f5b15886 100644
--- a/packages/clean_framework/test/providers/gateway_integration_test.dart
+++ b/packages/clean_framework/test/providers/gateway_integration_test.dart
@@ -18,7 +18,7 @@ void main() {
final useCase = provider.getUseCaseFromContext(context);
- await useCase.fetchDataImmediatelly();
+ await useCase.fetchDataImmediately();
final output = useCase.getOutput();
expect(output, TestOutput('success'));
@@ -34,7 +34,7 @@ void main() {
final useCase = provider.getUseCaseFromContext(context);
- await useCase.fetchDataImmediatelly();
+ await useCase.fetchDataImmediately();
final output = useCase.getOutput();
expect(output, TestOutput('failure'));
@@ -131,16 +131,15 @@ class TestUseCase extends UseCase {
TestUseCase(TestEntity entity)
: super(
entity: entity,
- outputFilters: {
- TestOutput: (entity) => TestOutput(entity.foo),
- },
- inputFilters: {
- TestSuccessInput: (TestSuccessInput input, TestEntity entity) =>
- entity.merge(foo: input.foo),
- },
+ transformers: [
+ OutputTransformer.from((entity) => TestOutput(entity.foo)),
+ InputTransformer.from(
+ (entity, input) => entity.merge(foo: input.foo),
+ ),
+ ],
);
- Future fetchDataImmediatelly() async {
+ Future fetchDataImmediately() async {
await request(
TestDirectOutput('123'),
onFailure: (_) => entity.merge(foo: 'failure'),
diff --git a/packages/clean_framework/test/providers/presenter_widget_test.dart b/packages/clean_framework/test/providers/presenter_widget_test.dart
index 5a57a228..69f29585 100644
--- a/packages/clean_framework/test/providers/presenter_widget_test.dart
+++ b/packages/clean_framework/test/providers/presenter_widget_test.dart
@@ -105,9 +105,9 @@ class TestUseCase extends UseCase {
TestUseCase()
: super(
entity: EntityFake(),
- outputFilters: {
- TestOutput: (EntityFake entity) => TestOutput(entity.value),
- },
+ transformers: [
+ OutputTransformer.from((entity) => TestOutput(entity.value)),
+ ],
);
Future fetch() async {
diff --git a/packages/clean_framework/test/providers/use_case_transformer_test.dart b/packages/clean_framework/test/providers/use_case_transformer_test.dart
new file mode 100644
index 00000000..3d52a0f1
--- /dev/null
+++ b/packages/clean_framework/test/providers/use_case_transformer_test.dart
@@ -0,0 +1,110 @@
+import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('UseCase Transformer tests', () {
+ test('output transformer', () {
+ final useCase = TestUseCase()
+ ..updateFoo('hello')
+ ..updateBar(3);
+
+ expect(useCase.entity.foo, 'hello');
+ expect(useCase.entity.bar, 3);
+
+ expect(useCase.getOutput().foo, 'hello');
+ expect(useCase.getOutput().bar, 3);
+ });
+
+ test('input transformer', () {
+ final useCase = TestUseCase()..setInput(FooInput('hello'));
+
+ expect(useCase.entity.foo, 'hello');
+
+ expect(useCase.getOutput().foo, 'hello');
+ });
+ });
+}
+
+class TestSuccessInput extends SuccessInput {
+ TestSuccessInput(this.foo);
+
+ final String foo;
+}
+
+class TestEntity extends Entity {
+ TestEntity({
+ this.foo = '',
+ this.bar = 0,
+ });
+
+ final String foo;
+ final int bar;
+
+ @override
+ List get props => [foo, bar];
+
+ TestEntity copyWith({
+ String? foo,
+ int? bar,
+ }) {
+ return TestEntity(
+ foo: foo ?? this.foo,
+ bar: bar ?? this.bar,
+ );
+ }
+}
+
+class TestUseCase extends UseCase {
+ TestUseCase()
+ : super(
+ entity: TestEntity(),
+ transformers: [
+ FooOutputTransformer(),
+ FooInputTransformer(),
+ OutputTransformer.from((entity) => BarOutput(entity.bar)),
+ ],
+ );
+
+ void updateFoo(String foo) {
+ entity = entity.copyWith(foo: foo);
+ }
+
+ void updateBar(int bar) {
+ entity = entity.copyWith(bar: bar);
+ }
+}
+
+class FooInput extends Input {
+ FooInput(this.foo);
+ final String foo;
+}
+
+class FooOutput extends Output {
+ FooOutput(this.foo);
+ final String foo;
+
+ @override
+ List get props => [foo];
+}
+
+class BarOutput extends Output {
+ BarOutput(this.bar);
+ final int bar;
+
+ @override
+ List get props => [bar];
+}
+
+class FooOutputTransformer extends OutputTransformer {
+ @override
+ FooOutput transform(TestEntity entity) {
+ return FooOutput(entity.foo);
+ }
+}
+
+class FooInputTransformer extends InputTransformer {
+ @override
+ TestEntity transform(TestEntity entity, FooInput input) {
+ return entity.copyWith(foo: input.foo);
+ }
+}
diff --git a/packages/clean_framework/test/providers/use_case_unit_test.dart b/packages/clean_framework/test/providers/use_case_unit_test.dart
index f9579cb0..7032a95d 100644
--- a/packages/clean_framework/test/providers/use_case_unit_test.dart
+++ b/packages/clean_framework/test/providers/use_case_unit_test.dart
@@ -10,7 +10,7 @@ void main() {
expect(viewModel.foo, '');
- await useCase.fetchDataImmediatelly();
+ await useCase.fetchDataImmediately();
expect(useCase.entity, TestEntity(foo: 'failure'));
@@ -23,13 +23,18 @@ void main() {
final successInput = TestSuccessInput('success');
expect(SuccessInput() == successInput, isFalse);
- useCase.subscribe(TestDirectOutput, (output) {
- return Right(successInput);
+ useCase.subscribe((output) {
+ return Right(successInput);
});
- expect(() => useCase.subscribe(TestDirectOutput, (_) {}), throwsStateError);
+ expect(
+ () => useCase.subscribe((_) {
+ return Right(successInput);
+ }),
+ throwsStateError,
+ );
- await useCase.fetchDataImmediatelly();
+ await useCase.fetchDataImmediately();
expect(useCase.entity, TestEntity(foo: 'success'));
@@ -38,9 +43,9 @@ void main() {
test('UseCase subscription with delayed response on input filter', () async {
final useCase = TestUseCase(TestEntity(foo: ''))
- ..subscribe(TestSubscriptionOutput, (output) {
- return Right(SuccessInput());
- });
+ ..subscribe(
+ (output) => Right(SuccessInput()),
+ );
await useCase.fetchDataEventually();
@@ -166,16 +171,15 @@ class TestUseCase extends UseCase {
TestUseCase(TestEntity entity)
: super(
entity: entity,
- outputFilters: {
- TestOutput: (entity) => TestOutput(entity.foo),
- },
- inputFilters: {
- TestSuccessInput: (TestSuccessInput input, TestEntity entity) =>
- entity.merge(foo: input.foo),
- },
+ transformers: [
+ OutputTransformer.from((entity) => TestOutput(entity.foo)),
+ InputTransformer.from(
+ (entity, input) => entity.merge(foo: input.foo),
+ ),
+ ],
);
- Future fetchDataImmediatelly() async {
+ Future fetchDataImmediately() async {
await request(
TestDirectOutput('123'),
onFailure: (_) => entity.merge(foo: 'failure'),
diff --git a/packages/clean_framework_test/lib/src/use_case_fake.dart b/packages/clean_framework_test/lib/src/use_case_fake.dart
index b8feb227..ce612ea6 100644
--- a/packages/clean_framework_test/lib/src/use_case_fake.dart
+++ b/packages/clean_framework_test/lib/src/use_case_fake.dart
@@ -7,30 +7,26 @@ typedef UseCaseSubscription = Future>
Output,
);
-class UseCaseFake extends Fake
+class UseCaseFake extends Fake
implements UseCase {
UseCaseFake({this.output});
EntityFake _entity = EntityFake();
- late Function subscription;
- I? successInput;
+ late RequestSubscription subscription;
+ S? successInput;
final Output? output;
@override
EntityFake get entity => _entity;
@override
- Future request(
+ Future request(
O output, {
- required EntityFake Function(S successInput) onSuccess,
- required EntityFake Function(FailureInput failureInput) onFailure,
+ required InputCallback onSuccess,
+ required InputCallback onFailure,
}) async {
- // ignore: avoid_dynamic_calls
- final either = await subscription(output) as Either;
- _entity = either.fold(
- (FailureInput failureInput) => onFailure(failureInput),
- (S successInput) => onSuccess(successInput),
- );
+ final either = await subscription(output) as Either;
+ _entity = either.fold(onFailure, onSuccess);
}
@override
@@ -39,8 +35,10 @@ class UseCaseFake extends Fake
}
@override
- void subscribe(Type outputType, Function callback) {
- subscription = callback;
+ void subscribe(
+ RequestSubscription subscription,
+ ) {
+ this.subscription = subscription;
}
Future doFakeRequest(O output) async {
@@ -48,7 +46,7 @@ class UseCaseFake extends Fake
output,
onFailure: (failure) => _entity.merge('failure'),
onSuccess: (success) {
- successInput = success as I?;
+ successInput = success as S?;
return _entity.merge('success');
},
);