Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
]
],
Expand All @@ -29,6 +29,5 @@
]
],
["Other Resources", "/resources"]

]
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# 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.
<Image src="/assets/adapter_layer.png" caption="Adaptive Layer"/>

Let's look at a simple example first:

```dart
class MyGateway extends Gateway<MyOutput,
MyRequest, MyResponse, MyInput> {
class MyGateway extends Gateway<MyOutput, MyRequest, MyResponse, MyInput> {
LastLoginDateGateway({ProvidersContext? context, UseCaseProvider? provider})
: super(
context: context ?? providersContext,
provider: provider ?? lastLoginUseCaseProvider);
context: context ?? providersContext,
provider: provider ?? lastLoginUseCaseProvider,
);

@override
MyRequest buildRequest(MyOutput output) {
return MyRequest(data: output.data);
}

@override
MyInput onSuccess(covariant FirebaseSuccessResponse response) {
MyInput onSuccess(FirebaseSuccessResponse response) {
return MyInput(data: response.data);
}

Expand Down Expand Up @@ -81,17 +82,14 @@ uiTest(
context: context,
builder: () => 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);
},
);

Expand Down Expand Up @@ -135,7 +133,7 @@ class AddMachineGetTotalGateway extends Gateway<
}

@override
onSuccess(covariant AddMachineGetTotalResponse response) {
onSuccess(AddMachineGetTotalResponse response) {
return AddMachineGetTotalInput(response.number);
}
}
Expand All @@ -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<AddMachineEntity> {
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<AddMachineEntity, AddMachineAddNumberInput>.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,
);
}
}
```
Expand All @@ -187,18 +190,18 @@ Here we are adding a way to trigger a request. This **onCreate** method will be
class AddMachinePresenter extends Presenter<AddMachineViewModel,
AddMachineUIOutput, AddMachineUseCase> {
AddMachinePresenter({
required UseCaseProvider provider,
required PresenterBuilder<AddMachineViewModel> 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>(
AddMachineAddNumberInput(int.parse(number)));
useCase.setInput<AddMachineAddNumberInput>(AddMachineAddNumberInput(int.parse(number)));
}

@override
Expand Down
97 changes: 56 additions & 41 deletions docs/codelabs/clean-framework/domain-layer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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;
}
```
Expand All @@ -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<MyEntity> {
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<MyEntity, MyInput>.from(
(entity, input) => entity.copyWith(data: input.data),
),
],
);
}
```

Expand All @@ -75,30 +83,31 @@ 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:

```dart
// 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);
}
}
```

The **entity** attribute is available in any UseCase. Each time we need to change at least one field, we need to replace the whole instance. If this is not done, the Use Case will not generate any Output, since it behaves like a ValueNotifier

### 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.

Expand All @@ -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);
},
);
}
```

Expand Down Expand Up @@ -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.
Expand All @@ -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<Object?> get props => [total];
}
Expand All @@ -217,14 +229,17 @@ class AddMachineEntity extends Entity {
```dart
class AddMachineUseCase extends UseCase<AddMachineEntity> {
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<AddMachineEntity, AddMachineAddNumberInput>.from(
(entity, input) => AddMachineEntity(entity.total + input.number),
),
],
);
}
```

Expand Down
Loading