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/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..ac7935de
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,6 @@
+### Contributing to Clean Framework
+Follow the steps below to set up a development environment:
+
+#### Project Setup
+- Install [Melos](https://pub.dev/packages/melos/install)
+- Run `melos bootstrap` to install dependencies and link packages
\ No newline at end of file
diff --git a/docs.json b/docs.json
index 9276568e..58e97a08 100644
--- a/docs.json
+++ b/docs.json
@@ -1,19 +1,20 @@
{
- "name": "Clean Framework Codelabs",
+ "name": "Clean Framework",
"theme": "#0984E3",
"logo":"/assets/icon_logomark.png",
"logoDark":"/assets/icon_logomark_dark.png",
"favicon":"/assets/icon_logomark.png",
"sidebar": [
["Installation", "/"],
- ["Clean Framework Layers",[
- ["Introduction","/codelabs/clean-framework/intro"],
- ["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"],
- ["External Interface Layer","/codelabs/clean-framework/external-interface-layer"]
- ]
+ ["Basics",[
+ ["Migrating to v2", "/codelabs/clean-framework/migration-guide"],
+ ["Overview","/codelabs/clean-framework/intro"],
+ ["Project Structure","/codelabs/clean-framework/project-structure"],
+ ["UI Layer","/codelabs/clean-framework/ui-layer"],
+ ["Domain Layer","/codelabs/clean-framework/domain-layer"],
+ ["External Interface Layer","/codelabs/clean-framework/external-interface-layer"],
+ ["Adapter Layer","/codelabs/clean-framework/adapter-layer"]
+ ]
],
["Feature Flags",
[
@@ -29,6 +30,5 @@
]
],
["Other Resources", "/resources"]
-
]
}
\ No newline at end of file
diff --git a/docs/assets/pokemon-app.png b/docs/assets/pokemon-app.png
new file mode 100644
index 00000000..1c91ddae
Binary files /dev/null and b/docs/assets/pokemon-app.png differ
diff --git a/docs/codelabs/clean-framework/adapter-layer.mdx b/docs/codelabs/clean-framework/adapter-layer.mdx
new file mode 100644
index 00000000..63dbab86
--- /dev/null
+++ b/docs/codelabs/clean-framework/adapter-layer.mdx
@@ -0,0 +1,253 @@
+# Adapter Layer: Gateways
+We already learned part of this layer components with the Presenter and View Model.
+
+The Gateway is the last piece of the puzzle.
+It is the one that connects the Use Case to the External Interface.
+It is the one that translates the Output into a Request, and the Response into an Input, for Use Case.
+
+
+
+Let's look at a simple example first:
+
+```dart
+class MyGateway extends Gateway {
+ @override
+ MyRequest buildRequest(MyOutput output) {
+ return MyRequest(data: output.data);
+ }
+
+ @override
+ MyInput onSuccess(FirebaseSuccessResponse response) {
+ return MyInput(data: response.data);
+ }
+
+ @override
+ FailureInput onFailure(FailureResponse failureResponse) {
+ return FailureInput();
+ }
+}
+
+final myGatewayProvider = GatewayProvider(MyGateway.new);
+```
+
+In a very similar role to a Presenter, the Gateways are translators, take Outputs and create Requests, passing the data, and when the data is received as a Response, then translate it into a valid Input.
+
+This is the way we create a bridge between very specific libraries and dependencies and the agnostic Domain layer. Gateways exist on a 1 to 1 relationship for every type of Output that is launched as part of a request from the Use Case.
+
+Since they are created at the start of the execution through a Provider, keep in mind that a *loader* of providers help you ensure an instance of the Gateway exists before attempting to create requests.
+
+The implementation makes the intent very clear: when the Output is launched, it triggers the **onSuccess** method to create a Request, which in turns gets launched to any External Interface that is listening to those types of requests.
+
+When the Response is launched by the External Interface, it could come back as a successful or failed response. On each case, the Gateway generates the proper Input, which is pushed into the Use Case immediately.
+
+These Gateways create a circuit that is thread-blocking. For when you want to create a request that doesn't require an immediate response, you can use another type of Gateway:
+
+```dart
+class MyGateway extends WatcherGateway {
+ // rest of the code is the same
+}
+```
+
+When extending the WatcherGateway, the External Interface connected to this Gateway will be able to send a stream of Responses. Each time a Response is received, the **onSuccess** method will be invoked, so a new Input gets created.
+
+The Use Case in this case will need to setup a proper input filter to allow the Inputs to change the Entity multiple times.
+
+For WatcherGateways, the **onFailure** method happens when the subscription could not be set for some reason. For example, for Firebase style dependencies, it could happen when attempting to create the connection for the stream of data.
+
+
+### Coding the Gateway
+
+Now let's create a Gateway that takes output from the previously created `HomeUseCase`
+and creates appropriate input from the data received from `PokemonExternalInterface`.
+
+#### lib/features/home/external_interface/pokemon_collection_gateway.dart
+```dart
+class PokemonCollectionGateway extends Gateway {
+ @override
+ PokemonCollectionRequest buildRequest(PokemonCollectionGatewayOutput output) {
+ return PokemonCollectionRequest();
+ }
+
+ @override
+ FailureInput onFailure(FailureResponse failureResponse) {
+ return FailureInput(message: failureResponse.message);
+ }
+
+ @override
+ PokemonCollectionSuccessInput onSuccess(PokemonSuccessResponse response) {
+ final deserializer = Deserializer(response.data);
+
+ return PokemonCollectionSuccessInput(
+ pokemonIdentities: deserializer.getList(
+ 'results',
+ converter: PokemonIdentity.fromJson,
+ ),
+ );
+ }
+}
+
+class PokemonCollectionGatewayOutput extends Output {
+ @override
+ List get props => [];
+}
+
+class PokemonCollectionSuccessInput extends SuccessInput {
+ PokemonCollectionSuccessInput({required this.pokemonIdentities});
+
+ final List pokemonIdentities;
+}
+
+final _pokemonResUrlRegex = RegExp(r'https://pokeapi.co/api/v2/pokemon/(\d+)/');
+
+class PokemonCollectionRequest extends GetPokemonRequest {
+ @override
+ String get resource => 'pokemon';
+
+ @override
+ Map get queryParams => {'limit': 1000};
+}
+
+class PokemonIdentity {
+ PokemonIdentity({required this.name, required this.id});
+
+ final String name;
+ final String id;
+
+ factory PokemonIdentity.fromJson(Map json) {
+ final deserializer = Deserializer(json);
+
+ final match = _pokemonResUrlRegex.firstMatch(deserializer.getString('url'));
+
+ return PokemonIdentity(
+ name: deserializer.getString('name'),
+ id: match?.group(1) ?? '0',
+ );
+ }
+}
+```
+
+Now that we have all the necessary pieces, we can now attach them to each other.
+This is done through providers. Let's get back to where we create our first provider
+i.e. `lib/providers.dart` and add two more providers as below.
+
+### lib/providers.dart
+```dart
+final pokemonCollectionGateway = GatewayProvider(
+ PokemonCollectionGateway.new,
+ useCases: [
+ homeUseCaseProvider,
+ ],
+);
+
+final pokemonExternalInterfaceProvider = ExternalInterfaceProvider(
+ PokemonExternalInterface.new,
+ gateways: [
+ pokemonCollectionGateway,
+ ],
+);
+```
+
+Note: Here we are attaching the `PokemonCollectionGateway` to the `PokemonExternalInterface`
+and the `HomeUseCase` to the `PokemonCollectionGateway`.
+
+After this, let's add the `pokemonExternalInterfaceProvider` to the `AppProviderScope`,
+so that it's initialized beforehand any requests is made to it.
+
+### lib/main.dart
+```dart
+return AppProviderScope(
+ externalInterfaceProviders: [
+ pokemonExternalInterfaceProvider,
+ ,
+ ...,
+);
+```
+
+Let's try running the app now. Is it working fine?
+No, right?
+We're still getting the static data from the `Presenter`.
+
+Remember that, `Presenter`s are also part of the adapter layer as it bridges between the use case and the UI.
+Let's fill in the gaps now. Head back to the `HomePresenter` and update it as below.
+
+#### lib/features/home/presentation/home_presenter.dart
+```dart
+class HomePresenter
+ extends Presenter {
+ HomePresenter({
+ super.key,
+ required super.builder,
+ }) : super(provider: homeUseCaseProvider);
+
+ @override
+ void onLayoutReady(BuildContext context, HomeUseCase useCase) {
+ useCase.fetchPokemons();
+ }
+
+ @override
+ HomeViewModel createViewModel(HomeUseCase useCase, HomeUIOutput output) {
+ return HomeViewModel(
+ pokemons: output.pokemons.map((pokemon) {
+ return PokemonViewModel(name: pokemon.name, imageUrl: pokemon.imageUrl);
+ }).toList(growable: false),
+ onSearch: (query) => useCase.setInput(PokemonSearchInput(name: query)),
+ onRefresh: () => useCase.fetchPokemons(isRefresh: true),
+ onRetry: useCase.fetchPokemons,
+ isLoading: output.status == HomeStatus.loading,
+ hasFailedLoading: output.status == HomeStatus.failed,
+ );
+ }
+
+ @override
+ void onOutputUpdate(BuildContext context, HomeUIOutput output) {
+ if (output.isRefresh) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ output.status == HomeStatus.failed
+ ? 'Sorry, failed refreshing pokemons!'
+ : 'Refreshed pokemons successfully!',
+ ),
+ ),
+ );
+ }
+ }
+}
+```
+
+Now let's make a request from Use Case. Head back to `HomeUseCase` and update the todo as below.
+
+```dart
+await request(
+ PokemonCollectionGatewayOutput(),
+ onSuccess: (success) {
+ final pokemons = success.pokemonIdentities.map(_resolvePokemon);
+
+ return entity.copyWith(
+ pokemons: pokemons.toList(growable: false),
+ status: HomeStatus.loaded,
+ isRefresh: isRefresh,
+ );
+ },
+ onFailure: (failure) {
+ return entity.copyWith(
+ status: HomeStatus.failed,
+ isRefresh: isRefresh,
+ );
+ },
+);
+
+...
+
+PokemonData _resolvePokemon(PokemonIdentity pokemon) {
+ return PokemonData(
+ name: pokemon.name.toUpperCase(),
+ imageUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${pokemon.id}.svg',
+ );
+}
+```
+
+Try running the app now. It should be working fine now.
+
+The full example with the detail page implementation can be
+[found here](https://github.com/MattHamburger/clean_framework/tree/main/packages/clean_framework/example).
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/adaptive-layer.mdx b/docs/codelabs/clean-framework/adaptive-layer.mdx
deleted file mode 100644
index 7f85d381..00000000
--- a/docs/codelabs/clean-framework/adaptive-layer.mdx
+++ /dev/null
@@ -1,214 +0,0 @@
-# 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.
-
-
-Let's look at a simple example first:
-
-```dart
-class MyGateway extends Gateway {
- LastLoginDateGateway({ProvidersContext? context, UseCaseProvider? provider})
- : super(
- context: context ?? providersContext,
- provider: provider ?? lastLoginUseCaseProvider);
-
- @override
- MyRequest buildRequest(MyOutput output) {
- return MyRequest(data: output.data);
- }
-
- @override
- MyInput onSuccess(covariant FirebaseSuccessResponse response) {
- return MyInput(data: response.data);
- }
-
- @override
- FailureInput onFailure(FailureResponse failureResponse) {
- return FailureInput();
- }
-}
-
-final myGatewayProvider = GatewayProvider(
- (_) => MyGateway(),
-);
-```
-
-In a very similar role to a Presenter, the Gateways are translators, take Outputs and create Requests, passing the data, and when the data is received as a Response, then translate it into a valid Input.
-
-This is the way we create a bridge between very specific libraries and dependencies and the agnostic Domain layer. Gateways exist on a 1 to 1 relationship for every type of Output that is lauched as part of a request from the Use Case.
-
-Since they are created at the start of the execution through a Provider, keep in mind that a *loader* of providers help you ensure an instance of the Gateway exists before attempting to create requests.
-
-The implementation makes the intent very clear: when the Output is launched, it triggers the **onSuccess** method to create a Request, which in turns gets launched to any External Interface that is listening to those types of requests.
-
-When the Response is launched by the External Interface, it could come back as a succesful or failed response. On each case, the Gateway generates the proper Input, which is pushed into the Use Case immediately.
-
-These Gateways create a circuit that is thread-blocking. For when you want to create a request that doesn't require an immediate response, you can use another type of Gateway:
-
-```dart
-class MyGateway extends WatcherGateway {
- // rest of the code is the same
- }
-```
-
-When extending the WatcherGateway, the External Interface connected to this Gateway will be able to send a stream of Responses. Each time a Response is received, the **onSuccess** method will be invoked, so a new Input gets created.
-
-The Use Case in this case will need to setup a proper input filter to allow the Inputs to change the Entity multiple times.
-
-For WatcherGateways, the **onFailure** method happens when the subscription could not be set for some reason. For example, for Firebase style dependencies, it could happen when attempting to create the connection for the stream of data.
-
-### Testing and Coding the Gateway
-
-Let's go back to the code of the Add Machine app we used on the previous section. The only scenario we have to code is the one that confirms the number is reset everytime you open the app.
-
-Creating a test for that is trivial, since we can either add an integration test that does the steps, or create a setup idential to a state where the app has closed the feature.
-
-But none of this will require us to write a Gateway or External Interface, so we will need to modify the requirements. Let's assume that the stakeholders found it was more helpful if we retrieved the previous calculated total each time with opened the feature.
-
-This change will require that the apps "remembers" the last total in some way, which will easily require an External Interface. We don't have to decide right now how we are going to store the number. It is more important to finish the implementation the simplest way possible, which is to keep the number in memory inside the External Interface.
-
-Right now we will only care about our Gateway, and how the Use Case will talk to it. So before we jump into the code, lets code the test that needs to pass:
-
-#### test/features/add_machine/presentation/add_machine_ui_test.dart
-```dart
- /// Given I have added one or more numbers on the Add Machine feature
- /// When I navigate away and open the feature again
- /// Then the total shown is the previous total that was shown.
-
-uiTest(
- 'AddMachineUI unit test - Scenario 4',
- context: context,
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- setup: () {
- 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);
- },
- );
-
-//...
-final context = ProvidersContext();
-final addMachineUseCaseProvider = UseCaseProvider((_) => AddMachineUseCase());
-
-```
-
-This time we use another type of helper, **ProviderTester** is a bit more flexible, since it can be used to test components that are not UI objects, while still providing a providers context.
-
-Here we are assuming we will have a Home widget that loads our feature UI, and shows us the total. We have to make the app now show that number instead of a 0. This number will be created by the Gateway for now, later we will move it to the External Interface.
-
-
-When adding gateways on tests, we can directly "connect" the transport method so we can insert whatever response we need for the test.
-
-
-Now, lets jump into the Gateway code:
-
-#### lib/features/add_machine/external_interface/add_machine_get_total_gateway.dart
-```dart
-class AddMachineGetTotalGateway extends Gateway<
- AddMachineGetTotalOutput,
- AddMachineGetTotalRequest,
- AddMachineGetTotalResponse,
- AddMachineGetTotalInput> {
- AddMachineGetTotalGateway({
- ProvidersContext? context,
- UseCaseProvider? provider,
- UseCase? useCase,
- }) : super(context: context, provider: provider, useCase: useCase);
-
- @override
- buildRequest(AddMachineGetTotalOutput output) {
- return AddMachineGetTotalRequest();
- }
-
- @override
- FailureInput onFailure(covariant FailureResponse failureResponse) {
- throw UnimplementedError();
- }
-
- @override
- onSuccess(covariant AddMachineGetTotalResponse response) {
- return AddMachineGetTotalInput(response.number);
- }
-}
-
-class AddMachineGetTotalRequest extends Request {}
-
-class AddMachineGetTotalResponse extends SuccessResponse {
- final int number;
-
- AddMachineGetTotalResponse(this.number);
-}
-```
-
-As we learned previously, our Gateway will be associated only with a **AddMachineGetTotalOutput**, which will get translated into a **AddMachineGetTotalRequest** object. The output doesn't send any extra data, so our Request is also empty.
-
-The **AddMachineGetTotalResponse** will hold the preserved number that we retrieve on the External Interface, so the Gateway needs to get it on a successful response and produce a valid **AddMachineGetTotalInput** that the Use Case can process.
-
-And with this code, the only thing we need to do is make the UseCase do a request to actually retrieve the number:
-
-#### lib/features/add_machine/domain/add_machine_use_case.dart
-```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),
- }) {
- onCreate();
- }
-
- void onCreate() {
- request(AddMachineGetTotalOutput(),
- onSuccess: (AddMachineGetTotalInput input) {
- return AddMachineEntity(input.number);
- },
- onFailure: (_) => entity);
- }
-}
-```
-
-Here we are adding a way to trigger a request. This **onCreate** method will be used by the Presenter once the UI is built, as follows:
-
-#### lib/features/add_machine/presentation/add_machine_presenter.dart
-```dart
-class AddMachinePresenter extends Presenter {
- AddMachinePresenter({
- required UseCaseProvider provider,
- required PresenterBuilder builder,
- }) : super(provider: provider, builder: builder);
-
- @override
- AddMachineViewModel createViewModel(useCase, output) => AddMachineViewModel(
- total: output.total.toString(),
- onAddNumber: (number) => _onAddNumber(useCase, number));
-
- void _onAddNumber(useCase, String number) {
- useCase.setInput(
- AddMachineAddNumberInput(int.parse(number)));
- }
-
- @override
- void onLayoutReady(context, AddMachineUseCase useCase) => useCase.onCreate();
-}
-```
-
-To be able to use a specific Use Case, we had to include the name of the class in the generics declaration.
-
-With the **onLayoutReady** override we are able to call any method on the use case the first time the UI is built.
-
-If all these changes are correct and the new test passes, congratulations! You now have attached a custom Gateway to your feature!
-
diff --git a/docs/codelabs/clean-framework/domain-layer.mdx b/docs/codelabs/clean-framework/domain-layer.mdx
index 0c7d7c65..e07b5b26 100644
--- a/docs/codelabs/clean-framework/domain-layer.mdx
+++ b/docs/codelabs/clean-framework/domain-layer.mdx
@@ -1,42 +1,44 @@
-# The Domain Layer: Entity, Use Case and the interactions with Outputs and Inputs
-Congratulations, at this point we are ready to start exploring the Domain Layer, the heart of anything important for the project.
+# The Domain Layer
+## Entity, Use Case and the interactions with Outputs and Inputs
+Let's start by exploring the Domain Layer, the heart of anything important for the project.
### Entity
-Let's start by understanding the Entities. If you are familiar with Domain Driven Design (DDD), you already know how important are the Domain components to an app. When the design is robust, there is a zero chance that the state of the app failes due to validation or null errors. Domain models have strict rules so it is very hard to create instances with inconsistent states.
+Let's begin with the Entities. If you are familiar with Domain Driven Design (DDD), you already know how important are the Domain components to an app. When the design is robust, there is a zero chance that the state of the app failes due to validation or null errors. Domain models have strict rules so it is very hard to create instances with inconsistent states.
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.
-1. Attributes should be final and have initial values on construction. Some of them could be required values, inserted at the time the UseCase is created as well (explained in the following section).
+2. Attributes should be final and have initial values on construction. Some of them could be required values, inserted at the time the UseCase is created as well (explained in the following section).
-1. Use proper data types instead of relaying on parsers. For example, use DateTime instead of a String for a date attribute. You can parse the date in Presenters and Gateways.
+3. Use proper data types instead of relaying on parsers. For example, use DateTime instead of a String for a date attribute. You can parse the date in Presenters and Gateways.
-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.
+4. 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.
+5. 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{
- final bool isRegistered;
- final UserNameEntity userName;
+class AccountEntity extends Entity {
+ AccountEntity({this.isRegistered = false, this.userName});
- AccountEntity({required this.isRegistered, this.userName});
+ final bool isRegistered;
+ final UserNameEntity? userName;
}
class UserNameEntity extends Entity{
+ UserNameEntity({required this.firstName, required 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;
+ String get fullName => '$firstName $lastName';
}
```
@@ -56,16 +58,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 +85,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 +97,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 +109,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,16 +119,19 @@ 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(
+ FetchUserDataGatewayOutput(),
+ onSuccess: (UserDataInput input) {
+ return entity.copyWith(name: input.name);
+ },
+ onFailure: (_) {
+ return entity.copyWith(error: Error.dataFetchError);
+ },
+ );
}
```
-The request method creates a Future where the instance of **FetchUserDataOuput** is published. If no one is listening to this specific type of output, an error is thrown. During development you might attach dummy Gateways to help you complete the Use Case behavior without the need to write any outside code.
+The request method creates a Future where the instance of **FetchUserDataGatewayOutput** is published. If no one is listening to this specific type of output, an error is thrown. During development you might attach dummy Gateways to help you complete the Use Case behavior without the need to write any outside code.
The request has two callbacks, for success and failures respectively.
@@ -146,94 +160,179 @@ When Gateways and Presenters need to send Inputs to the Use Case, both can use t
Gateways do this for you internally, but Presenters are free to use this method at anytime instead of calling a specific method on the UseCase.
-Inputs are better than calling methods since you don't require any knowledge on the Use Case implemetation, as we demonstrated on the previous section. If the feature is very simple, you can opt to use methods directly to have less code overall.
+Inputs are better than calling methods since you don't require any knowledge on the Use Case implementation, as we demonstrated on the previous section. If the feature is very simple, you can opt to use methods directly to have less code overall.
-### Testing and Coding Use Cases
-Now we are ready to continue the feature implementation we started on the previous section. Let's start with the test for the third Gherkin scenario:
+### Coding Use Cases
+
+We will start implementing the Use Case now.
-#### test/features/add_machine/presentation/add_machine_ui_test.dart
+#### lib/features/home/domain/home_entity.dart
```dart
- /// Given I have entered a number on the Add Machine feature
- /// When I write another number and press "Add"
- /// Then the total shown will be the sum of both numbers.
- uiTest(
- 'AddMachineUI unit test - Scenario 3',
- context: ProvidersContext(),
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- verify: (tester) async {
- final numberField = find.byKey(Key('NumberField'));
- expect(numberField, findsOneWidget);
-
- await tester.enterText(numberField, '15');
-
- final addButton = find.byKey(Key('AddButton'));
- expect(addButton, findsOneWidget);
-
- await tester.tap(addButton);
- await tester.pumpAndSettle();
-
- final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
- expect(sumTotalWidget, 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);
- },
- );
-
- // Replace the provider with these lines:
- final addMachineUseCaseProvider = UseCaseProvider((_) => StaticUseCase([
- AddMachineUIOutput(total: 0),
- AddMachineUIOutput(total: 15),
- AddMachineUIOutput(total: 22),
- ]));
+import 'package:clean_framework/clean_framework.dart';
+
+enum HomeStatus { initial, loading, loaded, failed }
+
+class HomeEntity extends Entity {
+ HomeEntity({
+ this.pokemons = const [],
+ this.pokemonNameQuery = '',
+ this.status = HomeStatus.initial,
+ this.isRefresh = false,
+ });
+
+ final List pokemons;
+ final String pokemonNameQuery;
+ final HomeStatus status;
+ final bool isRefresh;
+
+ @override
+ List get props {
+ return [pokemons, pokemonNameQuery, status, isRefresh];
+ }
+
+ @override
+ HomeEntity copyWith({
+ List? pokemons,
+ String? pokemonNameQuery,
+ HomeStatus? status,
+ bool? isRefresh,
+ }) {
+ return HomeEntity(
+ pokemons: pokemons ?? this.pokemons,
+ pokemonNameQuery: pokemonNameQuery ?? this.pokemonNameQuery,
+ status: status ?? this.status,
+ isRefresh: isRefresh ?? this.isRefresh,
+ );
+ }
+}
+
+class PokemonData extends Entity {
+ PokemonData({
+ required this.name,
+ required this.imageUrl,
+ });
+
+ final String name;
+ final String imageUrl;
+
+ @override
+ List get props => [name, imageUrl];
+}
```
-This is basically a copy/paste of the previous test, the only needed change is the use case fake now returning an additional output.
+The next step will be to create the Use Case.
+Create a new file called `home_use_case.dart` inside the `domain` directory.
+
+#### lib/features/home/domain/home_use_case.dart
+```dart
+import 'package:clean_framework/clean_framework.dart';
+
+import 'home_entity.dart';
-Once we have this test coded and passing, its time for some major refactoring on all three tests, since now we want to use a production-worthy use case. Let's add the new Entity and Use Case into their corresponding place inside the domain folder:
+class HomeUseCase extends UseCase {
+ HomeUseCase() : super(entity: HomeEntity());
-#### lib/features/add_machine/domain/add_machine_add_entity.dart
+ Future fetchPokemons({bool isRefresh = false}) async {
+ if (!isRefresh) {
+ entity = entity.copyWith(status: HomeStatus.loading);
+ }
+
+ // TODO: Make a request to fetch the pokemons
+
+ if (isRefresh) {
+ entity = entity.copyWith(isRefresh: false, status: HomeStatus.loaded);
+ }
+ }
+}
+```
+
+After creating the Use Case, we need to create an UI Output.
+This will be used by the Presenter later to display the data on the screen.
+Create a new file called `home_ui_output.dart` inside the `domain` directory.
+
+#### lib/features/home/domain/home_ui_output.dart
```dart
-class AddMachineEntity extends Entity {
- final int total;
+import 'package:clean_framework/clean_framework.dart';
+
+import 'home_entity.dart';
+
+class HomeUIOutput extends Output {
+ HomeUIOutput({
+ required this.pokemons,
+ required this.status,
+ required this.isRefresh,
+ });
- AddMachineEntity(this.total);
+ final List pokemons;
+ final HomeStatus status;
+ final bool isRefresh;
@override
- List get props => [total];
+ List get props => [pokemons, status, isRefresh];
}
```
-#### lib/features/add_machine/domain/add_machine_use_case.dart
+Now we need to create an output transformer so that the raw data in Use Case(i.e. Entity)
+can be transformed into UI Output.
+Create the following class in the Use Case.
+
```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),
- });
+class HomeUIOutputTransformer extends OutputTransformer {
+ @override
+ HomeUIOutput transform(HomeEntity entity) {
+ final filteredPokemons = entity.pokemons.where(
+ (pokemon) {
+ final pokeName = pokemon.name.toLowerCase();
+ return pokeName.contains(entity.pokemonNameQuery.toLowerCase());
+ },
+ );
+
+ return HomeUIOutput(
+ pokemons: filteredPokemons.toList(growable: false),
+ status: entity.status,
+ isRefresh: entity.isRefresh,
+ );
+ }
}
```
-#### test/features/add_machine/presentation/add_machine_ui_test.dart
+And since we need to take search input from the UI as well to filter the pokemons,
+we need to create an input & input transformer as well.
+Add the following classes to the file.
+
```dart
-// rest of code above, this is the only change:
-final addMachineUseCaseProvider = UseCaseProvider((_) => AddMachineUseCase());
+class PokemonSearchInput extends Input {
+ PokemonSearchInput({required this.name});
+
+ final String name;
+}
+
+class PokemonSearchInputTransformer extends InputTransformer {
+ @override
+ HomeEntity transform(HomeEntity entity, PokemonSearchInput input) {
+ return entity.copyWith(pokemonNameQuery: input.name);
+ }
+}
```
-After these changes, all 3 tests pass as normal, very easy refactor, right?
+Finally these transformers need to be added to the Use Case.
+
+```dart
+class HomeUseCase extends UseCase {
+ HomeUseCase()
+ : super(
+ entity: HomeEntity(),
+ transformers: [
+ HomeUIOutputTransformer(),
+ PokemonSearchInputTransformer(),
+ ],
+ );
+
+ ...
+}
+```
-Congratulations if you made it until this point, on the next section we will plug-in a Gateway,
\ No newline at end of file
+Congratulations if you made it until this point,
+on the next section we will plug-in gateway to the domain.
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/external-interface-layer.mdx b/docs/codelabs/clean-framework/external-interface-layer.mdx
index 060230de..7b87bbdc 100644
--- a/docs/codelabs/clean-framework/external-interface-layer.mdx
+++ b/docs/codelabs/clean-framework/external-interface-layer.mdx
@@ -1,20 +1,20 @@
# 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.
+This 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:
```dart
class TestInterface extends ExternalInterface {
- TestInterface(GatewayProvider provider)
- : super([() => provider.getGateway(context)]);
-
@override
void handleRequest() {
// For normal Gateways
on(
(request, send) async {
await Future.delayed(Duration(milliseconds: 100));
- send(Right(TestResponse('success')));
+ send(TestResponse('success'));
},
);
@@ -27,7 +27,7 @@ class TestInterface extends ExternalInterface {
);
final subscription = stream.listen(
- (count) => send(Right(TestResponse(count.toString()))),
+ (count) => send(TestResponse(count.toString())),
);
await Future.delayed(Duration(milliseconds: 500));
@@ -38,283 +38,81 @@ class TestInterface extends ExternalInterface {
}
```
-First let's understand the constructor. It requires a list of Gateway references, which are normally retrieved from providers. During tests, you can add the object reference direcly.
+First let's understand the constructor. It requires a list of Gateway references, which are normally retrieved from providers. During tests, you can add the object reference directly.
When the External Interface gets created by its Provider, this connection will attach the object to the mechanism that the Gateway uses to send Requests.
The **handleRequest** method will have one or multiple calls of the **on** method, each one associated to a Request Type. These types must extend from the Response type specified on the generics class declaration.
-Each of the **on** calls will send back an *Either* instance, where the *Right* value is a **SuccessResponse**, and the *Left* is a **FailureResponse**.
-
-External Interfaces are meant to listen to groups of Requests that use the same dependency. Clean Framework has default implementations of external interfaces for Firebase, GraphQL and REST services, ready to be used in any application, you just need to create the providers using them.
-
-### Testing and Coding the External Interface
-
-For the final changes on our Add Machine app, we will move the code for the static number in the Gateway to the External Interface. There are no further chages on the current tests, but as an exercise you can add an integration test that confirms the last scenario by adding a way to navigate to the feature, pop out, then open it again to confirm the number is preserved.
-
-This is the remaining code:
-
-#### lib/features/add_machine/external_interface/add_machine_external_interface.dart
-```dart
-class AddMachineExternalInterface
- extends ExternalInterface {
- int _savedNumber;
-
- AddMachineExternalInterface({
- required List gatewayConnections,
- int number = 0,
- }) : _savedNumber = number,
- super(gatewayConnections);
+Each of the **on** calls will send back a **SuccessResponse** or a **FailureResponse**.
- @override
- void handleRequest() {
- on((request, send) {
- send(AddMachineGetTotalResponse(_savedNumber));
- });
+External Interfaces are meant to listen to groups of Requests that use the same dependency.
+Clean Framework has default implementations of external interfaces for Firebase, GraphQL and REST services, ready to be used in any application, you just need to create the providers using them.
- on((request, send) {
- _savedNumber = request.number;
- send(AddMachineGetTotalResponse(_savedNumber));
- });
- }
- @override
- FailureResponse onError(Object error) {
- // left empty, enhance as an exercise later
- return UnknownFailureResponse();
- }
-}
-```
+### Coding the External Interface
-See how now we handle two types of request, one to just get the saved total, and the other to modify the total before sending the current value. This requires the creation of another Gateway and request, as follows:
+Here, we will create a simple External Interface that will use the [**Dio**](https://pub.dev/packages/dio) library to make a request to a PokeAPI.
+For the external interface, we first need to create a Request and a Response class.
+The Request class will be used to send the request to the External Interface,
+and the Response class will be used to receive the response from the External Interface.
-#### lib/features/add_machine/external_interface/add_machine_set_total_gateway.dart
+#### lib/features/home/external_interface/pokemon_request.dart
```dart
-class AddMachineSetTotalGateway extends Gateway<
- AddMachineSetTotalOutput,
- AddMachineSetTotalRequest,
- AddMachineGetTotalResponse,
- AddMachineGetTotalInput> {
- AddMachineSetTotalGateway({
- ProvidersContext? context,
- UseCaseProvider? provider,
- UseCase? useCase,
- }) : super(context: context, provider: provider, useCase: useCase);
-
- @override
- buildRequest(AddMachineSetTotalOutput output) {
- return AddMachineSetTotalRequest(output.number);
- }
-
- @override
- FailureInput onFailure(covariant FailureResponse failureResponse) {
- throw UnimplementedError();
- }
-
- @override
- onSuccess(covariant AddMachineGetTotalResponse response) {
- return AddMachineGetTotalInput(response.number);
- }
+abstract class PokemonRequest extends Request {
+ Map get queryParams => {};
}
-class AddMachineSetTotalRequest extends Request {
- final int number;
-
- AddMachineSetTotalRequest(this.number);
+abstract class GetPokemonRequest extends PokemonRequest {
+ String get resource;
}
```
-And here are the changes for the rest of components:
-
-#### lib/features/add_machine/domain/add_machine_use_case.dart
+#### lib/features/home/external_interface/pokemon_success_response.dart
```dart
-class AddMachineUseCase extends UseCase {
- AddMachineUseCase()
- : super(entity: AddMachineEntity(0), outputFilters: {
- AddMachineUIOutput: (AddMachineEntity e) =>
- AddMachineUIOutput(total: e.total),
- }) {
- onCreate();
- }
-
- void onAddNumber(int number) async {
- await request(AddMachineSetTotalOutput(number + entity.total),
- onSuccess: (AddMachineGetTotalInput input) {
- return AddMachineEntity(input.number);
- }, onFailure: (_) {
- return entity;
- });
- }
+class PokemonSuccessResponse extends SuccessResponse {
+ const PokemonSuccessResponse({required this.data});
- void onCreate() async {
- await request(AddMachineGetTotalOutput(),
- onSuccess: (AddMachineGetTotalInput input) {
- return AddMachineEntity(input.number);
- },
- onFailure: (_) => entity);
- }
+ final Map data;
}
```
-#### lib/features/add_machine/presentation/add_machine_presenter.dart
+Then, we can create the External Interface class.
+
+#### lib/features/home/external_interface/pokemon_external_interface.dart
```dart
-class AddMachinePresenter extends Presenter {
- AddMachinePresenter({
- required UseCaseProvider provider,
- required PresenterBuilder builder,
- }) : super(provider: provider, builder: builder);
+class PokemonExternalInterface extends ExternalInterface {
+ PokemonExternalInterface({
+ Dio? dio,
+ }) : _dio = dio ?? Dio(BaseOptions(baseUrl: 'https://pokeapi.co/api/v2/'));
+
+ final Dio _dio;
@override
- AddMachineViewModel createViewModel(useCase, output) => AddMachineViewModel(
- total: output.total.toString(),
- onAddNumber: (number) => _onAddNumber(useCase, number));
+ void handleRequest() {
+ on(
+ (request, send) async {
+ final response = await _dio.get>(
+ request.resource,
+ queryParameters: request.queryParams,
+ );
+
+ final data = response.data!;
- void _onAddNumber(AddMachineUseCase useCase, String number) {
- useCase.onAddNumber(int.parse(number));
+ send(PokemonSuccessResponse(data: data));
+ },
+ );
}
@override
- void onLayoutReady(context, AddMachineUseCase useCase) => useCase.onCreate();
+ FailureResponse onError(Object error) {
+ return UnknownFailureResponse(error);
+ }
}
```
-The main change is that now the Use Case uses a specific method to handle the request to change the saved number, instead of using an input filter.
-
-And finally, some minor corrections to all the tests, just to enable all the providers:
-
-```dart
-final context = ProvidersContext();
-late UseCaseProvider addMachineUseCaseProvider;
-late GatewayProvider getTotalGatewayProvider;
-late GatewayProvider setTotalGatewayProvider;
-late ExternalInterfaceProvider externalInterfaceProvider;
-
-void main() {
- final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
-
- void setup() {
- addMachineUseCaseProvider = UseCaseProvider((_) => AddMachineUseCase());
- getTotalGatewayProvider = GatewayProvider((_) =>
- AddMachineGetTotalGateway(
- context: context, provider: addMachineUseCaseProvider));
- setTotalGatewayProvider = GatewayProvider((_) =>
- AddMachineSetTotalGateway(
- context: context, provider: addMachineUseCaseProvider));
-
- externalInterfaceProvider = ExternalInterfaceProvider((_) =>
- AddMachineExternalInterface(
- gatewayConnections: >[
- () => getTotalGatewayProvider.getGateway(context),
- () => setTotalGatewayProvider.getGateway(context),
- ]));
- getTotalGatewayProvider.getGateway(context);
- setTotalGatewayProvider.getGateway(context);
- externalInterfaceProvider.getExternalInterface(context);
- }
-
- /// Given I have navigated to the Add Machine feature
- /// Then I will see the Add Machine screen
- /// And the total shown will be 0.
- uiTest(
- 'AddMachineUI unit test - Scenario 1',
- context: context,
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- setup: setup,
- verify: (tester) async {
- expect(find.text('Add Machine'), findsOneWidget);
-
- expect(sumTotalWidget, findsOneWidget);
-
- expect(find.descendant(of: sumTotalWidget, matching: find.text('0')),
- findsOneWidget);
- },
- );
-
- /// 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: context,
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- setup: setup,
- verify: (tester) async {
- final numberField = find.byKey(Key('NumberField'));
- expect(numberField, findsOneWidget);
-
- await tester.enterText(numberField, '15');
-
- final addButton = find.byKey(Key('AddButton'));
- expect(addButton, findsOneWidget);
-
- await tester.tap(addButton);
- await tester.pumpAndSettle();
- await tester.pumpAndSettle();
-
- expect(sumTotalWidget, findsOneWidget);
-
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
- },
- );
-
- /// Given I have entered a number on the Add Machine feature
- /// When I write another number and press "Add"
- /// Then the total shown will be the sum of both numbers.
- uiTest(
- 'AddMachineUI unit test - Scenario 3',
- context: context,
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- setup: setup,
- verify: (tester) async {
- final numberField = find.byKey(Key('NumberField'));
- expect(numberField, findsOneWidget);
-
- await tester.enterText(numberField, '15');
-
- final addButton = find.byKey(Key('AddButton'));
- expect(addButton, findsOneWidget);
-
- await tester.tap(addButton);
- await tester.pumpAndSettle();
-
- expect(sumTotalWidget, 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);
- },
- );
-
- /// Given I have added one or more numbers on the Add Machine feature
- /// When I navigate away and open the feature again
- /// Then the total shown is the previous total that was shown.
- uiTest(
- 'AddMachineUI unit test - Scenario 4',
- context: context,
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- setup: () {
- setup();
-
- final gateway = setTotalGatewayProvider.getGateway(context);
-
- // We add a pre-existent request, so by the time the UI is build,
- // the use case already has this value
- gateway.transport(AddMachineSetTotalRequest(740));
- },
- verify: (tester) async {
- expect(find.descendant(of: sumTotalWidget, matching: find.text('740')),
- findsOneWidget);
- },
- );
-}
-```
\ No newline at end of file
+After the completion of external interface,
+we now need to connect the external interface with our domain layer.
+We'll do that by creating a gateway in next step,
+which act as an adapter between the external interface layer and the domain layer.
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/intro.mdx b/docs/codelabs/clean-framework/intro.mdx
index c3cb1c48..39894efe 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 contains 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/migration-guide.mdx b/docs/codelabs/clean-framework/migration-guide.mdx
new file mode 100644
index 00000000..db9c184f
--- /dev/null
+++ b/docs/codelabs/clean-framework/migration-guide.mdx
@@ -0,0 +1,149 @@
+### Migrating to v2
+The v2 release of the clean_framework is more optimized, easier to use, with better error reporting.
+This guide will help you migrate your existing code use the v2 of the package.
+
+#### Gradual Migration
+If you want to migrate your project to v2 gradually, you can do so by following these steps:
+- Update your `pubspec.yaml` file to use the latest version of `clean_framework`.
+- The new `package:clean_framework/clean_framework.dart` exports new classes for clean_framework which can conflict with the old classes.
+ To avoid this, replace all the imports for `package:clean_framework/clean_framework.dart` & `package:clean_framework/clean_framework_providers.dart` with `package:clean_framework/clean_framework_legacy.dart`.
+- If you were using `package:clean_framework/clean_framework_defaults.dart`, all the classes from defaults have been moved into sub-package.
+ So, please add the necessary sub-packages and import the classes from there.
+
+#### Migrating Use Case
+The `inputFilters` & `outputFilters` are now deprecated in favor of `transformers`.
+
+```dart
+// Before
+class HomeUseCase extends UseCase {
+ HomeUseCase()
+ : super(
+ entity: HomeEntity(),
+ outputFilters: {
+ FooOutput: (entity) => FooOutput(entity.foo),
+ BarOutput: (entity) => BarOutput(entity.bar),
+ },
+ inputFilters: {
+ FooInput: (input, entity) {
+ return entity.copyWith(foo: (input as FooInput).foo);
+ },
+ },
+ );
+
+ ...
+}
+
+// After
+class HomeUseCase extends UseCase {
+ HomeUseCase()
+ : super(
+ entity: HomeEntity(),
+ transformers: [
+ FooOutputTransformer(),
+ BarOutputTransformer(),
+ FooInputTransformer(),
+ ],
+ );
+
+ ...
+}
+```
+
+See [use_case_transformer_test.dart](https://github.com/MattHamburger/clean_framework/blob/develop/packages/clean_framework/test/core/use_case/use_case_transformer_test.dart) for more details.
+
+#### Migrating Gateways
+The gateways no longer requires to hold the associated use case providers as it's now attached through the gateway provider itself.
+
+```dart
+// Before
+class MyGateway extends Gateway {
+ MyGateway()
+ : super(
+ context: providersContext,
+ provider: featureUseCaseProvider,
+ );
+
+ ...
+}
+
+// After
+class MyGateway extends Gateway {
+ ...
+}
+```
+
+#### Migrating External Interfaces
+The external interfaces no longer requires to have gateway connection as it's now done by the external interface provider itself.
+
+```dart
+// Before
+class MyExternalInterface extends ExternalInterface {
+ MyExternalInterface(): super(
+ gatewayConnections: [
+ () => myGatewayProvider.getGateway(providersContext),
+ ],
+ );
+
+ ...
+}
+
+// After
+class MyExternalInterface extends ExternalInterface {
+ ...
+}
+```
+
+#### Migrating Providers
+The providers are now much simpler and easier to use. And manually initializing the providers is no longer required.
+
+```dart
+/// Before
+final myUseCaseProvider = UseCaseProvider((_) => LastLoginUseCase());
+
+final myGatewayProvider = GatewayProvider((_) => LastLoginDateGateway());
+
+final myExternalInterface = ExternalInterfaceProvider(
+ (_) => MyExternalInterface(
+ gatewayConnections: [
+ () => myGatewayProvider.getGateway(providersContext),
+ ],
+ ),
+);
+
+void loadProviders(){
+ myUseCaseProvider.getUseCaseFromContext(providersContext);
+ myGatewayProvider.getGateway(providersContext);
+ myExternalInterface.getExternalInterface(providersContext);
+}
+
+/// After
+final myUseCaseProvider = UseCaseProvider(MyUseCase.new);
+
+final myGateway = GatewayProvider(
+ MyGateway.new,
+ useCases: [myUseCaseProvider],
+);
+
+final myExternalInterfaceProvider = ExternalInterfaceProvider(
+ MyExternalInterface.new,
+ gateways: [mynGatewayProvider],
+);
+```
+
+#### Migrating AppProvidersContainer
+To more align with Riverpod and other scoped widgets used by the framework. The `AppProvidersContainer` has been renamed to `AppProviderScope`.
+
+```dart
+/// Before
+loadProviders();
+
+AppProvidersContainer(
+ providersContext: providersContext,
+ child: MyApp(),
+);
+
+/// After
+AppProviderScope(
+ child: MyApp(),
+);
+```
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/project-structure.mdx b/docs/codelabs/clean-framework/project-structure.mdx
new file mode 100644
index 00000000..fbe81570
--- /dev/null
+++ b/docs/codelabs/clean-framework/project-structure.mdx
@@ -0,0 +1,83 @@
+### Project Structure
+
+We suggest you organize your app into Features, with the assumption that features don't depend on each other.
+The goal should be to be able to delete a feature completely and don't break any code.
+
+Each feature could be organized in this way:
+
+```
+lib
+ providers.dart
+ features
+ feature
+ domain
+ feature_entity.dart
+ feature_ui_output.dart
+ feature_use_case.dart
+ feature_input.dart
+ external_interface
+ feature_gateway.dart
+ presentation
+ feature_presenter.dart
+ feature_ui.dart
+ feature_view_model.dart
+```
+
+Notice that the name of the feature is a prefix for all the files inside.
+We prefer this naming convention so they are easier to identify on searches,
+but you are free to follow any convention that suits your need.
+
+The folder structure is also a suggestion,
+you can add multiple layers if the feature begins to grow and have multiple screens and interactions.
+
+### The Providers
+
+Use Cases, Gateways and External Interfaces are instances of classes that are not Flutter Widgets,
+so they are not dependant on the Flutter Context.
+To have access to them, you can "publish" them using the Providers pattern.
+
+If you notice on the files list shown above,
+outside the features folder we have a file where we list all the providers used on the app.
+For large projects this is probably not the best idea, since this file can be long and bloated,
+so probably splitting the providers by feature could work better.
+
+This is an example on how this file can be coded:
+
+```dart
+final featureUseCaseProvider = UseCaseProvider(FeatureUseCase.new);
+
+final featureGatewayProvider = GatewayProvider(
+ FeatureGateway.new
+ useCases: [featureUseCaseProvider],
+);
+
+final graphQLExternalInterfaceProvider = ExternalInterfaceProvider(
+ GraphQLExternalInterface.new
+ gateways: [featureGatewayProvider],
+);
+```
+
+Clean Framework uses **Riverpod** for the Providers behavior,
+so you can understand why the providers are global instances.
+For anyone not familiar to how Riverpod works, this might seem inappropriate,
+specially coming from a strict OO formation. Justifying why this is useful and desirable,
+please refer to the [Riverpod documentation](https://riverpod.dev/docs/concepts/providers),
+since the creator already did a great job explaining this approach.
+
+Providers create instances lazily, but some of the listeners need to be connected before use cases make any request.
+That is why we need to "touch" all gateway and external interfaces providers to ensure they are created when the app starts.
+
+Adding external interface providers to the `externalInterfaceProviders` in **AppProviderScope**
+will ensure that all external interfaces are created.
+
+```dart
+void main() {
+ runApp(
+ AppProviderScope(
+ externalInterfaceProviders: [
+ graphQLExternalInterfaceProvider,
+ ],
+ ),
+ );
+}
+```
\ No newline at end of file
diff --git a/docs/codelabs/clean-framework/setup.mdx b/docs/codelabs/clean-framework/setup.mdx
deleted file mode 100644
index 073fd4ee..00000000
--- a/docs/codelabs/clean-framework/setup.mdx
+++ /dev/null
@@ -1,85 +0,0 @@
-# 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.
-
-### 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:_
-```
-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.
-
-Each feature could be organized in this way:
-
-```
-lib
- providers_loader.dart
- features
- my_new_feature
- domain
- my_new_feature_usecase.dart
- my_new_feature_entity.dart
- my_new_feature_outputs.dart
- my_new_feature_inputs.
- presentation
- my_new_feature_presenter.dart
- my_new_feature_view_model.dart
- my_new_feature_ui.dart
- external_interfaces
- my_new_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.
-
-The folder structure is also a suggestion, you can add multiple layers if the feature begins to grow and have multiple screens and interactions.
-
-### The Providers
-
-Use Cases, Gateways and External Interfaces are instances of classes that are not Flutter Widgets, so they are not dependant on the Flutter Context. To have access to them, you can "publish" them using the Providers pattern.
-
-If you notice on the files list shown above, outside the features folder we have a file where we list all the providers used on the app. For large projects this is probably not the best idea, since this file can be long and bloated, so probably splitting the providers by feature could work better.
-
-This is an example on how this file can be coded:
-
-```dart
-final myNewFeatureUseCaseProvider =
- UseCaseProvider(
- (_) => LastLoginUseCase(),
-);
-
-final myNewFeatureGatewayProvider = GatewayProvider(
- (_) => MyNewFeatureGateway(),
-);
-
-void loadProviders() {
- myNewFeatureUseCaseProvider.getUseCaseFromContext(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.
-
-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:
-
-```dart
-void main() {
- loadProviders();
- runApp(MyApp());
-}
-```
-
-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..04f6a796 100644
--- a/docs/codelabs/clean-framework/ui-layer.mdx
+++ b/docs/codelabs/clean-framework/ui-layer.mdx
@@ -1,435 +1,318 @@
# 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 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.
-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.
+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.
-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.
+They tend to have only Strings.
+This is intentional since the Presenter has the responsibility of any formatting and parsing done to the data.
-They tend to have only Strings. This is intentional since the Presenter has the responsibility of any formating 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 assume 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 asume 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
+### Codelab
+To better understand the flow and project structure,
+we'll create an application that will fetch data from a service and display it on a screen.
-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.
+For this example, we will use the [PokéAPI](https://pokeapi.co/).
-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.
+### Coding the UI Layer
-
-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.
-
+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.
### The feature requirements
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
-And the total shown will be 0.
-
-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 have entered a number on the Add Machine feature
-When I write another number and press "Add"
-Then the total shown will be the sum of both numbers.
-
-Given I have added one or more numbers on the Add Machine feature
-When I navigate away and open the feature again
-Then the total shown is 0.
+Given I have navigated to the Home feature
+Then I will see the list of Pokemon
+And I will see a search bar
+When I type a Pokemon name on the search bar
+Then I will see the list of Pokemon filtered by the search term
```
-And this is the design of the page, which we have as reference, but the scope of the codelab won't be to focus on completing the code to reflect exactly the appearance, it will be up to you to finish the implementation.
-
-
+And this is the design of the page, which we have as reference.
-
-These are not the only scenarios that should exist, since we are not covering possible error scenarios, like when the user adds no input at all, or tries to write something that is not a number. We leave the gaps of behavior out to be covered as an additional exercise for the developers.
-
+
-### The UI component test
-UI components are extensions of Flutter Widgets, so this means the we have to use a Widget Tester. Our goal is to confirm that the data is retrieved correctly from the view model.
+The first step is to create a view model with the properties that the feature requires.
+Let's create one at `lib/features/home/presentation` directory:
-This is how our basic test looks like:
-
-#### test/features/add_machine/presentation/add_machine_ui_test.dart
+#### lib/features/home/presentation/home_view_model.dart
```dart
-void main() {
- uiTest(
- 'AddMachineUI unit test',
- context: ProvidersContext(),
- builder: () => AddMachineUI(),
- verify: (tester) async {
+class HomeViewModel extends ViewModel {
+ const HomeViewModel({
+ required this.pokemons,
+ required this.isLoading,
+ required this.hasFailedLoading,
+ required this.onRetry,
+ required this.onRefresh,
+ required this.onSearch,
+ });
+
+ final List pokemons;
+ final bool isLoading;
+ final bool hasFailedLoading;
+
+ final VoidCallback onRetry;
+ final AsyncCallback onRefresh;
+ final ValueChanged onSearch;
- expect(find.text('Add Machine'), findsOneWidget);
+ @override
+ List get props => [pokemons, isLoading, hasFailedLoading];
+}
- final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
- expect(sumTotalWidget, findsOneWidget);
+class PokemonViewModel extends ViewModel {
+ const PokemonViewModel({
+ required this.name,
+ required this.imageUrl,
+ });
- expect(find.descendant(of: sumTotalWidget, matching: find.text('0')), findsOneWidget);
+ final String name;
+ final String imageUrl;
- },
- );
+ @override
+ List get props => [name, imageUrl];
}
```
-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.
-
+The next step is to create a Presenter.
+This class will be responsible for listening to the Use Case outputs and creating the View Model.
+For Presenter, we need to setup a skeleton of Use Case first.
-Now, to explain the code:
-* Notice how we are using our own "tester" component, the uiTest function. This helper uses a Widget tester internally, but also helps on the setup of a MaterialApp with a proper provider context. The context allows the override of already defined providers if needed.
+Note: Here we'll only create the skeleton of the Use Case, but we'll not implement it yet.
-* The builder creates an instance of a class that extends from the Clean Framework UI abstract class.
+Let's create it at `lib/features/home/domain` directory:
-* Verify is a function parameter to attach all the expects and actions done normally on widget tests.
-
-The test is confirming that the first Gherkin scenario happens correctly, but of course the test cannot pass until we have coded the actual UI class. The first piece of code we have to provide is precisely this UI implementation.
-
-But in practice, we not only need that. UI is coupled to a valid ViewModel, which gets translated from a specific Output inside a Presenter. So lets create the minimal code on these classes to make the test pass:
-
-#### lib/features/add_machine/presentation/add_machine_ui.dart
+#### lib/features/home/domain/home_entity.dart
```dart
-class AddMachineUI extends UI {
- AddMachineUI({required PresenterCreator create})
- : super(create: create);
-
- @override
- Widget build(BuildContext context, AddMachineViewModel viewModel) {
- return Column(children: [
- Text('Add Machine'),
- Container(
- key: Key('SumTotalWidget'),
- child: Text(viewModel.total),
- ),
- ]);
- }
-
+class HomeEntity extends Entity {
@override
- create(PresenterBuilder builder) {
- throw UnimplementedError();
- }
+ List get props => [];
}
```
-#### lib/features/add_machine/presentation/add_machine_view_model.dart
+#### lib/features/home/domain/home_ui_output.dart
```dart
-class AddMachineViewModel extends ViewModel {
- final String total;
-
- AddMachineViewModel({required this.total});
-
+class HomeUIOutput extends Output {
@override
- List get props => [total];
+ List get props => [];
}
```
-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.
-
-* 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).
-
-
-Now let's look at the necessary changes to the test itself:
-
-#### test/features/add_machine/presentation/add_machine_ui_test.dart
+#### lib/features/home/domain/home_use_case.dart
```dart
-void main() {
- uiTest(
- 'AddMachineUI unit test',
- context: ProvidersContext(),
- builder: () => AddMachineUI(
- create: (builder) => AddMachinePresenter(builder: builder),
- ),
- verify: (tester) async {
- expect(find.text('Add Machine'), findsOneWidget);
-
- final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
- expect(sumTotalWidget, findsOneWidget);
-
- expect(find.descendant(of: sumTotalWidget, matching: find.text('0')),
- findsOneWidget);
- },
+class HomeUseCase extends UseCase {
+ HomeUseCase() : super(
+ entity: HomeEntity(),
+ transformers: [
+ OutputTransformer.from((_) => HomeUIOutput()),
+ ],
);
}
-
-class AddMachinePresenter
- extends Presenter {
- AddMachinePresenter({
- required PresenterBuilder builder,
- }) : super(provider: addMachineUseCaseProvider, builder: builder);
-
- @override
- AddMachineViewModel createViewModel(UseCase useCase, output) =>
- AddMachineViewModel(total: output.total.toString());
-
- AddMachineUIOutput subscribe(_) => AddMachineUIOutput(total: 0);
-}
-
-class AddMachineUIOutput extends Output {
- final int total;
-
- AddMachineUIOutput({required this.total});
-
- @override
- List get props => [total];
-}
-
-final addMachineUseCaseProvider = UseCaseProvider((_) => UseCaseFake());
-
```
-The Presenter, Output and UseCaseProvider are using as much fake data as possible to control the outcome of the test.
-
-
-We use to write any mocks and fakes in the test file that uses them, and try to not share them, since scenarios change over time and trying to refactor all the helpers and derivates can be a complex and time-consuming task. Projects benefit more from fast unit tests that can be changed easily over time. If this doesn't fit your company policy, feel free to adapt the implementation to your needs.
-
-
-### A complete Presenter
-
-Now lets evolve our current code so we can test the second scenario. This is the test for it:
+After the entity skeleton, let's create a provider for it in the `lib/providers.dart`:
+#### lib/providers.dart
```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.
- uiTest(
- 'AddMachineUI unit test - Scenario 2',
- context: ProvidersContext(),
- builder: () => AddMachineUI(
- create: (builder) => AddMachinePresenter(builder: builder),
- ),
- verify: (tester) async {
- final numberField = find.byKey(Key('NumberField'));
- expect(numberField, findsOneWidget);
-
- await tester.enterText(numberField, '15');
-
- final addButton = find.byKey(Key('AddButton'));
- expect(addButton, findsOneWidget);
-
- await tester.tap(addButton);
-
- final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
- expect(sumTotalWidget, findsOneWidget);
-
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
- },
- );
+final homeUseCaseProvider = UseCaseProvider(HomeUseCase.new);
```
-
-There are opportunities to refactor the two tests, since we are repeating usages of finders, but it would be hard to understand the code on the codelab, we will have to constantly be looking at all the tests to understand the code. We leave that effort to you.
-
-
-To make this test work, we will need to first move the Presenter code into its corresponding place inside the production features code, complete the implementation, and make the test use a fake Use Case that publishes a single static Output.
+Now let's create the Presenter at `lib/features/home/presentation` directory:
-#### lib/features/add_machine/presentation/add_machine_presenter.dart
+### lib/features/home/presentation/home_presenter.dart
```dart
-class AddMachinePresenter
- extends Presenter {
- AddMachinePresenter({
- required UseCaseProvider provider,
- required PresenterBuilder builder,
- }) : super(provider: provider, builder: builder);
+class HomePresenter extends Presenter {
+ HomePresenter({
+ super.key,
+ required super.builder,
+ }) : super(provider: homeUseCaseProvider);
@override
- AddMachineViewModel createViewModel(useCase, output) => AddMachineViewModel(
- total: output.total.toString(),
- onAddNumber: (number) => _onAddNumber(useCase, number));
-
- void _onAddNumber(useCase, String number) {
- useCase.setInput(
- AddMachineAddNumberInput(int.parse(number)));
+ HomeViewModel createViewModel(HomeUseCase useCase, HomeUIOutput output) {
+ const spriteBaseUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world';
+
+ return HomeViewModel(
+ pokemons: const [
+ PokemonViewModel(name: 'Bulbasaur', imageUrl: '$spriteBaseUrl/1.svg'),
+ PokemonViewModel(name: 'Charmander', imageUrl: '$spriteBaseUrl/4.svg'),
+ PokemonViewModel(name: 'Squirtle', imageUrl: '$spriteBaseUrl/7.svg'),
+ PokemonViewModel(name: 'Pikachu', imageUrl: '$spriteBaseUrl/25.svg'),
+ ],
+ onSearch: (query) {},
+ onRefresh: () async {},
+ onRetry: () {},
+ isLoading: false,
+ hasFailedLoading: false,
+ );
}
}
```
-#### lib/features/add_machine/presentation/add_machine_view_model.dart
-```dart
-class AddMachineViewModel extends ViewModel {
- final String total;
- final ValueChanged onAddNumber;
-
- AddMachineViewModel({required this.total, required this.onAddNumber});
+Then we can create the UI at `lib/features/home/presentation` directory:
- @override
- List get props => [total];
-}
-```
-
-#### lib/features/add_machine/domain/add_machine_ui_output.dart
+#### lib/features/home/presentation/home_ui.dart
```dart
-class AddMachineUIOutput extends Output {
- final int total;
-
- AddMachineUIOutput({required this.total});
+class HomeUI extends UI {
+ HomeUI({super.key});
@override
- List get props => [total];
-}
-```
-
-#### lib/features/add_machine/domain/add_machine_add_number_input.dart
-```dart
-class AddMachineAddNumberInput extends Input {
- final int number;
-
- AddMachineAddNumberInput(this.number);
-}
-
-class AddMachineViewModel extends ViewModel {
- final String total;
- final ValueChanged onAddNumber;
-
- AddMachineViewModel({required this.total, required this.onAddNumber});
+ HomePresenter create(PresenterBuilder builder) {
+ return HomePresenter(builder: builder);
+ }
@override
- List get props => [total];
-}
-```
-
-
-About the code so far:
-
-* The Presenter got rid of the "subscribe" override since we will depend now entirely on an AddMachineUIOutput object from a provided use case.
-
-* When sending messages to the use case, we can either do it through a custom method or by using an Input, as we are doing here. Using the input helps us to not have to declare a custom Use Case, to make the test pass with as little code as possible.
-
-* The View Model has a new attribute, the callback that we will use to link the user action on the UI with an Input that is sent to the Use Case. ***Notice how callbacks are not considered part of the fieds used for equality comparisons.***
-
-* Both input and output classes now are inside the proper folder. UI components can import from domain files, and at this point, only the Presenter and test mocks create instances of them.
-
-We have to make fixes on the UI to add the new widgets:
-
-#### lib/features/add_machine/presentation/add_machine_ui.dart
-```dart
-class AddMachineUI extends UI {
- final UseCaseProvider provider;
-
- AddMachineUI({required this.provider});
+ Widget build(BuildContext context, HomeViewModel viewModel) {
+ final textTheme = Theme.of(context).textTheme;
+
+ Widget child;
+ if (viewModel.isLoading) {
+ child = const Center(child: CircularProgressIndicator());
+ } else if (viewModel.hasFailedLoading) {
+ child = _LoadingFailed(onRetry: viewModel.onRetry);
+ } else {
+ child = RefreshIndicator(
+ onRefresh: viewModel.onRefresh,
+ child: Scrollbar(
+ thumbVisibility: true,
+ child: ListView.builder(
+ prototypeItem: const SizedBox(height: 176), // 160 + 16
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ itemBuilder: (context, index) {
+ final pokemon = viewModel.pokemons[index];
+
+ return PokemonCard(
+ key: ValueKey(pokemon.name),
+ imageUrl: pokemon.imageUrl,
+ name: pokemon.name,
+ heroTag: pokemon.name,
+ onTap: () { /*TODO: Navigate to detail page*/ },
+ );
+ },
+ itemCount: viewModel.pokemons.length,
+ ),
+ ),
+ );
+ }
- @override
- Widget build(BuildContext context, AddMachineViewModel viewModel) {
- final fieldController = TextEditingController();
return Scaffold(
- body: Column(children: [
- Text('Add Machine'),
- Container(
- key: Key('SumTotalWidget'),
- child: Text(viewModel.total),
- ),
- TextFormField(
- key: Key('NumberField'),
- controller: fieldController,
- decoration: const InputDecoration(
- border: UnderlineInputBorder(), labelText: 'Write a number'),
+ appBar: AppBar(
+ title: const Text('Pokémon'),
+ centerTitle: false,
+ titleTextStyle: textTheme.displaySmall!.copyWith(
+ fontWeight: FontWeight.w300,
),
- ElevatedButton(
- key: Key('AddButton'),
- onPressed: () => viewModel.onAddNumber(fieldController.value.text),
- child: Text('Add'),
- ),
- ]),
+ bottom: viewModel.isLoading || viewModel.hasFailedLoading
+ ? null
+ : PokemonSearchField(onChanged: viewModel.onSearch),
+ ),
+ body: child,
);
}
+}
+
+class _LoadingFailed extends StatelessWidget {
+ const _LoadingFailed({required this.onRetry});
+
+ final VoidCallback onRetry;
@override
- create(PresenterBuilder builder) =>
- AddMachinePresenter(provider: provider, builder: builder);
+ Widget build(BuildContext context) {
+ return Center(
+ child: Column(
+ children: [
+ Text(
+ 'Oops, loading failed.',
+ style: Theme.of(context).textTheme.displaySmall,
+ ),
+ const SizedBox(height: 24),
+ OutlinedButton(
+ onPressed: onRetry,
+ child: const Text('Help Flareon, find her friends'),
+ ),
+ const SizedBox(height: 64),
+ ],
+ ),
+ );
+ }
}
```
-* Notice that now the View Model has a callback field, which the UI calls to send the current number text to the Presenter. This is the goal of the code separation, we delegate the parsing and validation of the field value to the Presenter, which in turn can rely on the Use Case for complex validations.
-
-* We are intentionally creating a TextEditingController inside a build method. This is not what Flutter developers normally do, since any rebuild will override the current value, but for this simple feature this is enough. If this becomes an issue, then we suggest creating a wrapper widget around your field, with a state that handles the controller, just remember to avoid using the state for anything else.
+Finally, set the `HomeUI` as the home for the app.
-Now that we have a full presenter implementation, the test can stop relying on the test presenter we coded previously, and change the mocks, now we need to mock the Use Case, as follows:
+Note: Remember to add `AppProviderScope` as the top level widget,
+which hold all the state of providers created by the app.
-#### test/features/add_machine/presentation/add_machine_ui_test.dart
+#### lib/main.dart
```dart
void main() {
- uiTest(
- 'AddMachineUI unit test - Scenario 2',
- context: ProvidersContext(),
- builder: () => AddMachineUI(provider: addMachineUseCaseProvider),
- verify: (tester) async {
- final numberField = find.byKey(Key('NumberField'));
- expect(numberField, findsOneWidget);
-
- await tester.enterText(numberField, '15');
-
- final addButton = find.byKey(Key('AddButton'));
- expect(addButton, findsOneWidget);
-
- await tester.tap(addButton);
- await tester.pumpAndSettle();
-
- final sumTotalWidget = find.byKey(Key('SumTotalWidget'));
- expect(sumTotalWidget, findsOneWidget);
-
- expect(find.descendant(of: sumTotalWidget, matching: find.text('15')),
- findsOneWidget);
- },
- );
+ runApp(const MyApp());
}
-final addMachineUseCaseProvider = UseCaseProvider((_) => StaticUseCase([
- AddMachineUIOutput(total: 0),
- AddMachineUIOutput(total: 15),
- ]));
-
-class StaticUseCase extends UseCase {
- static int _index = 0;
- final List outputs;
-
- StaticUseCase(this.outputs) : super(entity: EmptyEntity());
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
@override
- void setInput(I input) {
- _index++;
- entity = EmptyEntity();
- }
-
- @override
- O getOutput() {
- return outputs[_index] as O;
+ Widget build(BuildContext context) {
+ return AppProviderScope(
+ child: MaterialApp(
+ title: 'Pokemon',
+ theme: ThemeData.from(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
+ useMaterial3: true,
+ ),
+ home: HomeUI(),
+ ),
+ );
}
}
-
-class EmptyEntity extends Entity {
- @override
- List get props => [];
-}
-
```
-
-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.
-
+At this point the app should run perfectly fine with the static data we added in the view model.
-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.
+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/docs/index.mdx b/docs/index.mdx
index 1d92088e..664c88c6 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -1,13 +1,12 @@
+Using old version of Clean Framework? Head to [Documentation for v1](https://docs.page/MattHamburger/clean_framework~dca68cd737d0a6d68e287ce0cd7ad73f2f7d1c5b)
+
# Installation
[](https://codecov.io/gh/MattHamburger/clean_framework)
## Depend on it
Add this to your package's pubspec.yaml file:
+ 
-```
-dependencies:
- clean_framework: ^1.5.0
-```
## Install it
You can install packages from the command line: with Flutter:
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 00000000..63938e2e
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1,78 @@
+# Miscellaneous
+pubspec.lock
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+*.code-workspace
+/coverage
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/example/README.md b/example/README.md
new file mode 100644
index 00000000..bd080962
--- /dev/null
+++ b/example/README.md
@@ -0,0 +1,4 @@
+# Clean Framework Example
+
+Please refer to the [example app here](https://github.com/MattHamburger/clean_framework/tree/main/packages/clean_framework/example) instead.
+
diff --git a/example/android/.gitignore b/example/android/.gitignore
new file mode 100644
index 00000000..0a741cb4
--- /dev/null
+++ b/example/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
new file mode 100644
index 00000000..07124d61
--- /dev/null
+++ b/example/android/app/build.gradle
@@ -0,0 +1,60 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 32
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ applicationId "com.acmesoftware.example"
+ minSdkVersion 21
+ targetSdkVersion 30
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..5d04f4bc
--- /dev/null
+++ b/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a7d36c8f
--- /dev/null
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/clean_framework/example/android/app/src/main/kotlin/com/acmesoftware/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/acmesoftware/example/MainActivity.kt
similarity index 100%
rename from packages/clean_framework/example/android/app/src/main/kotlin/com/acmesoftware/example/MainActivity.kt
rename to example/android/app/src/main/kotlin/com/acmesoftware/example/MainActivity.kt
diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000..304732f8
--- /dev/null
+++ b/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..db77bb4b
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..17987b79
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..09d43914
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..d5f1c8d3
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..4d6372ee
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..1f83a33f
--- /dev/null
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..5d04f4bc
--- /dev/null
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/build.gradle b/example/android/build.gradle
new file mode 100644
index 00000000..73d46bcf
--- /dev/null
+++ b/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.6.10'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.2.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
new file mode 100644
index 00000000..a6738207
--- /dev/null
+++ b/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableR8=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..039eda99
--- /dev/null
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
new file mode 100644
index 00000000..44e62bcf
--- /dev/null
+++ b/example/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/packages/clean_framework/example/assets/flags.json b/example/assets/flags.json
similarity index 100%
rename from packages/clean_framework/example/assets/flags.json
rename to example/assets/flags.json
diff --git a/packages/clean_framework/example/integration_test/app_init_integration_test.dart b/example/integration_test/app_init_integration_test.dart
similarity index 87%
rename from packages/clean_framework/example/integration_test/app_init_integration_test.dart
rename to example/integration_test/app_init_integration_test.dart
index b0f74962..7463d9e0 100644
--- a/packages/clean_framework/example/integration_test/app_init_integration_test.dart
+++ b/example/integration_test/app_init_integration_test.dart
@@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
-import 'package:clean_framework_example/main.dart' as app;
+import 'package:example/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
diff --git a/packages/clean_framework/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id
similarity index 100%
rename from packages/clean_framework/example/ios/Flutter/.last_build_id
rename to example/ios/Flutter/.last_build_id
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 00000000..d2c61fc4
--- /dev/null
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 9.0
+
+
diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 00000000..ec97fc6f
--- /dev/null
+++ b/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 00000000..c4855bfe
--- /dev/null
+++ b/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/example/ios/Podfile b/example/ios/Podfile
new file mode 100644
index 00000000..313ea4a1
--- /dev/null
+++ b/example/ios/Podfile
@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+platform :ios, '11.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..1193fe64
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,597 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3AC18C1ED27F481C1BED83BE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDFC7454685DF6498BF1978D /* Pods_Runner.framework */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+ 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
+ 8834B17F6053F1307161D81D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ C86FFF5B8092A5BF5E37B3D2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ DDFC7454685DF6498BF1978D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ E2ED0B9341254BC4E8A32D0C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3AC18C1ED27F481C1BED83BE /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 356C1EDA1C4718B997F03609 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ E2ED0B9341254BC4E8A32D0C /* Pods-Runner.debug.xcconfig */,
+ 8834B17F6053F1307161D81D /* Pods-Runner.release.xcconfig */,
+ C86FFF5B8092A5BF5E37B3D2 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ 8D059E797CB2EFECD38A668A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ DDFC7454685DF6498BF1978D /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 356C1EDA1C4718B997F03609 /* Pods */,
+ 8D059E797CB2EFECD38A668A /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 97C146F11CF9000F007C117D /* Supporting Files */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ CF2D77B37A31DFC34F7DB60E /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 4EC7F78FD432909300C8796A /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "The Chromium Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 4EC7F78FD432909300C8796A /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC/openssl_grpc.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseFirestore/FirebaseFirestore.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
+ "${BUILT_PRODUCTS_DIR}/Libuv-gRPC/uv.framework",
+ "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
+ "${BUILT_PRODUCTS_DIR}/abseil/absl.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-C++/grpcpp.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
+ "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/uv.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/absl.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpcpp.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ CF2D77B37A31DFC34F7DB60E /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+ 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.huntington.core;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.huntington.core;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.huntington.core;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..3db53b6e
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..21a3cc14
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/packages/clean_framework/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h
similarity index 100%
rename from packages/clean_framework/example/ios/Runner/AppDelegate.h
rename to example/ios/Runner/AppDelegate.h
diff --git a/packages/clean_framework/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m
similarity index 100%
rename from packages/clean_framework/example/ios/Runner/AppDelegate.m
rename to example/ios/Runner/AppDelegate.m
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..d36b1fab
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 00000000..dc9ada47
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 00000000..28c6bf03
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 00000000..2ccbfd96
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 00000000..f091b6b0
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 00000000..4cde1211
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 00000000..d0ef06e7
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 00000000..dcdc2306
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 00000000..2ccbfd96
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 00000000..c8f9ed8f
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 00000000..a6d6b860
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 00000000..a6d6b860
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 00000000..75b2d164
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 00000000..c4df70d3
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 00000000..6a84f41e
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 00000000..d0e1f585
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 00000000..0bedcf2f
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 00000000..89c2725b
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000..f2e259c7
--- /dev/null
+++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 00000000..f3c28516
--- /dev/null
+++ b/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
new file mode 100644
index 00000000..a9cf4e86
--- /dev/null
+++ b/example/ios/Runner/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ CleanFrameworkExample
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ CADisableMinimumFrameDurationOnPhone
+
+
+
diff --git a/packages/clean_framework/example/ios/Runner/main.m b/example/ios/Runner/main.m
similarity index 100%
rename from packages/clean_framework/example/ios/Runner/main.m
rename to example/ios/Runner/main.m
diff --git a/packages/clean_framework/example/lib/asset_feature_provider.dart b/example/lib/asset_feature_provider.dart
similarity index 100%
rename from packages/clean_framework/example/lib/asset_feature_provider.dart
rename to example/lib/asset_feature_provider.dart
diff --git a/example/lib/demo_router.dart b/example/lib/demo_router.dart
new file mode 100644
index 00000000..2990e994
--- /dev/null
+++ b/example/lib/demo_router.dart
@@ -0,0 +1,66 @@
+import 'package:example/features/country/presentation/country_ui.dart';
+import 'package:example/features/last_login/presentation/last_login_ui.dart';
+import 'package:example/features/random_cat/presentation/random_cat_ui.dart';
+import 'package:example/home_page.dart';
+import 'package:example/routes.dart';
+import 'package:clean_framework_router/clean_framework_router.dart';
+import 'package:flutter/material.dart';
+
+class DemoRouter extends AppRouter {
+ @override
+ RouterConfiguration configureRouter() {
+ return RouterConfiguration(
+ routes: [
+ AppRoute(
+ route: Routes.home,
+ builder: (context, state) => HomePage(),
+ routes: [
+ AppRoute(
+ route: Routes.lastLogin,
+ builder: (context, state) => LastLoginUI(),
+ ),
+ AppRoute(
+ route: Routes.countries,
+ builder: (context, state) => CountryUI(),
+ routes: [
+ AppRoute(
+ route: Routes.countryDetail,
+ builder: (context, state) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(state.params['country'] ?? ''),
+ ),
+ body: Center(
+ child: Text(state.queryParams['capital'].toString()),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ AppRoute(
+ route: Routes.randomCat,
+ builder: (context, state) => RandomCatUI(),
+ ),
+ ],
+ ),
+ ],
+ errorBuilder: (context, state) => Page404(error: state.error),
+ );
+ }
+}
+
+class Page404 extends StatelessWidget {
+ const Page404({required this.error});
+
+ final Exception? error;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: Text(error.toString()),
+ ),
+ );
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/country/domain/country_entity.dart b/example/lib/features/country/domain/country_entity.dart
similarity index 94%
rename from packages/clean_framework/example/lib/features/country/domain/country_entity.dart
rename to example/lib/features/country/domain/country_entity.dart
index 0aca6d66..5b972e97 100644
--- a/packages/clean_framework/example/lib/features/country/domain/country_entity.dart
+++ b/example/lib/features/country/domain/country_entity.dart
@@ -1,4 +1,4 @@
-import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
import 'country_model.dart';
diff --git a/packages/clean_framework/example/lib/features/country/domain/country_model.dart b/example/lib/features/country/domain/country_model.dart
similarity index 90%
rename from packages/clean_framework/example/lib/features/country/domain/country_model.dart
rename to example/lib/features/country/domain/country_model.dart
index 2684665c..5c465815 100644
--- a/packages/clean_framework/example/lib/features/country/domain/country_model.dart
+++ b/example/lib/features/country/domain/country_model.dart
@@ -1,4 +1,4 @@
-import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
import 'package:equatable/equatable.dart';
class CountryInput extends Input with EquatableMixin {
diff --git a/packages/clean_framework/example/lib/features/country/domain/country_use_case.dart b/example/lib/features/country/domain/country_use_case.dart
similarity index 82%
rename from packages/clean_framework/example/lib/features/country/domain/country_use_case.dart
rename to example/lib/features/country/domain/country_use_case.dart
index eb56d91c..7a601967 100644
--- a/packages/clean_framework/example/lib/features/country/domain/country_use_case.dart
+++ b/example/lib/features/country/domain/country_use_case.dart
@@ -1,4 +1,4 @@
-import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
import 'country_entity.dart';
import 'country_model.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/country/domain/country_view_model.dart b/example/lib/features/country/domain/country_view_model.dart
similarity index 93%
rename from packages/clean_framework/example/lib/features/country/domain/country_view_model.dart
rename to example/lib/features/country/domain/country_view_model.dart
index eed7562e..99fdc002 100644
--- a/packages/clean_framework/example/lib/features/country/domain/country_view_model.dart
+++ b/example/lib/features/country/domain/country_view_model.dart
@@ -1,4 +1,4 @@
-import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
class CountryViewModel extends ViewModel {
CountryViewModel({
diff --git a/packages/clean_framework/example/lib/features/country/external_interface/country_gateway.dart b/example/lib/features/country/external_interface/country_gateway.dart
similarity index 88%
rename from packages/clean_framework/example/lib/features/country/external_interface/country_gateway.dart
rename to example/lib/features/country/external_interface/country_gateway.dart
index 07deb7dd..c9c7100d 100644
--- a/packages/clean_framework/example/lib/features/country/external_interface/country_gateway.dart
+++ b/example/lib/features/country/external_interface/country_gateway.dart
@@ -1,5 +1,5 @@
-import 'package:clean_framework_example/features/country/domain/country_use_case.dart';
-import 'package:clean_framework_example/providers.dart';
+import 'package:example/features/country/domain/country_use_case.dart';
+import 'package:example/providers.dart';
import 'package:clean_framework_graphql/clean_framework_graphql.dart';
class CountryGateway extends GraphQLGateway {
@override
- Presenter create(builder) => CountryPresenter(builder: builder);
+ CountryPresenter create(PresenterBuilder builder) {
+ return CountryPresenter(builder: builder);
+ }
@override
Widget build(BuildContext context, CountryViewModel model) {
@@ -77,7 +80,7 @@ class CountryUI extends UI {
title: Text(country.name),
subtitle: Text(country.capital),
horizontalTitleGap: 0,
- onTap: () => router.to(
+ onTap: () => context.router.go(
Routes.countryDetail,
params: {'country': country.name},
queryParams: {'capital': country.capital},
diff --git a/packages/clean_framework/example/lib/features/last_login/domain/last_login_entity.dart b/example/lib/features/last_login/domain/last_login_entity.dart
similarity index 90%
rename from packages/clean_framework/example/lib/features/last_login/domain/last_login_entity.dart
rename to example/lib/features/last_login/domain/last_login_entity.dart
index a4e2b338..745419a5 100644
--- a/packages/clean_framework/example/lib/features/last_login/domain/last_login_entity.dart
+++ b/example/lib/features/last_login/domain/last_login_entity.dart
@@ -1,4 +1,4 @@
-import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
class LastLoginEntity extends Entity {
final DateTime lastLogin;
diff --git a/example/lib/features/last_login/domain/last_login_use_case.dart b/example/lib/features/last_login/domain/last_login_use_case.dart
new file mode 100644
index 00000000..c1bea019
--- /dev/null
+++ b/example/lib/features/last_login/domain/last_login_use_case.dart
@@ -0,0 +1,68 @@
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'last_login_entity.dart';
+
+class LastLoginUseCase extends UseCase {
+ LastLoginUseCase()
+ : 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: (_) => entity,
+ );
+ }
+}
+
+class LastLoginUIOutput extends Output {
+ final DateTime lastLogin;
+
+ LastLoginUIOutput({required this.lastLogin});
+ @override
+ List get props => [lastLogin];
+}
+
+class LastLoginCTAUIOutput extends Output {
+ final bool isLoading;
+
+ LastLoginCTAUIOutput({required this.isLoading});
+
+ @override
+ List get props => [isLoading];
+}
+
+class LastLoginDateOutput extends Output {
+ @override
+ List get props => [];
+}
+
+class LastLoginDateInput extends SuccessInput {
+ final DateTime lastLogin;
+
+ 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/last_login/external_interface/last_login_date_gateway.dart b/example/lib/features/last_login/external_interface/last_login_date_gateway.dart
similarity index 75%
rename from packages/clean_framework/example/lib/features/last_login/external_interface/last_login_date_gateway.dart
rename to example/lib/features/last_login/external_interface/last_login_date_gateway.dart
index bc4aefd0..a7a9549e 100644
--- a/packages/clean_framework/example/lib/features/last_login/external_interface/last_login_date_gateway.dart
+++ b/example/lib/features/last_login/external_interface/last_login_date_gateway.dart
@@ -1,7 +1,6 @@
-import 'package:clean_framework/clean_framework.dart';
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework_example/features/last_login/domain/last_login_use_case.dart';
-import 'package:clean_framework_example/providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/last_login/domain/last_login_use_case.dart';
+import 'package:example/providers.dart';
import 'package:clean_framework_firestore/clean_framework_firestore.dart';
class LastLoginDateGateway extends FirebaseGateway {
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/lib/features/random_cat/domain/random_cat_view_model.dart b/example/lib/features/random_cat/domain/random_cat_view_model.dart
similarity index 83%
rename from packages/clean_framework/example/lib/features/random_cat/domain/random_cat_view_model.dart
rename to example/lib/features/random_cat/domain/random_cat_view_model.dart
index bb9c781c..c264cde0 100644
--- a/packages/clean_framework/example/lib/features/random_cat/domain/random_cat_view_model.dart
+++ b/example/lib/features/random_cat/domain/random_cat_view_model.dart
@@ -1,4 +1,4 @@
-import 'package:clean_framework/clean_framework_providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
class RandomCatViewModel extends ViewModel {
RandomCatViewModel({
diff --git a/packages/clean_framework/example/lib/features/random_cat/external_interface/random_cat_gateway.dart b/example/lib/features/random_cat/external_interface/random_cat_gateway.dart
similarity index 79%
rename from packages/clean_framework/example/lib/features/random_cat/external_interface/random_cat_gateway.dart
rename to example/lib/features/random_cat/external_interface/random_cat_gateway.dart
index 1633d1f1..c71d3758 100644
--- a/packages/clean_framework/example/lib/features/random_cat/external_interface/random_cat_gateway.dart
+++ b/example/lib/features/random_cat/external_interface/random_cat_gateway.dart
@@ -1,6 +1,6 @@
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework_example/features/random_cat/domain/random_cat_use_case.dart';
-import 'package:clean_framework_example/providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/random_cat/domain/random_cat_use_case.dart';
+import 'package:example/providers.dart';
import 'package:clean_framework_rest/clean_framework_rest.dart';
class RandomCatGateway extends RestGateway {
diff --git a/packages/clean_framework/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart
similarity index 100%
rename from packages/clean_framework/example/lib/generated_plugin_registrant.dart
rename to example/lib/generated_plugin_registrant.dart
diff --git a/packages/clean_framework/example/lib/home_page.dart b/example/lib/home_page.dart
similarity index 94%
rename from packages/clean_framework/example/lib/home_page.dart
rename to example/lib/home_page.dart
index 58b9e411..76a2e704 100644
--- a/packages/clean_framework/example/lib/home_page.dart
+++ b/example/lib/home_page.dart
@@ -1,5 +1,6 @@
import 'package:clean_framework/clean_framework.dart';
-import 'package:clean_framework_example/routes.dart';
+import 'package:example/routes.dart';
+import 'package:clean_framework_router/clean_framework_router.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -99,7 +100,7 @@ class _List extends StatelessWidget {
ListTile(
title: Text(title),
leading: Icon(iconData),
- onTap: () => router.to(route),
+ onTap: () => context.router.go(route),
),
Divider(),
],
diff --git a/example/lib/main.dart b/example/lib/main.dart
new file mode 100644
index 00000000..c611625a
--- /dev/null
+++ b/example/lib/main.dart
@@ -0,0 +1,50 @@
+import 'dart:developer';
+
+import 'package:clean_framework/clean_framework.dart';
+import 'package:example/asset_feature_provider.dart';
+import 'package:example/demo_router.dart';
+import 'package:example/providers.dart';
+import 'package:clean_framework_router/clean_framework_router.dart';
+import 'package:flutter/material.dart';
+
+Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ loadProviders();
+
+ runApp(ExampleApp());
+}
+
+class ExampleApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return FeatureScope(
+ register: () => AssetFeatureProvider(),
+ loader: (featureProvider) async {
+ // To demonstrate the lazy update triggered by change in feature flags.
+ await Future.delayed(Duration(seconds: 2));
+ await featureProvider.load('assets/flags.json');
+ },
+ onLoaded: () {
+ log('Feature Flags activated.');
+ },
+ child: AppProvidersContainer(
+ providersContext: providersContext,
+ child: AppRouterScope(
+ create: () => DemoRouter(),
+ builder: (context) {
+ return MaterialApp.router(
+ routerConfig: context.router.config,
+ theme: ThemeData(
+ pageTransitionsTheme: PageTransitionsTheme(
+ builders: {
+ TargetPlatform.android: ZoomPageTransitionsBuilder(),
+ },
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/providers.dart b/example/lib/providers.dart
new file mode 100644
index 00000000..78a3d67c
--- /dev/null
+++ b/example/lib/providers.dart
@@ -0,0 +1,85 @@
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/last_login/domain/last_login_entity.dart';
+import 'package:example/features/last_login/domain/last_login_use_case.dart';
+import 'package:example/features/last_login/external_interface/last_login_date_gateway.dart';
+import 'package:clean_framework_firestore/clean_framework_firestore.dart';
+import 'package:clean_framework_graphql/clean_framework_graphql.dart';
+import 'package:clean_framework_rest/clean_framework_rest.dart';
+import 'package:flutter/foundation.dart';
+
+import 'features/country/domain/country_entity.dart';
+import 'features/country/domain/country_use_case.dart';
+import 'features/country/external_interface/country_gateway.dart';
+import 'features/random_cat/domain/random_cat_entity.dart';
+import 'features/random_cat/domain/random_cat_use_case.dart';
+import 'features/random_cat/external_interface/random_cat_gateway.dart';
+
+ProvidersContext _providersContext = ProvidersContext();
+
+ProvidersContext get providersContext => _providersContext;
+
+@visibleForTesting
+void resetProvidersContext([ProvidersContext? context]) {
+ _providersContext = context ?? ProvidersContext();
+}
+
+final lastLoginUseCaseProvider =
+ UseCaseProvider(
+ (_) => LastLoginUseCase(),
+);
+
+final lastLoginGatewayProvider = GatewayProvider(
+ (_) => LastLoginDateGateway(),
+);
+
+final countryUseCaseProvider = UseCaseProvider(
+ (_) => CountryUseCase(),
+);
+
+final countryGatewayProvider = GatewayProvider(
+ (_) => CountryGateway(),
+);
+
+final randomCatUseCaseProvider =
+ UseCaseProvider(
+ (_) => RandomCatUseCase(),
+);
+
+final randomCatGatewayProvider = GatewayProvider(
+ (_) => RandomCatGateway(),
+);
+
+final firebaseExternalInterface = ExternalInterfaceProvider(
+ (_) => FirebaseExternalInterface(
+ firebaseClient: FirebaseClientFake({'date': '2021-10-07'}),
+ gatewayConnections: [
+ () => lastLoginGatewayProvider.getGateway(providersContext),
+ ],
+ ),
+);
+
+final graphQLExternalInterface = ExternalInterfaceProvider(
+ (_) => GraphQLExternalInterface(
+ link: 'https://countries.trevorblades.com',
+ gatewayConnections: [
+ () => countryGatewayProvider.getGateway(providersContext),
+ ],
+ ),
+);
+
+final restExternalInterface = ExternalInterfaceProvider(
+ (_) => RestExternalInterface(
+ baseUrl: 'https://thatcopy.pw',
+ gatewayConnections: [
+ () => randomCatGatewayProvider.getGateway(providersContext),
+ ],
+ ),
+);
+
+void loadProviders() {
+ lastLoginUseCaseProvider.getUseCaseFromContext(providersContext);
+ lastLoginGatewayProvider.getGateway(providersContext);
+ firebaseExternalInterface.getExternalInterface(providersContext);
+ graphQLExternalInterface.getExternalInterface(providersContext);
+ restExternalInterface.getExternalInterface(providersContext);
+}
diff --git a/example/lib/routes.dart b/example/lib/routes.dart
new file mode 100644
index 00000000..4359b795
--- /dev/null
+++ b/example/lib/routes.dart
@@ -0,0 +1,13 @@
+import 'package:clean_framework_router/clean_framework_router.dart';
+
+enum Routes with RoutesMixin {
+ home('/'),
+ lastLogin('last-login'),
+ countries('countries'),
+ countryDetail(':country'),
+ randomCat('random-cat');
+
+ const Routes(this.path);
+
+ final String path;
+}
diff --git a/packages/clean_framework/example/lib/widgets.dart b/example/lib/widgets.dart
similarity index 100%
rename from packages/clean_framework/example/lib/widgets.dart
rename to example/lib/widgets.dart
diff --git a/example/macos/Podfile b/example/macos/Podfile
new file mode 100644
index 00000000..fe733905
--- /dev/null
+++ b/example/macos/Podfile
@@ -0,0 +1,40 @@
+platform :osx, '10.13'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..7fc36747
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,635 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ 7DA976D09DD8F17862F72FAB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E5F56207E8DEFAA3AC1A815 /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 031D7B6B51C58E643C17E6C3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 8E5F56207E8DEFAA3AC1A815 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9604139AD857FA2346CCC7E9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ B2BDE31144464ED25CC665CA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 7DA976D09DD8F17862F72FAB /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ 87C2ABD9930C52127428C150 /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* example.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ 87C2ABD9930C52127428C150 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ B2BDE31144464ED25CC665CA /* Pods-Runner.debug.xcconfig */,
+ 9604139AD857FA2346CCC7E9 /* Pods-Runner.release.xcconfig */,
+ 031D7B6B51C58E643C17E6C3 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 8E5F56207E8DEFAA3AC1A815 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ C94987F84CA617B6D57816A1 /* [CP] Check Pods Manifest.lock */,
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ 181AA18C70E82BA2AFA5B60B /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 181AA18C70E82BA2AFA5B60B /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+ C94987F84CA617B6D57816A1 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..fb7259e1
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..21a3cc14
--- /dev/null
+++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 00000000..d53ef643
--- /dev/null
+++ b/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..a2ec33f1
--- /dev/null
+++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 00000000..82b6f9d9
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 00000000..13b35eba
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 00000000..0a3f5fa4
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 00000000..bdb57226
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 00000000..f083318e
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 00000000..326c0e72
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 00000000..2f1632cf
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 00000000..80e867a4
--- /dev/null
+++ b/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 00000000..e35ec744
--- /dev/null
+++ b/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.acmesoftware.example
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2022 com.acmesoftware. All rights reserved.
diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 00000000..36b0fd94
--- /dev/null
+++ b/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 00000000..dff4f495
--- /dev/null
+++ b/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 00000000..42bcbf47
--- /dev/null
+++ b/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 00000000..dddb8a30
--- /dev/null
+++ b/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+
+
diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist
new file mode 100644
index 00000000..4789daa6
--- /dev/null
+++ b/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 00000000..2722837e
--- /dev/null
+++ b/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController.init()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+ }
+}
diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements
new file mode 100644
index 00000000..852fa1a4
--- /dev/null
+++ b/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
new file mode 100644
index 00000000..848b2317
--- /dev/null
+++ b/example/pubspec.yaml
@@ -0,0 +1,33 @@
+name: example
+description: Sandbox with example of all components from clean framework
+version: 1.5.0
+publish_to: none
+
+environment:
+ sdk: '>=2.17.0 <3.0.0'
+ flutter: '>=3.0.0'
+
+dependencies:
+ flutter:
+ sdk: flutter
+ clean_framework: ^1.5.0
+ clean_framework_firestore: ^0.1.0
+ clean_framework_graphql: ^0.1.0
+ clean_framework_rest: ^0.1.0
+ clean_framework_router: ^0.1.0
+ intl: ^0.18.0
+
+dev_dependencies:
+ clean_framework_test: ^0.1.0
+ flutter_test:
+ sdk: flutter
+ mockito: ^5.0.0-nullsafety.7
+ equatable: ^2.0.5
+ integration_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
+
+ assets:
+ - assets/flags.json
diff --git a/packages/clean_framework/example/test/features/country/domain/country_use_case_test.dart b/example/test/features/country/domain/country_use_case_test.dart
similarity index 90%
rename from packages/clean_framework/example/test/features/country/domain/country_use_case_test.dart
rename to example/test/features/country/domain/country_use_case_test.dart
index c9cb792d..f7c300f9 100644
--- a/packages/clean_framework/example/test/features/country/domain/country_use_case_test.dart
+++ b/example/test/features/country/domain/country_use_case_test.dart
@@ -1,7 +1,7 @@
-import 'package:clean_framework_example/features/country/domain/country_entity.dart';
-import 'package:clean_framework_example/features/country/domain/country_model.dart';
-import 'package:clean_framework_example/features/country/domain/country_use_case.dart';
-import 'package:clean_framework_example/providers.dart';
+import 'package:example/features/country/domain/country_entity.dart';
+import 'package:example/features/country/domain/country_model.dart';
+import 'package:example/features/country/domain/country_use_case.dart';
+import 'package:example/providers.dart';
import 'package:flutter_test/flutter_test.dart';
final continents = {
diff --git a/packages/clean_framework/example/test/features/country/external_interface/country_gateway_test.dart b/example/test/features/country/external_interface/country_gateway_test.dart
similarity index 82%
rename from packages/clean_framework/example/test/features/country/external_interface/country_gateway_test.dart
rename to example/test/features/country/external_interface/country_gateway_test.dart
index 525d4149..10c88968 100644
--- a/packages/clean_framework/example/test/features/country/external_interface/country_gateway_test.dart
+++ b/example/test/features/country/external_interface/country_gateway_test.dart
@@ -1,7 +1,7 @@
import 'package:clean_framework/clean_framework.dart';
-import 'package:clean_framework_example/features/country/domain/country_entity.dart';
-import 'package:clean_framework_example/features/country/domain/country_model.dart';
-import 'package:clean_framework_example/providers.dart';
+import 'package:example/features/country/domain/country_entity.dart';
+import 'package:example/features/country/domain/country_model.dart';
+import 'package:example/providers.dart';
import 'package:clean_framework_graphql/clean_framework_graphql.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -16,7 +16,7 @@ void main() {
final gateway = countryGatewayProvider.getGateway(providersContext);
gateway.transport = (request) async {
- return Right(
+ return Either.right(
GraphQLSuccessResponse(
data: {
'countries': [
diff --git a/packages/clean_framework/example/test/features/country/presentation/country_ui_test.dart b/example/test/features/country/presentation/country_ui_test.dart
similarity index 94%
rename from packages/clean_framework/example/test/features/country/presentation/country_ui_test.dart
rename to example/test/features/country/presentation/country_ui_test.dart
index 3869ea5d..843fe543 100644
--- a/packages/clean_framework/example/test/features/country/presentation/country_ui_test.dart
+++ b/example/test/features/country/presentation/country_ui_test.dart
@@ -1,7 +1,8 @@
import 'package:clean_framework/clean_framework.dart';
-import 'package:clean_framework_example/features/country/presentation/country_ui.dart';
-import 'package:clean_framework_example/providers.dart';
-import 'package:clean_framework_example/routes.dart';
+import 'package:example/demo_router.dart';
+import 'package:example/features/country/presentation/country_ui.dart';
+import 'package:example/providers.dart';
+import 'package:example/routes.dart';
import 'package:clean_framework_graphql/clean_framework_graphql.dart';
import 'package:clean_framework_test/clean_framework_test.dart';
import 'package:flutter/material.dart';
@@ -12,12 +13,14 @@ import '../../../home_page_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
+ final router = DemoRouter();
+
setupUITest(context: providersContext, router: router);
final gateway = countryGatewayProvider.getGateway(providersContext);
gateway.transport = (request) async {
- return Right(
+ return Either.right(
GraphQLSuccessResponse(
data: {
'countries': request.continentCode == 'NA'
@@ -98,7 +101,7 @@ void main() {
child: child,
),
verify: (tester) async {
- router.to(Routes.countries);
+ router.go(Routes.countries);
await tester.pumpAndSettle();
final listTileFinder = find.byType(ListTile);
diff --git a/packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart b/example/test/features/last_login/domain/last_login_usecase_test.dart
similarity index 71%
rename from packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart
rename to example/test/features/last_login/domain/last_login_usecase_test.dart
index 9701c941..514fee15 100644
--- a/packages/clean_framework/example/test/features/last_login/domain/last_login_usecase_test.dart
+++ b/example/test/features/last_login/domain/last_login_usecase_test.dart
@@ -1,7 +1,7 @@
import 'package:clean_framework/clean_framework.dart';
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework_example/features/last_login/domain/last_login_entity.dart';
-import 'package:clean_framework_example/features/last_login/domain/last_login_use_case.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/last_login/domain/last_login_entity.dart';
+import 'package:example/features/last_login/domain/last_login_use_case.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
@@ -11,10 +11,9 @@ void main() {
// Subscription shortcut to mock a successful response from a Gateway
- useCase.subscribe(
- LastLoginDateOutput,
- (_) => Right(
- LastLoginDateInput(currentDate)));
+ useCase.subscribe(
+ (_) => Either.right(LastLoginDateInput(currentDate)),
+ );
var output = useCase.getOutput();
expect(output, LastLoginUIOutput(lastLogin: DateTime.parse('1900-01-01')));
@@ -35,10 +34,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 Either.left(FailureInput());
+ },
+ );
await useCase.fetchCurrentDate();
diff --git a/packages/clean_framework/example/test/features/last_login/external_interface/last_login_date_gateway_test.dart b/example/test/features/last_login/external_interface/last_login_date_gateway_test.dart
similarity index 68%
rename from packages/clean_framework/example/test/features/last_login/external_interface/last_login_date_gateway_test.dart
rename to example/test/features/last_login/external_interface/last_login_date_gateway_test.dart
index b1ff98d1..9a2ab107 100644
--- a/packages/clean_framework/example/test/features/last_login/external_interface/last_login_date_gateway_test.dart
+++ b/example/test/features/last_login/external_interface/last_login_date_gateway_test.dart
@@ -1,10 +1,8 @@
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework/src/app_providers_container.dart';
-import 'package:clean_framework_example/features/last_login/domain/last_login_use_case.dart';
-import 'package:clean_framework_example/features/last_login/external_interface/last_login_date_gateway.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/last_login/domain/last_login_use_case.dart';
+import 'package:example/features/last_login/external_interface/last_login_date_gateway.dart';
import 'package:clean_framework_firestore/clean_framework_firestore.dart';
import 'package:clean_framework_test/clean_framework_test.dart';
-import 'package:either_dart/either.dart';
import 'package:flutter_test/flutter_test.dart';
final context = ProvidersContext();
@@ -16,7 +14,7 @@ void main() {
var gateway = LastLoginDateGateway(context: context, provider: provider);
gateway.transport = (request) async =>
- Right(FirebaseSuccessResponse({'date': '2000-01-01'}));
+ Either.right(FirebaseSuccessResponse({'date': '2000-01-01'}));
final testRequest = LastLoginDateRequest();
expect(testRequest.id, '12345');
@@ -32,7 +30,8 @@ void main() {
final provider = UseCaseProvider((_) => useCase);
var gateway = LastLoginDateGateway(context: context, provider: provider);
- gateway.transport = (request) async => Left(UnknownFailureResponse());
+ gateway.transport =
+ (request) async => Either.left(UnknownFailureResponse());
await useCase.doFakeRequest(LastLoginDateOutput());
diff --git a/packages/clean_framework/example/test/features/last_login/presentation/last_login_presenter_test.dart b/example/test/features/last_login/presentation/last_login_presenter_test.dart
similarity index 90%
rename from packages/clean_framework/example/test/features/last_login/presentation/last_login_presenter_test.dart
rename to example/test/features/last_login/presentation/last_login_presenter_test.dart
index d589b321..38fa23e9 100644
--- a/packages/clean_framework/example/test/features/last_login/presentation/last_login_presenter_test.dart
+++ b/example/test/features/last_login/presentation/last_login_presenter_test.dart
@@ -1,7 +1,7 @@
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework_example/features/last_login/domain/last_login_use_case.dart';
-import 'package:clean_framework_example/features/last_login/presentation/last_login_presenter.dart';
-import 'package:clean_framework_example/features/last_login/presentation/last_login_view_model.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/last_login/domain/last_login_use_case.dart';
+import 'package:example/features/last_login/presentation/last_login_presenter.dart';
+import 'package:example/features/last_login/presentation/last_login_view_model.dart';
import 'package:clean_framework_test/clean_framework_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
diff --git a/packages/clean_framework/example/test/features/last_login/presentation/last_login_ui_test.dart b/example/test/features/last_login/presentation/last_login_ui_test.dart
similarity index 87%
rename from packages/clean_framework/example/test/features/last_login/presentation/last_login_ui_test.dart
rename to example/test/features/last_login/presentation/last_login_ui_test.dart
index d47ec12b..c85ea040 100644
--- a/packages/clean_framework/example/test/features/last_login/presentation/last_login_ui_test.dart
+++ b/example/test/features/last_login/presentation/last_login_ui_test.dart
@@ -1,8 +1,8 @@
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework_example/features/last_login/domain/last_login_use_case.dart';
-import 'package:clean_framework_example/features/last_login/presentation/last_login_presenter.dart';
-import 'package:clean_framework_example/features/last_login/presentation/last_login_ui.dart';
-import 'package:clean_framework_example/features/last_login/presentation/last_login_view_model.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/last_login/domain/last_login_use_case.dart';
+import 'package:example/features/last_login/presentation/last_login_presenter.dart';
+import 'package:example/features/last_login/presentation/last_login_ui.dart';
+import 'package:example/features/last_login/presentation/last_login_view_model.dart';
import 'package:clean_framework_test/clean_framework_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
diff --git a/packages/clean_framework/example/test/features/random_cat/domain/random_cat_use_case_test.dart b/example/test/features/random_cat/domain/random_cat_use_case_test.dart
similarity index 84%
rename from packages/clean_framework/example/test/features/random_cat/domain/random_cat_use_case_test.dart
rename to example/test/features/random_cat/domain/random_cat_use_case_test.dart
index 0e9c3d18..72fd5c9a 100644
--- a/packages/clean_framework/example/test/features/random_cat/domain/random_cat_use_case_test.dart
+++ b/example/test/features/random_cat/domain/random_cat_use_case_test.dart
@@ -1,7 +1,7 @@
import 'package:clean_framework/clean_framework.dart';
-import 'package:clean_framework/clean_framework_providers.dart';
-import 'package:clean_framework_example/features/random_cat/domain/random_cat_entity.dart';
-import 'package:clean_framework_example/providers.dart';
+import 'package:clean_framework/clean_framework_legacy.dart';
+import 'package:example/features/random_cat/domain/random_cat_entity.dart';
+import 'package:example/providers.dart';
import 'package:clean_framework_rest/clean_framework_rest.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -19,7 +19,7 @@ void main() {
final gateway = randomCatGatewayProvider.getGateway(providersContext);
gateway.transport = (request) async {
- return Right(RestSuccessResponse(
+ return Either.right(RestSuccessResponse(
data: {
'id': 420,
'webpurl':
@@ -54,7 +54,7 @@ void main() {
final gateway = randomCatGatewayProvider.getGateway(providersContext);
gateway.transport = (request) async {
- return Left(UnknownFailureResponse());
+ return Either.left(UnknownFailureResponse());
};
expect(
diff --git a/packages/clean_framework/example/test/home_page_test.dart b/example/test/home_page_test.dart
similarity index 83%
rename from packages/clean_framework/example/test/home_page_test.dart
rename to example/test/home_page_test.dart
index 53db2764..f1219540 100644
--- a/packages/clean_framework/example/test/home_page_test.dart
+++ b/example/test/home_page_test.dart
@@ -1,18 +1,17 @@
import 'package:clean_framework/clean_framework.dart';
import 'package:clean_framework/clean_framework_defaults.dart';
-import 'package:clean_framework_example/features/country/presentation/country_ui.dart';
-import 'package:clean_framework_example/features/last_login/presentation/last_login_ui.dart';
-import 'package:clean_framework_example/features/random_cat/presentation/random_cat_ui.dart';
-import 'package:clean_framework_example/home_page.dart';
-import 'package:clean_framework_example/providers.dart';
-import 'package:clean_framework_example/routes.dart';
+import 'package:example/demo_router.dart';
+import 'package:example/features/country/presentation/country_ui.dart';
+import 'package:example/features/last_login/presentation/last_login_ui.dart';
+import 'package:example/features/random_cat/presentation/random_cat_ui.dart';
+import 'package:example/home_page.dart';
+import 'package:example/providers.dart';
+import 'package:clean_framework_router/clean_framework_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
- tearDown(() {
- router.reset();
- });
+ loadProviders();
group('HomePage tests | ', () {
testWidgets(
@@ -80,7 +79,10 @@ void main() {
expect(graphQLTileFinder, findsOneWidget);
await tester.tap(graphQLTileFinder);
- await tester.pumpAndSettle();
+ // pumpAndSettle times out here; as the page has non-deterministic loading indicator
+ // so pumping each frame individually
+ await tester.pump();
+ await tester.pump();
expect(find.byType(CountryUI), findsOneWidget);
},
@@ -113,10 +115,13 @@ Widget buildWidget(Widget widget) {
child: AppProvidersContainer(
providersContext: providersContext,
onBuild: (_, __) {},
- child: MaterialApp.router(
- routeInformationParser: router.informationParser,
- routerDelegate: router.delegate,
- routeInformationProvider: router.informationProvider,
+ child: AppRouterScope(
+ create: () => DemoRouter(),
+ builder: (context) {
+ return MaterialApp.router(
+ routerConfig: context.router.config,
+ );
+ },
),
),
);
diff --git a/packages/clean_framework/example/test/main_test.dart b/example/test/main_test.dart
similarity index 86%
rename from packages/clean_framework/example/test/main_test.dart
rename to example/test/main_test.dart
index 7d67f2a0..b24761aa 100644
--- a/packages/clean_framework/example/test/main_test.dart
+++ b/example/test/main_test.dart
@@ -1,5 +1,5 @@
-import 'package:clean_framework_example/providers.dart';
-import 'package:clean_framework_example/main.dart' as app;
+import 'package:example/providers.dart';
+import 'package:example/main.dart' as app;
import 'package:clean_framework_firestore/clean_framework_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
diff --git a/packages/clean_framework/example/test/routes_test.dart b/example/test/routes_test.dart
similarity index 53%
rename from packages/clean_framework/example/test/routes_test.dart
rename to example/test/routes_test.dart
index 33e3593f..98864a03 100644
--- a/packages/clean_framework/example/test/routes_test.dart
+++ b/example/test/routes_test.dart
@@ -1,5 +1,6 @@
-import 'package:clean_framework_example/home_page.dart';
-import 'package:clean_framework_example/routes.dart';
+import 'package:example/demo_router.dart';
+import 'package:example/home_page.dart';
+import 'package:clean_framework_router/clean_framework_router.dart';
import 'package:flutter_test/flutter_test.dart';
import 'home_page_test.dart';
@@ -10,7 +11,10 @@ void main() {
(tester) async {
await tester.pumpWidget(buildWidget(HomePage()));
- router.open('/non-existent');
+ final router =
+ AppRouterScope.of(tester.element(find.byType(HomePage))).router;
+
+ router.goLocation('/non-existent');
await tester.pumpAndSettle();
expect(find.byType(Page404), findsOneWidget);
diff --git a/example/web/favicon.png b/example/web/favicon.png
new file mode 100644
index 00000000..8aaa46ac
Binary files /dev/null and b/example/web/favicon.png differ
diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png
new file mode 100644
index 00000000..b749bfef
Binary files /dev/null and b/example/web/icons/Icon-192.png differ
diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png
new file mode 100644
index 00000000..88cfd48d
Binary files /dev/null and b/example/web/icons/Icon-512.png differ
diff --git a/example/web/index.html b/example/web/index.html
new file mode 100644
index 00000000..1460b5e9
--- /dev/null
+++ b/example/web/index.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ example
+
+
+
+
+
+
+
+
diff --git a/example/web/manifest.json b/example/web/manifest.json
new file mode 100644
index 00000000..8c012917
--- /dev/null
+++ b/example/web/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "example",
+ "short_name": "example",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/melos.yaml b/melos.yaml
index 6b5b4b5c..731432e0 100644
--- a/melos.yaml
+++ b/melos.yaml
@@ -1,6 +1,7 @@
name: clean_framework
packages:
+ - example
- packages/**
command:
diff --git a/packages/clean_framework/CHANGELOG.md b/packages/clean_framework/CHANGELOG.md
index f63eafd5..cb33ff6d 100644
--- a/packages/clean_framework/CHANGELOG.md
+++ b/packages/clean_framework/CHANGELOG.md
@@ -1,4 +1,26 @@
# Changelog
+## 2.0.0
+**Jan 17, 2022**
+**Breaking Change**
+- Removed dependencies on sub packages. Sub-packages can be added separately as per the requirement.
+```text
+Sub-packages:
+- clean_framework_router
+- clean_framework_graphql
+- clean_framework_rest
+- clean_framework_firestore
+- clean_framework_test
+```
+- Added `AppProviderScope`
+- Remove old feature flagging classes in favor of new open feature based classes
+- Simplified Either implementation
+- Introduced `transformers` to **UseCase**; use cases are now lazily instantiated
+- Simplified provider creation and usage
+- Added `UseCaseProvider.autoDispose`
+- Added `UseCaseProviderBridge`
+
+For migration guide, please refer to [the docs](https://docs.page/MattHamburger/clean_framework/codelabs/clean-framework/migration-guide).
+
## 1.5.0
**Nov 1, 2022**
- Breakdown into sub packages.
diff --git a/packages/clean_framework/example/.gitignore b/packages/clean_framework/example/.gitignore
index 63938e2e..24476c5d 100644
--- a/packages/clean_framework/example/.gitignore
+++ b/packages/clean_framework/example/.gitignore
@@ -1,5 +1,4 @@
# Miscellaneous
-pubspec.lock
*.class
*.log
*.pyc
@@ -9,8 +8,7 @@ pubspec.lock
.buildlog/
.history
.svn/
-*.code-workspace
-/coverage
+migrate_working_dir/
# IntelliJ related
*.iml
@@ -25,54 +23,22 @@ pubspec.lock
# Flutter/Dart/Pub related
**/doc/api/
+**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
-build/
+/build/
-# Android related
-**/android/**/gradle-wrapper.jar
-**/android/.gradle
-**/android/captures/
-**/android/gradlew
-**/android/gradlew.bat
-**/android/local.properties
-**/android/**/GeneratedPluginRegistrant.java
+# Symbolication related
+app.*.symbols
-# iOS/XCode related
-**/ios/**/*.mode1v3
-**/ios/**/*.mode2v3
-**/ios/**/*.moved-aside
-**/ios/**/*.pbxuser
-**/ios/**/*.perspectivev3
-**/ios/**/*sync/
-**/ios/**/.sconsign.dblite
-**/ios/**/.tags*
-**/ios/**/.vagrant/
-**/ios/**/DerivedData/
-**/ios/**/Icon?
-**/ios/**/Pods/
-**/ios/**/.symlinks/
-**/ios/**/profile
-**/ios/**/xcuserdata
-**/ios/.generated/
-**/ios/Flutter/App.framework
-**/ios/Flutter/Flutter.framework
-**/ios/Flutter/Flutter.podspec
-**/ios/Flutter/Generated.xcconfig
-**/ios/Flutter/app.flx
-**/ios/Flutter/app.zip
-**/ios/Flutter/flutter_assets/
-**/ios/Flutter/flutter_export_environment.sh
-**/ios/ServiceDefinitions.json
-**/ios/Runner/GeneratedPluginRegistrant.*
+# Obfuscation related
+app.*.map.json
-# Exceptions to above rules.
-!**/ios/**/default.mode1v3
-!**/ios/**/default.mode2v3
-!**/ios/**/default.pbxuser
-!**/ios/**/default.perspectivev3
-!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/clean_framework/example/.metadata b/packages/clean_framework/example/.metadata
new file mode 100644
index 00000000..262ceed0
--- /dev/null
+++ b/packages/clean_framework/example/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled.
+
+version:
+ revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ channel: stable
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: android
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: ios
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: linux
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: macos
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: web
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ - platform: windows
+ create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+ base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/packages/clean_framework/example/README.md b/packages/clean_framework/example/README.md
index 9a33f403..8268e694 100644
--- a/packages/clean_framework/example/README.md
+++ b/packages/clean_framework/example/README.md
@@ -1,2 +1,3 @@
-# Example
+# Clean Framework Example
+An example application to demonstrate usage of clean_framework package.
\ No newline at end of file
diff --git a/packages/clean_framework/example/android/.gitignore b/packages/clean_framework/example/android/.gitignore
index 0a741cb4..6f568019 100644
--- a/packages/clean_framework/example/android/.gitignore
+++ b/packages/clean_framework/example/android/.gitignore
@@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
+**/*.keystore
+**/*.jks
diff --git a/packages/clean_framework/example/android/app/build.gradle b/packages/clean_framework/example/android/app/build.gradle
index 07124d61..bfefb711 100644
--- a/packages/clean_framework/example/android/app/build.gradle
+++ b/packages/clean_framework/example/android/app/build.gradle
@@ -26,26 +26,37 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 32
+ compileSdkVersion flutter.compileSdkVersion
+ ndkVersion flutter.ndkVersion
- sourceSets {
- main.java.srcDirs += 'src/main/kotlin'
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
}
- lintOptions {
- disable 'InvalidPackage'
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
- applicationId "com.acmesoftware.example"
- minSdkVersion 21
- targetSdkVersion 30
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.acmesoftware.clean_framework_example"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
+ minSdkVersion flutter.minSdkVersion
+ targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
diff --git a/packages/clean_framework/example/android/app/src/debug/AndroidManifest.xml b/packages/clean_framework/example/android/app/src/debug/AndroidManifest.xml
index 5d04f4bc..ff05e929 100644
--- a/packages/clean_framework/example/android/app/src/debug/AndroidManifest.xml
+++ b/packages/clean_framework/example/android/app/src/debug/AndroidManifest.xml
@@ -1,6 +1,7 @@
-
diff --git a/packages/clean_framework/example/android/app/src/main/AndroidManifest.xml b/packages/clean_framework/example/android/app/src/main/AndroidManifest.xml
index a7d36c8f..289bce58 100644
--- a/packages/clean_framework/example/android/app/src/main/AndroidManifest.xml
+++ b/packages/clean_framework/example/android/app/src/main/AndroidManifest.xml
@@ -1,16 +1,21 @@
-
+
+
+
diff --git a/packages/clean_framework/example/android/app/src/main/kotlin/com/acmesoftware/clean_framework_example/MainActivity.kt b/packages/clean_framework/example/android/app/src/main/kotlin/com/acmesoftware/clean_framework_example/MainActivity.kt
new file mode 100644
index 00000000..d908f0b5
--- /dev/null
+++ b/packages/clean_framework/example/android/app/src/main/kotlin/com/acmesoftware/clean_framework_example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.acmesoftware.clean_framework_example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/clean_framework/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/clean_framework/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000..f74085f3
--- /dev/null
+++ b/packages/clean_framework/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/packages/clean_framework/example/android/app/src/main/res/values-night/styles.xml b/packages/clean_framework/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..06952be7
--- /dev/null
+++ b/packages/clean_framework/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/packages/clean_framework/example/android/app/src/main/res/values/styles.xml b/packages/clean_framework/example/android/app/src/main/res/values/styles.xml
index 1f83a33f..cb1ef880 100644
--- a/packages/clean_framework/example/android/app/src/main/res/values/styles.xml
+++ b/packages/clean_framework/example/android/app/src/main/res/values/styles.xml
@@ -1,18 +1,18 @@
-
-
-
diff --git a/packages/clean_framework/example/android/app/src/profile/AndroidManifest.xml b/packages/clean_framework/example/android/app/src/profile/AndroidManifest.xml
index 5d04f4bc..ff05e929 100644
--- a/packages/clean_framework/example/android/app/src/profile/AndroidManifest.xml
+++ b/packages/clean_framework/example/android/app/src/profile/AndroidManifest.xml
@@ -1,6 +1,7 @@
-
diff --git a/packages/clean_framework/example/android/build.gradle b/packages/clean_framework/example/android/build.gradle
index 73d46bcf..83ae2200 100644
--- a/packages/clean_framework/example/android/build.gradle
+++ b/packages/clean_framework/example/android/build.gradle
@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.2.2'
+ classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
diff --git a/packages/clean_framework/example/android/gradle.properties b/packages/clean_framework/example/android/gradle.properties
index a6738207..94adc3a3 100644
--- a/packages/clean_framework/example/android/gradle.properties
+++ b/packages/clean_framework/example/android/gradle.properties
@@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
-android.enableR8=true
diff --git a/packages/clean_framework/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/clean_framework/example/android/gradle/wrapper/gradle-wrapper.properties
index 039eda99..cb24abda 100644
--- a/packages/clean_framework/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/clean_framework/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
-#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
diff --git a/packages/clean_framework/example/assets/sad-flareon.png b/packages/clean_framework/example/assets/sad-flareon.png
new file mode 100644
index 00000000..7c926c62
Binary files /dev/null and b/packages/clean_framework/example/assets/sad-flareon.png differ
diff --git a/packages/clean_framework/example/ios/.gitignore b/packages/clean_framework/example/ios/.gitignore
new file mode 100644
index 00000000..7a7f9873
--- /dev/null
+++ b/packages/clean_framework/example/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/clean_framework/example/ios/Flutter/AppFrameworkInfo.plist b/packages/clean_framework/example/ios/Flutter/AppFrameworkInfo.plist
index d2c61fc4..9625e105 100644
--- a/packages/clean_framework/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/packages/clean_framework/example/ios/Flutter/AppFrameworkInfo.plist
@@ -1,26 +1,26 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- App
- CFBundleIdentifier
- io.flutter.flutter.app
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- App
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleSignature
- ????
- CFBundleVersion
- 1.0
- MinimumOSVersion
- 9.0
-
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 11.0
+
diff --git a/packages/clean_framework/example/ios/Podfile b/packages/clean_framework/example/ios/Podfile
index 313ea4a1..88359b22 100644
--- a/packages/clean_framework/example/ios/Podfile
+++ b/packages/clean_framework/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-platform :ios, '11.0'
+# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/packages/clean_framework/example/ios/Runner.xcodeproj/project.pbxproj b/packages/clean_framework/example/ios/Runner.xcodeproj/project.pbxproj
index 1193fe64..a72a2790 100644
--- a/packages/clean_framework/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/clean_framework/example/ios/Runner.xcodeproj/project.pbxproj
@@ -8,11 +8,9 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 3AC18C1ED27F481C1BED83BE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDFC7454685DF6498BF1978D /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
- 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
- 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 78E4F938059396E024EB71D2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0968838A7D5D1BB77EB80101 /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -32,24 +30,23 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 0968838A7D5D1BB77EB80101 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 19D5F6AC73F9E8450638D8B0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 5195105C73D01DF3EB5583DC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 617B2E60B73B81FE47FDD48D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
- 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
- 8834B17F6053F1307161D81D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- C86FFF5B8092A5BF5E37B3D2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
- DDFC7454685DF6498BF1978D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- E2ED0B9341254BC4E8A32D0C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -57,30 +54,30 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 3AC18C1ED27F481C1BED83BE /* Pods_Runner.framework in Frameworks */,
+ 78E4F938059396E024EB71D2 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 356C1EDA1C4718B997F03609 /* Pods */ = {
+ 0AA87682C795D3DF9DEDDD49 /* Frameworks */ = {
isa = PBXGroup;
children = (
- E2ED0B9341254BC4E8A32D0C /* Pods-Runner.debug.xcconfig */,
- 8834B17F6053F1307161D81D /* Pods-Runner.release.xcconfig */,
- C86FFF5B8092A5BF5E37B3D2 /* Pods-Runner.profile.xcconfig */,
+ 0968838A7D5D1BB77EB80101 /* Pods_Runner.framework */,
);
- name = Pods;
- path = Pods;
+ name = Frameworks;
sourceTree = "";
};
- 8D059E797CB2EFECD38A668A /* Frameworks */ = {
+ 74290CDFC45CF0DBEFB30AB6 /* Pods */ = {
isa = PBXGroup;
children = (
- DDFC7454685DF6498BF1978D /* Pods_Runner.framework */,
+ 5195105C73D01DF3EB5583DC /* Pods-Runner.debug.xcconfig */,
+ 617B2E60B73B81FE47FDD48D /* Pods-Runner.release.xcconfig */,
+ 19D5F6AC73F9E8450638D8B0 /* Pods-Runner.profile.xcconfig */,
);
- name = Frameworks;
+ name = Pods;
+ path = Pods;
sourceTree = "";
};
9740EEB11CF90186004384FC /* Flutter */ = {
@@ -100,8 +97,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
- 356C1EDA1C4718B997F03609 /* Pods */,
- 8D059E797CB2EFECD38A668A /* Frameworks */,
+ 74290CDFC45CF0DBEFB30AB6 /* Pods */,
+ 0AA87682C795D3DF9DEDDD49 /* Frameworks */,
);
sourceTree = "";
};
@@ -116,27 +113,18 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
- 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
- 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
- 97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "";
};
- 97C146F11CF9000F007C117D /* Supporting Files */ = {
- isa = PBXGroup;
- children = (
- 97C146F21CF9000F007C117D /* main.m */,
- );
- name = "Supporting Files";
- sourceTree = "";
- };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -144,14 +132,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
- CF2D77B37A31DFC34F7DB60E /* [CP] Check Pods Manifest.lock */,
+ CECBBFDD7DFC0AEA322230B0 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- 4EC7F78FD432909300C8796A /* [CP] Embed Pods Frameworks */,
+ 64F4F9758C06ADDC72BCEC53 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -169,15 +157,16 @@
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
- ORGANIZATIONNAME = "The Chromium Authors";
+ ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 3.2";
+ compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@@ -201,7 +190,6 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
@@ -224,42 +212,17 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
- 4EC7F78FD432909300C8796A /* [CP] Embed Pods Frameworks */ = {
+ 64F4F9758C06ADDC72BCEC53 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
- inputPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
- "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC/openssl_grpc.framework",
- "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework",
- "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework",
- "${BUILT_PRODUCTS_DIR}/FirebaseFirestore/FirebaseFirestore.framework",
- "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
- "${BUILT_PRODUCTS_DIR}/Libuv-gRPC/uv.framework",
- "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
- "${BUILT_PRODUCTS_DIR}/abseil/absl.framework",
- "${BUILT_PRODUCTS_DIR}/gRPC-C++/grpcpp.framework",
- "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
- "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
- "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
- "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
- outputPaths = (
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestore.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/uv.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/absl.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpcpp.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -280,7 +243,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
- CF2D77B37A31DFC34F7DB60E /* [CP] Check Pods Manifest.lock */ = {
+ CECBBFDD7DFC0AEA322230B0 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -309,8 +272,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
- 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -381,6 +343,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -391,23 +354,19 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = HW5PK5B369;
ENABLE_BITCODE = NO;
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)/Flutter",
- );
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- LIBRARY_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)/Flutter",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.huntington.core;
+ PRODUCT_BUNDLE_IDENTIFIER = com.acmesoftware.cleanFrameworkExample;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
@@ -511,6 +470,9 @@
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -521,23 +483,20 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = HW5PK5B369;
ENABLE_BITCODE = NO;
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)/Flutter",
- );
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- LIBRARY_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)/Flutter",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.huntington.core;
+ PRODUCT_BUNDLE_IDENTIFIER = com.acmesoftware.cleanFrameworkExample;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -547,23 +506,19 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = HW5PK5B369;
ENABLE_BITCODE = NO;
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)/Flutter",
- );
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- LIBRARY_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)/Flutter",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.huntington.core;
+ PRODUCT_BUNDLE_IDENTIFIER = com.acmesoftware.cleanFrameworkExample;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
diff --git a/packages/clean_framework/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/clean_framework/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/packages/clean_framework/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/packages/clean_framework/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/clean_framework/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3db53b6e..c87d15a3 100644
--- a/packages/clean_framework/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/clean_framework/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
-
-
-
-
+
+
-
-
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/packages/clean_framework/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/clean_framework/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/packages/clean_framework/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/packages/clean_framework/example/ios/Runner/AppDelegate.swift b/packages/clean_framework/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 00000000..70693e4a
--- /dev/null
+++ b/packages/clean_framework/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/packages/clean_framework/example/ios/Runner/Info.plist b/packages/clean_framework/example/ios/Runner/Info.plist
index a9cf4e86..56c27a60 100644
--- a/packages/clean_framework/example/ios/Runner/Info.plist
+++ b/packages/clean_framework/example/ios/Runner/Info.plist
@@ -1,47 +1,51 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- CleanFrameworkExample
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Clean Framework Example
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ clean_framework_example
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
diff --git a/packages/clean_framework/example/ios/Runner/Runner-Bridging-Header.h b/packages/clean_framework/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 00000000..308a2a56
--- /dev/null
+++ b/packages/clean_framework/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/clean_framework/example/lib/core/pokemon/pokemon_external_interface.dart b/packages/clean_framework/example/lib/core/pokemon/pokemon_external_interface.dart
new file mode 100644
index 00000000..7eac63c0
--- /dev/null
+++ b/packages/clean_framework/example/lib/core/pokemon/pokemon_external_interface.dart
@@ -0,0 +1,34 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_request.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_success_response.dart';
+import 'package:dio/dio.dart';
+
+class PokemonExternalInterface
+ extends ExternalInterface {
+ PokemonExternalInterface({
+ Dio? dio,
+ }) : _dio = dio ?? Dio(BaseOptions(baseUrl: 'https://pokeapi.co/api/v2/'));
+
+ final Dio _dio;
+
+ @override
+ void handleRequest() {
+ on(
+ (request, send) async {
+ final response = await _dio.get>(
+ request.resource,
+ queryParameters: request.queryParams,
+ );
+
+ final data = response.data!;
+
+ send(PokemonSuccessResponse(data: data));
+ },
+ );
+ }
+
+ @override
+ FailureResponse onError(Object error) {
+ return UnknownFailureResponse(error);
+ }
+}
diff --git a/packages/clean_framework/example/lib/core/pokemon/pokemon_request.dart b/packages/clean_framework/example/lib/core/pokemon/pokemon_request.dart
new file mode 100644
index 00000000..1bc49967
--- /dev/null
+++ b/packages/clean_framework/example/lib/core/pokemon/pokemon_request.dart
@@ -0,0 +1,9 @@
+import 'package:clean_framework/clean_framework.dart';
+
+abstract class PokemonRequest extends Request {
+ Map get queryParams => {};
+}
+
+abstract class GetPokemonRequest extends PokemonRequest {
+ String get resource;
+}
diff --git a/packages/clean_framework/example/lib/core/pokemon/pokemon_success_response.dart b/packages/clean_framework/example/lib/core/pokemon/pokemon_success_response.dart
new file mode 100644
index 00000000..c29048a9
--- /dev/null
+++ b/packages/clean_framework/example/lib/core/pokemon/pokemon_success_response.dart
@@ -0,0 +1,7 @@
+import 'package:clean_framework/clean_framework.dart';
+
+class PokemonSuccessResponse extends SuccessResponse {
+ PokemonSuccessResponse({required this.data});
+
+ final Map data;
+}
diff --git a/packages/clean_framework/example/lib/features/home/domain/home_entity.dart b/packages/clean_framework/example/lib/features/home/domain/home_entity.dart
new file mode 100644
index 00000000..3b0f4a8d
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/domain/home_entity.dart
@@ -0,0 +1,42 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/home/models/pokemon_model.dart';
+
+enum HomeStatus { initial, loading, loaded, failed }
+
+class HomeEntity extends Entity {
+ HomeEntity({
+ this.pokemons = const [],
+ this.pokemonNameQuery = '',
+ this.status = HomeStatus.initial,
+ this.isRefresh = false,
+ this.lastViewedPokemon = '',
+ });
+
+ final List pokemons;
+ final String pokemonNameQuery;
+ final HomeStatus status;
+ final bool isRefresh;
+ final String lastViewedPokemon;
+
+ @override
+ List get props {
+ return [pokemons, pokemonNameQuery, status, isRefresh, lastViewedPokemon];
+ }
+
+ @override
+ HomeEntity copyWith({
+ List? pokemons,
+ String? pokemonNameQuery,
+ HomeStatus? status,
+ bool? isRefresh,
+ String? lastViewedPokemon,
+ }) {
+ return HomeEntity(
+ pokemons: pokemons ?? this.pokemons,
+ pokemonNameQuery: pokemonNameQuery ?? this.pokemonNameQuery,
+ status: status ?? this.status,
+ isRefresh: isRefresh ?? this.isRefresh,
+ lastViewedPokemon: lastViewedPokemon ?? this.lastViewedPokemon,
+ );
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/home/domain/home_ui_output.dart b/packages/clean_framework/example/lib/features/home/domain/home_ui_output.dart
new file mode 100644
index 00000000..b13609ef
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/domain/home_ui_output.dart
@@ -0,0 +1,20 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/home/domain/home_entity.dart';
+import 'package:clean_framework_example/features/home/models/pokemon_model.dart';
+
+class HomeUIOutput extends Output {
+ HomeUIOutput({
+ required this.pokemons,
+ required this.status,
+ required this.isRefresh,
+ required this.lastViewedPokemon,
+ });
+
+ final List pokemons;
+ final HomeStatus status;
+ final bool isRefresh;
+ final String lastViewedPokemon;
+
+ @override
+ List get props => [pokemons, status, isRefresh, lastViewedPokemon];
+}
diff --git a/packages/clean_framework/example/lib/features/home/domain/home_use_case.dart b/packages/clean_framework/example/lib/features/home/domain/home_use_case.dart
new file mode 100644
index 00000000..77795d0b
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/domain/home_use_case.dart
@@ -0,0 +1,105 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/home/domain/home_entity.dart';
+import 'package:clean_framework_example/features/home/domain/home_ui_output.dart';
+import 'package:clean_framework_example/features/home/external_interface/pokemon_collection_gateway.dart';
+import 'package:clean_framework_example/features/home/models/pokemon_model.dart';
+
+const _spritesBaseUrl =
+ 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites';
+
+class HomeUseCase extends UseCase {
+ HomeUseCase()
+ : super(
+ entity: HomeEntity(),
+ transformers: [
+ HomeUIOutputTransformer(),
+ PokemonSearchInputTransformer(),
+ LastViewedPokemonInputTransformer(),
+ ],
+ );
+
+ Future fetchPokemons({bool isRefresh = false}) async {
+ if (!isRefresh) {
+ entity = entity.copyWith(status: HomeStatus.loading);
+ }
+
+ await request(
+ PokemonCollectionGatewayOutput(),
+ onSuccess: (success) {
+ final pokemons = success.pokemonIdentities.map(_resolvePokemon);
+
+ return entity.copyWith(
+ pokemons: pokemons.toList(growable: false),
+ status: HomeStatus.loaded,
+ isRefresh: isRefresh,
+ );
+ },
+ onFailure: (failure) {
+ return entity.copyWith(
+ status: HomeStatus.failed,
+ isRefresh: isRefresh,
+ );
+ },
+ );
+
+ if (isRefresh) {
+ entity = entity.copyWith(isRefresh: false, status: HomeStatus.loaded);
+ }
+ }
+
+ PokemonModel _resolvePokemon(PokemonIdentity pokemon) {
+ return PokemonModel(
+ name: pokemon.name.toUpperCase(),
+ imageUrl: '$_spritesBaseUrl/pokemon/other/dream-world/${pokemon.id}.svg',
+ );
+ }
+}
+
+class PokemonSearchInput extends Input {
+ PokemonSearchInput({required this.name});
+
+ final String name;
+}
+
+class HomeUIOutputTransformer
+ extends OutputTransformer {
+ @override
+ HomeUIOutput transform(HomeEntity entity) {
+ final filteredPokemons = entity.pokemons.where(
+ (pokemon) {
+ final pokeName = pokemon.name.toLowerCase();
+ return pokeName.contains(entity.pokemonNameQuery.toLowerCase());
+ },
+ );
+
+ return HomeUIOutput(
+ pokemons: filteredPokemons.toList(growable: false),
+ status: entity.status,
+ isRefresh: entity.isRefresh,
+ lastViewedPokemon: entity.lastViewedPokemon,
+ );
+ }
+}
+
+class PokemonSearchInputTransformer
+ extends InputTransformer {
+ @override
+ HomeEntity transform(HomeEntity entity, PokemonSearchInput input) {
+ return entity.copyWith(pokemonNameQuery: input.name);
+ }
+}
+
+class LastViewedPokemonInput extends Input {
+ LastViewedPokemonInput({required this.name});
+
+ final String name;
+}
+
+class LastViewedPokemonInputTransformer
+ extends InputTransformer {
+ @override
+ HomeEntity transform(HomeEntity entity, LastViewedPokemonInput input) {
+ return entity.copyWith(lastViewedPokemon: input.name);
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/home/external_interface/pokemon_collection_gateway.dart b/packages/clean_framework/example/lib/features/home/external_interface/pokemon_collection_gateway.dart
new file mode 100644
index 00000000..f8064fc0
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/external_interface/pokemon_collection_gateway.dart
@@ -0,0 +1,70 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_request.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_success_response.dart';
+
+class PokemonCollectionGateway extends Gateway<
+ PokemonCollectionGatewayOutput,
+ PokemonCollectionRequest,
+ PokemonSuccessResponse,
+ PokemonCollectionSuccessInput> {
+ @override
+ PokemonCollectionRequest buildRequest(PokemonCollectionGatewayOutput output) {
+ return PokemonCollectionRequest();
+ }
+
+ @override
+ FailureInput onFailure(FailureResponse failureResponse) {
+ return FailureInput(message: failureResponse.message);
+ }
+
+ @override
+ PokemonCollectionSuccessInput onSuccess(PokemonSuccessResponse response) {
+ final deserializer = Deserializer(response.data);
+
+ return PokemonCollectionSuccessInput(
+ pokemonIdentities: deserializer.getList(
+ 'results',
+ converter: PokemonIdentity.fromJson,
+ ),
+ );
+ }
+}
+
+class PokemonCollectionGatewayOutput extends Output {
+ @override
+ List get props => [];
+}
+
+class PokemonCollectionSuccessInput extends SuccessInput {
+ PokemonCollectionSuccessInput({required this.pokemonIdentities});
+
+ final List pokemonIdentities;
+}
+
+final _pokemonResUrlRegex = RegExp(r'https://pokeapi.co/api/v2/pokemon/(\d+)/');
+
+class PokemonIdentity {
+ PokemonIdentity({required this.name, required this.id});
+
+ final String name;
+ final String id;
+
+ factory PokemonIdentity.fromJson(Map json) {
+ final deserializer = Deserializer(json);
+
+ final match = _pokemonResUrlRegex.firstMatch(deserializer.getString('url'));
+
+ return PokemonIdentity(
+ name: deserializer.getString('name'),
+ id: match?.group(1) ?? '0',
+ );
+ }
+}
+
+class PokemonCollectionRequest extends GetPokemonRequest {
+ @override
+ String get resource => 'pokemon';
+
+ @override
+ Map get queryParams => {'limit': 1000};
+}
diff --git a/packages/clean_framework/example/lib/features/home/models/pokemon_model.dart b/packages/clean_framework/example/lib/features/home/models/pokemon_model.dart
new file mode 100644
index 00000000..18511a0d
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/models/pokemon_model.dart
@@ -0,0 +1,9 @@
+class PokemonModel {
+ PokemonModel({
+ required this.name,
+ required this.imageUrl,
+ });
+
+ final String name;
+ final String imageUrl;
+}
diff --git a/packages/clean_framework/example/lib/features/home/presentation/home_presenter.dart b/packages/clean_framework/example/lib/features/home/presentation/home_presenter.dart
new file mode 100644
index 00000000..738167e4
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/presentation/home_presenter.dart
@@ -0,0 +1,47 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/home/domain/home_entity.dart';
+import 'package:clean_framework_example/features/home/domain/home_ui_output.dart';
+import 'package:clean_framework_example/features/home/domain/home_use_case.dart';
+import 'package:clean_framework_example/features/home/presentation/home_view_model.dart';
+import 'package:clean_framework_example/providers.dart';
+import 'package:flutter/material.dart';
+
+class HomePresenter
+ extends Presenter {
+ HomePresenter({
+ required super.builder,
+ }) : super(provider: homeUseCaseProvider);
+
+ @override
+ void onLayoutReady(BuildContext context, HomeUseCase useCase) {
+ useCase.fetchPokemons();
+ }
+
+ @override
+ HomeViewModel createViewModel(HomeUseCase useCase, HomeUIOutput output) {
+ return HomeViewModel(
+ pokemons: output.pokemons,
+ onSearch: (query) => useCase.setInput(PokemonSearchInput(name: query)),
+ onRefresh: () => useCase.fetchPokemons(isRefresh: true),
+ onRetry: useCase.fetchPokemons,
+ isLoading: output.status == HomeStatus.loading,
+ hasFailedLoading: output.status == HomeStatus.failed,
+ lastViewedPokemon: output.lastViewedPokemon,
+ );
+ }
+
+ @override
+ void onOutputUpdate(BuildContext context, HomeUIOutput output) {
+ if (output.isRefresh) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ output.status == HomeStatus.failed
+ ? 'Sorry, failed refreshing pokemons!'
+ : 'Refreshed pokemons successfully!',
+ ),
+ ),
+ );
+ }
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/home/presentation/home_ui.dart b/packages/clean_framework/example/lib/features/home/presentation/home_ui.dart
new file mode 100644
index 00000000..368d13fc
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/presentation/home_ui.dart
@@ -0,0 +1,118 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/home/presentation/home_presenter.dart';
+import 'package:clean_framework_example/features/home/presentation/home_view_model.dart';
+import 'package:clean_framework_example/routing/routes.dart';
+import 'package:clean_framework_example/widgets/pokemon_card.dart';
+import 'package:clean_framework_example/widgets/pokemon_search_field.dart';
+import 'package:clean_framework_router/clean_framework_router.dart';
+import 'package:flutter/material.dart';
+
+class HomeUI extends UI {
+ @override
+ HomePresenter create(PresenterBuilder builder) {
+ return HomePresenter(builder: builder);
+ }
+
+ @override
+ Widget build(BuildContext context, HomeViewModel viewModel) {
+ final textTheme = Theme.of(context).textTheme;
+
+ Widget child;
+ if (viewModel.isLoading) {
+ child = Center(child: CircularProgressIndicator());
+ } else if (viewModel.hasFailedLoading) {
+ child = _LoadingFailed(onRetry: viewModel.onRetry);
+ } else {
+ child = RefreshIndicator(
+ onRefresh: viewModel.onRefresh,
+ child: Scrollbar(
+ thumbVisibility: true,
+ child: ListView.builder(
+ prototypeItem: SizedBox(height: 176), // 160 + 16
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ itemBuilder: (context, index) {
+ final pokemon = viewModel.pokemons[index];
+
+ return PokemonCard(
+ key: ValueKey(pokemon.name),
+ imageUrl: pokemon.imageUrl,
+ name: pokemon.name,
+ heroTag: pokemon.name,
+ onTap: () => context.router.go(
+ Routes.profile,
+ params: {'pokemon_name': pokemon.name},
+ ),
+ );
+ },
+ itemCount: viewModel.pokemons.length,
+ ),
+ ),
+ );
+ }
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('Pokémon'),
+ centerTitle: false,
+ titleTextStyle: textTheme.displaySmall!.copyWith(
+ fontWeight: FontWeight.w300,
+ ),
+ bottom: viewModel.isLoading || viewModel.hasFailedLoading
+ ? null
+ : PokemonSearchField(onChanged: viewModel.onSearch),
+ actions: [
+ if (viewModel.lastViewedPokemon.isNotEmpty)
+ Text.rich(
+ TextSpan(
+ text: 'Last Viewed: ',
+ children: [
+ TextSpan(
+ text: viewModel.lastViewedPokemon,
+ style: textTheme.labelSmall,
+ ),
+ ],
+ style: textTheme.bodySmall,
+ ),
+ ),
+ const SizedBox(width: 16),
+ ],
+ ),
+ body: child,
+ );
+ }
+}
+
+class _LoadingFailed extends StatelessWidget {
+ const _LoadingFailed({required this.onRetry});
+
+ final VoidCallback onRetry;
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Image.asset('assets/sad-flareon.png', height: 300),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Oops',
+ style: Theme.of(context).textTheme.displaySmall,
+ ),
+ const SizedBox(height: 8),
+ Text('I lost my fellow Pokémons'),
+ const SizedBox(height: 24),
+ OutlinedButton(
+ onPressed: onRetry,
+ child: Text('Help Flareon, find her friends'),
+ ),
+ const SizedBox(height: 64),
+ ],
+ ),
+ );
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/home/presentation/home_view_model.dart b/packages/clean_framework/example/lib/features/home/presentation/home_view_model.dart
new file mode 100644
index 00000000..7a3c5352
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/home/presentation/home_view_model.dart
@@ -0,0 +1,29 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/home/models/pokemon_model.dart';
+import 'package:flutter/foundation.dart';
+
+class HomeViewModel extends ViewModel {
+ HomeViewModel({
+ required this.pokemons,
+ required this.isLoading,
+ required this.hasFailedLoading,
+ required this.lastViewedPokemon,
+ required this.onRetry,
+ required this.onRefresh,
+ required this.onSearch,
+ });
+
+ final List pokemons;
+ final bool isLoading;
+ final bool hasFailedLoading;
+ final String lastViewedPokemon;
+
+ final VoidCallback onRetry;
+ final AsyncCallback onRefresh;
+ final ValueChanged onSearch;
+
+ @override
+ List get props {
+ return [pokemons, isLoading, hasFailedLoading, lastViewedPokemon];
+ }
+}
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
deleted file mode 100644
index 29dd733d..00000000
--- a/packages/clean_framework/example/lib/features/last_login/domain/last_login_use_case.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-import 'package:clean_framework/clean_framework_providers.dart';
-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,
- );
-
- 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;
- });
- }
-}
-
-class LastLoginUIOutput extends Output {
- final DateTime lastLogin;
-
- LastLoginUIOutput({required this.lastLogin});
- @override
- List get props => [lastLogin];
-}
-
-class LastLoginCTAUIOutput extends Output {
- final bool isLoading;
-
- LastLoginCTAUIOutput({required this.isLoading});
-
- @override
- List get props => [isLoading];
-}
-
-class LastLoginDateOutput extends Output {
- @override
- List get props => [];
-}
-
-class LastLoginDateInput extends SuccessInput {
- final DateTime lastLogin;
-
- LastLoginDateInput(this.lastLogin);
-}
diff --git a/packages/clean_framework/example/lib/features/profile/domain/profile_entity.dart b/packages/clean_framework/example/lib/features/profile/domain/profile_entity.dart
new file mode 100644
index 00000000..924190d5
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/domain/profile_entity.dart
@@ -0,0 +1,49 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/profile/models/pokemon_profile_model.dart';
+
+class ProfileEntity extends Entity {
+ ProfileEntity({
+ this.name = '',
+ this.types = const [],
+ this.description = '',
+ this.height = 0,
+ this.weight = 0,
+ this.stats = const [],
+ });
+
+ final String name;
+ final List types;
+ final String description;
+ final int height;
+ final int weight;
+ final List stats;
+
+ @override
+ List get props => [name, types, description, height, weight, stats];
+
+ @override
+ ProfileEntity copyWith({
+ String? name,
+ List? types,
+ String? description,
+ int? height,
+ int? weight,
+ List? stats,
+ }) {
+ return ProfileEntity(
+ name: name ?? this.name,
+ types: types ?? this.types,
+ description: description ?? this.description,
+ height: height ?? this.height,
+ weight: weight ?? this.weight,
+ stats: stats ?? this.stats,
+ );
+ }
+}
+
+class PokemonStat {
+ PokemonStat({required this.name, required this.point});
+
+ final String name;
+ final int point;
+}
diff --git a/packages/clean_framework/example/lib/features/profile/domain/profile_ui_output.dart b/packages/clean_framework/example/lib/features/profile/domain/profile_ui_output.dart
new file mode 100644
index 00000000..4bd2940d
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/domain/profile_ui_output.dart
@@ -0,0 +1,21 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/profile/domain/profile_entity.dart';
+
+class ProfileUIOutput extends Output {
+ ProfileUIOutput({
+ required this.types,
+ required this.description,
+ required this.height,
+ required this.weight,
+ required this.stats,
+ });
+
+ final List types;
+ final String description;
+ final double height;
+ final double weight;
+ final List stats;
+
+ @override
+ List get props => [types, description, height, weight, stats];
+}
diff --git a/packages/clean_framework/example/lib/features/profile/domain/profile_use_case.dart b/packages/clean_framework/example/lib/features/profile/domain/profile_use_case.dart
new file mode 100644
index 00000000..3bdd843f
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/domain/profile_use_case.dart
@@ -0,0 +1,80 @@
+import 'dart:math';
+
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/profile/domain/profile_entity.dart';
+import 'package:clean_framework_example/features/profile/domain/profile_ui_output.dart';
+import 'package:clean_framework_example/features/profile/external_interface/pokemon_profile_gateway.dart';
+import 'package:clean_framework_example/features/profile/external_interface/pokemon_species_gateway.dart';
+
+class ProfileUseCase extends UseCase {
+ ProfileUseCase()
+ : super(
+ entity: ProfileEntity(),
+ transformers: [ProfileUIOutputTransformer()],
+ );
+
+ void fetchPokemonProfile(String name) {
+ final pokeName = name.toLowerCase();
+
+ request(
+ PokemonSpeciesGatewayOutput(name: pokeName),
+ onSuccess: (success) {
+ final descriptions = success.species.descriptions.where(
+ (desc) => desc.language == 'en',
+ );
+
+ final randomIndex = Random().nextInt(descriptions.length);
+
+ return entity.copyWith(
+ description: descriptions.elementAt(randomIndex).text,
+ );
+ },
+ onFailure: (failure) => entity,
+ );
+
+ request(
+ PokemonProfileGatewayOutput(name: pokeName),
+ onSuccess: (success) {
+ final profile = success.profile;
+
+ return entity.copyWith(
+ name: name,
+ types: profile.types,
+ height: profile.height,
+ weight: profile.weight,
+ stats: profile.stats,
+ );
+ },
+ onFailure: (failure) => entity,
+ );
+ }
+}
+
+class ProfileUIOutputTransformer
+ extends OutputTransformer {
+ @override
+ ProfileUIOutput transform(ProfileEntity entity) {
+ return ProfileUIOutput(
+ types: entity.types,
+ description: entity.description.replaceAll(RegExp(r'[\n\f]'), ' '),
+ height: entity.height / 10,
+ weight: entity.weight / 10,
+ stats: entity.stats.map(
+ (s) {
+ return PokemonStat(
+ name: _kebabToTitleCase(s.name),
+ point: s.baseStat,
+ );
+ },
+ ).toList(growable: false),
+ );
+ }
+
+ String _kebabToTitleCase(String input) {
+ return input
+ .replaceAll('special', 'sp.')
+ .split('-')
+ .map((s) => s[0].toUpperCase() + s.substring(1))
+ .join(' ');
+ }
+}
diff --git a/packages/clean_framework/example/lib/features/profile/external_interface/pokemon_profile_gateway.dart b/packages/clean_framework/example/lib/features/profile/external_interface/pokemon_profile_gateway.dart
new file mode 100644
index 00000000..0674846f
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/external_interface/pokemon_profile_gateway.dart
@@ -0,0 +1,48 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_request.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_success_response.dart';
+import 'package:clean_framework_example/features/profile/models/pokemon_profile_model.dart';
+
+class PokemonProfileGateway extends Gateway {
+ @override
+ PokemonProfileRequest buildRequest(PokemonProfileGatewayOutput output) {
+ return PokemonProfileRequest(name: output.name);
+ }
+
+ @override
+ FailureInput onFailure(FailureResponse failureResponse) {
+ return FailureInput(message: failureResponse.message);
+ }
+
+ @override
+ PokemonProfileSuccessInput onSuccess(PokemonSuccessResponse response) {
+ return PokemonProfileSuccessInput(
+ profile: PokemonProfileModel.fromJson(response.data),
+ );
+ }
+}
+
+class PokemonProfileGatewayOutput extends Output {
+ PokemonProfileGatewayOutput({required this.name});
+
+ final String name;
+
+ @override
+ List get props => [name];
+}
+
+class PokemonProfileSuccessInput extends SuccessInput {
+ PokemonProfileSuccessInput({required this.profile});
+
+ final PokemonProfileModel profile;
+}
+
+class PokemonProfileRequest extends GetPokemonRequest {
+ PokemonProfileRequest({required this.name});
+
+ final String name;
+
+ @override
+ String get resource => 'pokemon/$name';
+}
diff --git a/packages/clean_framework/example/lib/features/profile/external_interface/pokemon_species_gateway.dart b/packages/clean_framework/example/lib/features/profile/external_interface/pokemon_species_gateway.dart
new file mode 100644
index 00000000..413a4157
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/external_interface/pokemon_species_gateway.dart
@@ -0,0 +1,48 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_request.dart';
+import 'package:clean_framework_example/core/pokemon/pokemon_success_response.dart';
+import 'package:clean_framework_example/features/profile/models/pokemon_species_model.dart';
+
+class PokemonSpeciesGateway extends Gateway {
+ @override
+ PokemonSpeciesRequest buildRequest(PokemonSpeciesGatewayOutput output) {
+ return PokemonSpeciesRequest(name: output.name);
+ }
+
+ @override
+ FailureInput onFailure(FailureResponse failureResponse) {
+ return FailureInput(message: failureResponse.message);
+ }
+
+ @override
+ PokemonSpeciesSuccessInput onSuccess(PokemonSuccessResponse response) {
+ return PokemonSpeciesSuccessInput(
+ species: PokemonSpeciesModel.fromJson(response.data),
+ );
+ }
+}
+
+class PokemonSpeciesGatewayOutput extends Output {
+ PokemonSpeciesGatewayOutput({required this.name});
+
+ final String name;
+
+ @override
+ List get props => [name];
+}
+
+class PokemonSpeciesSuccessInput extends SuccessInput {
+ PokemonSpeciesSuccessInput({required this.species});
+
+ final PokemonSpeciesModel species;
+}
+
+class PokemonSpeciesRequest extends GetPokemonRequest {
+ PokemonSpeciesRequest({required this.name});
+
+ final String name;
+
+ @override
+ String get resource => 'pokemon-species/$name';
+}
diff --git a/packages/clean_framework/example/lib/features/profile/models/pokemon_profile_model.dart b/packages/clean_framework/example/lib/features/profile/models/pokemon_profile_model.dart
new file mode 100644
index 00000000..81aa1f6b
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/models/pokemon_profile_model.dart
@@ -0,0 +1,52 @@
+import 'package:clean_framework/clean_framework.dart';
+
+class PokemonProfileModel {
+ PokemonProfileModel({
+ required this.baseExperience,
+ required this.height,
+ required this.weight,
+ required this.stats,
+ required this.types,
+ });
+
+ factory PokemonProfileModel.fromJson(Map json) {
+ final deserializer = Deserializer(json);
+
+ return PokemonProfileModel(
+ baseExperience: deserializer.getInt('base_experience'),
+ height: deserializer.getInt('height'),
+ weight: deserializer.getInt('weight'),
+ stats: deserializer.getList(
+ 'stats',
+ converter: PokemonStatModel.fromJson,
+ ),
+ types: deserializer.getList('types', converter: (t) => t['type']['name']),
+ );
+ }
+
+ final int baseExperience;
+ final int height;
+ final int weight;
+ final List stats;
+ final List types;
+}
+
+class PokemonStatModel {
+ PokemonStatModel({
+ required this.name,
+ required this.baseStat,
+ });
+
+ factory PokemonStatModel.fromJson(Map json) {
+ final deserializer = Deserializer(json);
+ final stat = deserializer('stat');
+
+ return PokemonStatModel(
+ name: stat.getString('name'),
+ baseStat: deserializer.getInt('base_stat'),
+ );
+ }
+
+ final String name;
+ final int baseStat;
+}
diff --git a/packages/clean_framework/example/lib/features/profile/models/pokemon_species_model.dart b/packages/clean_framework/example/lib/features/profile/models/pokemon_species_model.dart
new file mode 100644
index 00000000..15290929
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/models/pokemon_species_model.dart
@@ -0,0 +1,38 @@
+import 'package:clean_framework/clean_framework.dart';
+
+class PokemonSpeciesModel {
+ PokemonSpeciesModel({required this.descriptions});
+
+ factory PokemonSpeciesModel.fromJson(Map json) {
+ final deserializer = Deserializer(json);
+
+ return PokemonSpeciesModel(
+ descriptions: deserializer.getList(
+ 'flavor_text_entries',
+ converter: PokemonDescriptionModel.fromJson,
+ ),
+ );
+ }
+
+ final List descriptions;
+}
+
+class PokemonDescriptionModel {
+ PokemonDescriptionModel({
+ required this.text,
+ required this.language,
+ });
+
+ factory PokemonDescriptionModel.fromJson(Map json) {
+ final deserializer = Deserializer(json);
+ final language = deserializer('language');
+
+ return PokemonDescriptionModel(
+ text: deserializer.getString('flavor_text'),
+ language: language.getString('name'),
+ );
+ }
+
+ final String text;
+ final String language;
+}
diff --git a/packages/clean_framework/example/lib/features/profile/presentation/profile_presenter.dart b/packages/clean_framework/example/lib/features/profile/presentation/profile_presenter.dart
new file mode 100644
index 00000000..9d84441f
--- /dev/null
+++ b/packages/clean_framework/example/lib/features/profile/presentation/profile_presenter.dart
@@ -0,0 +1,35 @@
+import 'package:clean_framework/clean_framework.dart';
+import 'package:clean_framework_example/features/profile/domain/profile_ui_output.dart';
+import 'package:clean_framework_example/features/profile/domain/profile_use_case.dart';
+import 'package:clean_framework_example/features/profile/presentation/profile_view_model.dart';
+import 'package:clean_framework_example/providers.dart';
+import 'package:flutter/material.dart';
+
+class ProfilePresenter
+ extends Presenter