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. - - - -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. -### 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()); -} -``` - \ 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 - +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. - + -### 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). - - +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. - - - -### 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); ``` - - -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 => []; -} - ``` - +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 [![Coverage](https://codecov.io/gh/MattHamburger/clean_framework/branch/main/graph/badge.svg)](https://codecov.io/gh/MattHamburger/clean_framework) ## Depend on it Add this to your package's pubspec.yaml file: + ![clean_framework](https://img.shields.io/pub/v/clean_framework?label=%20clean_framework%3A%20&style=flat-square) -``` -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 { + ProfilePresenter({ + required super.builder, + required this.name, + }) : super(provider: profileUseCaseProvider); + + final String name; + + @protected + void onLayoutReady(BuildContext context, ProfileUseCase useCase) { + useCase.fetchPokemonProfile(name); + } + + @override + ProfileViewModel createViewModel( + ProfileUseCase useCase, + ProfileUIOutput output, + ) { + return ProfileViewModel( + pokemonTypes: output.types.map(PokemonType.new).toList(growable: false), + description: output.description, + height: '📏 ${output.height} m', + weight: '⚖️ ${output.weight} kg', + stats: output.stats, + ); + } +} diff --git a/packages/clean_framework/example/lib/features/profile/presentation/profile_ui.dart b/packages/clean_framework/example/lib/features/profile/presentation/profile_ui.dart new file mode 100644 index 00000000..e47d0544 --- /dev/null +++ b/packages/clean_framework/example/lib/features/profile/presentation/profile_ui.dart @@ -0,0 +1,231 @@ +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/presentation/profile_presenter.dart'; +import 'package:clean_framework_example/features/profile/presentation/profile_view_model.dart'; +import 'package:clean_framework_example/widgets/spotlight.dart'; +import 'package:flutter/material.dart'; + +class ProfileUI extends UI { + ProfileUI({required this.pokemonName}); + + final String pokemonName; + + @override + ProfilePresenter create(PresenterBuilder builder) { + return ProfilePresenter(builder: builder, name: pokemonName); + } + + @override + Widget build(BuildContext context, ProfileViewModel viewModel) { + return Scaffold( + appBar: AppBar( + title: Text(pokemonName), + elevation: 0, + backgroundColor: Colors.transparent, + ), + extendBodyBehindAppBar: true, + body: Spotlight( + height: 200, + heroTag: pokemonName, + cacheKey: pokemonName, + builder: (context) { + final pokeTypes = viewModel.pokemonTypes; + + return Card( + margin: EdgeInsets.zero, + elevation: Theme.of(context).brightness == Brightness.light ? 0 : 4, + color: Theme.of(context).colorScheme.surface.withAlpha(120), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(48), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 96, 24, 16), + child: SingleChildScrollView( + child: Column( + children: [ + Wrap( + runSpacing: 8, + spacing: 8, + children: pokeTypes.map(_PokeTypeChip.new).toList(), + ), + const SizedBox(height: 24), + AnimatedSize( + duration: const Duration(milliseconds: 300), + child: Text( + viewModel.description, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + const SizedBox(height: 24), + _BodyMeasurement( + height: viewModel.height, + weight: viewModel.weight, + ), + const SizedBox(height: 32), + _ProfileStats(stats: viewModel.stats), + ], + ), + ), + ), + ); + }, + ), + ); + } +} + +class _ProfileStats extends StatelessWidget { + const _ProfileStats({required this.stats}); + + final List stats; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + for (final stat in stats) + Padding( + padding: const EdgeInsets.all(6), + child: _StatRow(stat: stat), + ), + ], + ); + } +} + +class _StatRow extends StatelessWidget { + const _StatRow({required this.stat}); + + final PokemonStat stat; + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + final primaryColor = themeData.colorScheme.primary; + final pointFraction = stat.point / 255; + + return Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + Expanded( + child: Text( + stat.name, + style: themeData.textTheme.bodySmall, + ), + ), + const SizedBox(width: 16), + Text( + stat.point.toString(), + style: themeData.textTheme.titleSmall, + ), + ], + ), + ), + const SizedBox(width: 32), + Expanded( + flex: 3, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + color: primaryColor.withAlpha(stat.point), + value: pointFraction, + minHeight: 8, + backgroundColor: themeData.colorScheme.surfaceVariant, + ), + ), + ), + ], + ); + } +} + +class _BodyMeasurement extends StatelessWidget { + const _BodyMeasurement({ + required this.height, + required this.weight, + }); + + final String height; + final String weight; + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + final titleStyle = themeData.textTheme.displaySmall!.copyWith(fontSize: 20); + final subtitleStyle = themeData.textTheme.bodySmall!.copyWith( + color: themeData.colorScheme.outline, + ); + + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Column( + children: [ + Text(weight, style: titleStyle), + const SizedBox(height: 4), + Text('Weight', style: subtitleStyle), + ], + ), + ), + Container( + width: 1, + height: 32, + color: themeData.colorScheme.surfaceVariant, + ), + Expanded( + child: Column( + children: [ + Text(height, style: titleStyle), + const SizedBox(height: 4), + Text('Height', style: subtitleStyle), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _PokeTypeChip extends StatelessWidget { + const _PokeTypeChip(this.type); + + final PokemonType type; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + shape: StadiumBorder( + side: BorderSide(color: type.color), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(type.emoji), + Text( + type.name, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: type.color), + ), + ], + ), + ), + ); + } +} diff --git a/packages/clean_framework/example/lib/features/profile/presentation/profile_view_model.dart b/packages/clean_framework/example/lib/features/profile/presentation/profile_view_model.dart new file mode 100644 index 00000000..6b0c1e5f --- /dev/null +++ b/packages/clean_framework/example/lib/features/profile/presentation/profile_view_model.dart @@ -0,0 +1,74 @@ +import 'package:clean_framework/clean_framework.dart'; +import 'package:clean_framework_example/features/profile/domain/profile_entity.dart'; +import 'package:flutter/material.dart'; + +class ProfileViewModel extends ViewModel { + ProfileViewModel({ + required this.pokemonTypes, + required this.description, + required this.height, + required this.weight, + required this.stats, + }); + + final List pokemonTypes; + final String description; + final String height; + final String weight; + final List stats; + + @override + List get props => [pokemonTypes, description, height, weight, stats]; +} + +class PokemonType { + PokemonType(this.name) + : color = _pokemonTypeColors[name] ?? Colors.white, + emoji = _pokemonTypeEmojis[name] ?? ''; + + final String name; + final String emoji; + final Color color; +} + +const _pokemonTypeColors = { + 'normal': Color(0xFFA8A77A), + 'fire': Color(0xFFEE8130), + 'water': Color(0xFF6390F0), + 'electric': Color(0xFFF7D02C), + 'grass': Color(0xFF7AC74C), + 'ice': Color(0xFF96D9D6), + 'fighting': Color(0xFFC22E28), + 'poison': Color(0xFFA33EA1), + 'ground': Color(0xFFE2BF65), + 'flying': Color(0xFFA98FF3), + 'psychic': Color(0xFFF95587), + 'bug': Color(0xFFA6B91A), + 'rock': Color(0xFFB6A136), + 'ghost': Color(0xFF735797), + 'dragon': Color(0xFF6F35FC), + 'dark': Color(0xFF705746), + 'steel': Color(0xFFB7B7CE), + 'fairy': Color(0xFFD685AD), +}; + +const _pokemonTypeEmojis = { + 'normal': '🔶', + 'fire': '🔥', + 'water': '💧', + 'electric': '⚡️', + 'grass': '🍃', + 'ice': '❄️', + 'fighting': '👊', + 'poison': '🍄', + 'ground': '🏔', + 'flying': '💨', + 'psychic': '👁️', + 'bug': '🕸', + 'rock': '🗿', + 'ghost': '👻', + 'dragon': '🐲', + 'dark': '🌙', + 'steel': '⚙️', + 'fairy': '🌈', +}; diff --git a/packages/clean_framework/example/lib/main.dart b/packages/clean_framework/example/lib/main.dart index d91b6762..a113120a 100644 --- a/packages/clean_framework/example/lib/main.dart +++ b/packages/clean_framework/example/lib/main.dart @@ -1,45 +1,42 @@ -import 'dart:developer'; - import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework_example/asset_feature_provider.dart'; import 'package:clean_framework_example/providers.dart'; -import 'package:clean_framework_example/routes.dart'; +import 'package:clean_framework_example/routing/routes.dart'; +import 'package:clean_framework_router/clean_framework_router.dart'; import 'package:flutter/material.dart'; -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - loadProviders(); - - runApp(ExampleApp()); +void main() { + runApp(const MyApp()); } -class ExampleApp extends StatelessWidget { +class MyApp extends StatelessWidget { + const MyApp({super.key}); + @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: MaterialApp.router( - routeInformationParser: router.informationParser, - routerDelegate: router.delegate, - routeInformationProvider: router.informationProvider, - theme: ThemeData( - pageTransitionsTheme: PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - }, + return AppProviderScope( + externalInterfaceProviders: [ + pokemonExternalInterfaceProvider, + ], + child: AppRouterScope( + create: PokeRouter.new, + builder: (context) { + return MaterialApp.router( + title: 'Clean Framework Example', + routerConfig: context.router.config, + theme: ThemeData.from( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), + useMaterial3: true, + ), + darkTheme: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.green, + brightness: Brightness.dark, + ), + useMaterial3: true, ), - ), - ), + themeMode: ThemeMode.dark, + ); + }, ), ); } diff --git a/packages/clean_framework/example/lib/providers.dart b/packages/clean_framework/example/lib/providers.dart index 08987b32..40cb4215 100644 --- a/packages/clean_framework/example/lib/providers.dart +++ b/packages/clean_framework/example/lib/providers.dart @@ -1,86 +1,51 @@ 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_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 'package:clean_framework_example/core/pokemon/pokemon_external_interface.dart'; +import 'package:clean_framework_example/features/home/domain/home_use_case.dart'; +import 'package:clean_framework_example/features/home/external_interface/pokemon_collection_gateway.dart'; +import 'package:clean_framework_example/features/profile/domain/profile_entity.dart'; +import 'package:clean_framework_example/features/profile/domain/profile_use_case.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'; -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 homeUseCaseProvider = UseCaseProvider.autoDispose( + HomeUseCase.new, + (bridge) { + bridge.connect( + profileUseCaseProvider, + selector: (e) => e.name, + (oldPokeName, pokeName) { + if (oldPokeName != pokeName) { + bridge.useCase.setInput(LastViewedPokemonInput(name: pokeName)); + } + }, + ); + }, ); -final countryUseCaseProvider = UseCaseProvider( - (_) => CountryUseCase(), +final profileUseCaseProvider = UseCaseProvider( + ProfileUseCase.new, ); -final countryGatewayProvider = GatewayProvider( - (_) => CountryGateway(), +final pokemonCollectionGateway = GatewayProvider( + PokemonCollectionGateway.new, + useCases: [homeUseCaseProvider], ); -final randomCatUseCaseProvider = - UseCaseProvider( - (_) => RandomCatUseCase(), +final pokemonProfileGateway = GatewayProvider( + PokemonProfileGateway.new, + useCases: [profileUseCaseProvider], ); -final randomCatGatewayProvider = GatewayProvider( - (_) => RandomCatGateway(), +final pokemonSpeciesGateway = GatewayProvider( + PokemonSpeciesGateway.new, + useCases: [profileUseCaseProvider], ); -final firebaseExternalInterface = ExternalInterfaceProvider( - (_) => FirebaseExternalInterface( - firebaseClient: FirebaseClientFake({'date': '2021-10-07'}), - gatewayConnections: [ - () => lastLoginGatewayProvider.getGateway(providersContext), - ], - ), +final pokemonExternalInterfaceProvider = ExternalInterfaceProvider( + PokemonExternalInterface.new, + gateways: [ + pokemonCollectionGateway, + pokemonProfileGateway, + pokemonSpeciesGateway, + ], ); - -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/packages/clean_framework/example/lib/routes.dart b/packages/clean_framework/example/lib/routes.dart deleted file mode 100644 index b53b4863..00000000 --- a/packages/clean_framework/example/lib/routes.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:clean_framework/clean_framework.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:flutter/material.dart'; - -enum Routes { - home, - lastLogin, - countries, - countryDetail, - randomCat, -} - -final router = AppRouter( - routes: [ - AppRoute( - name: Routes.home, - path: '/', - builder: (context, state) => HomePage(), - routes: [ - AppRoute( - name: Routes.lastLogin, - path: 'last-login', - builder: (context, state) => LastLoginUI(), - ), - AppRoute( - name: Routes.countries, - path: 'countries', - builder: (context, state) => CountryUI(), - routes: [ - AppRoute( - name: Routes.countryDetail, - path: ':country', - builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: Text(state.getParam('country')), - ), - body: Center( - child: Text(state.queryParams['capital'].toString()), - ), - ); - }, - ), - ], - ), - AppRoute( - name: Routes.randomCat, - path: 'random-cat', - builder: (context, state) => RandomCatUI(), - ), - ], - ), - ], - errorBuilder: (context, state) => Page404(error: state.error), -); - -class Page404 extends StatelessWidget { - const Page404({Key? key, required this.error}) : super(key: key); - - final Exception? error; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Text(error.toString()), - ), - ); - } -} diff --git a/packages/clean_framework/example/lib/routing/routes.dart b/packages/clean_framework/example/lib/routing/routes.dart new file mode 100644 index 00000000..32014fab --- /dev/null +++ b/packages/clean_framework/example/lib/routing/routes.dart @@ -0,0 +1,46 @@ +import 'package:animations/animations.dart'; +import 'package:clean_framework_example/features/home/presentation/home_ui.dart'; +import 'package:clean_framework_example/features/profile/presentation/profile_ui.dart'; +import 'package:clean_framework_router/clean_framework_router.dart'; + +enum Routes with RoutesMixin { + home('/'), + profile(':pokemon_name'); + + const Routes(this.path); + + @override + final String path; +} + +class PokeRouter extends AppRouter { + @override + RouterConfiguration configureRouter() { + return RouterConfiguration( + routes: [ + AppRoute( + route: Routes.home, + builder: (_, __) => HomeUI(), + routes: [ + AppRoute.custom( + route: Routes.profile, + builder: (_, state) { + return ProfileUI( + pokemonName: state.params['pokemon_name'] ?? '', + ); + }, + transitionsBuilder: (_, animation, secondaryAnimation, child) { + return SharedAxisTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + ), + ], + ), + ], + ); + } +} diff --git a/packages/clean_framework/example/lib/widgets/pokemon_card.dart b/packages/clean_framework/example/lib/widgets/pokemon_card.dart new file mode 100644 index 00000000..c38fdd5d --- /dev/null +++ b/packages/clean_framework/example/lib/widgets/pokemon_card.dart @@ -0,0 +1,60 @@ +import 'package:clean_framework_example/widgets/svg_palette_card.dart'; +import 'package:flutter/material.dart'; + +class PokemonCard extends StatelessWidget { + const PokemonCard({ + super.key, + required this.imageUrl, + required this.name, + required this.onTap, + required this.heroTag, + }); + + final String imageUrl; + final String name; + final VoidCallback onTap; + final String heroTag; + + @override + Widget build(BuildContext context) { + return SvgPaletteCard( + cacheKey: name, + url: imageUrl, + onTap: onTap, + height: 160, + margin: EdgeInsets.symmetric(vertical: 8), + builder: (context, picture) { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + name, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.w300), + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / 2, + ), + child: Hero(tag: heroTag, child: picture), + ), + ], + ), + ); + }, + backgroundColorBuilder: (context, palette) { + final color = Theme.of(context).brightness == Brightness.light + ? palette.lightVibrantColor?.color + : palette.darkVibrantColor?.color; + + return color?.withAlpha(120); + }, + ); + } +} diff --git a/packages/clean_framework/example/lib/widgets/pokemon_search_field.dart b/packages/clean_framework/example/lib/widgets/pokemon_search_field.dart new file mode 100644 index 00000000..906b234f --- /dev/null +++ b/packages/clean_framework/example/lib/widgets/pokemon_search_field.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class PokemonSearchField extends StatelessWidget with PreferredSizeWidget { + const PokemonSearchField({super.key, required this.onChanged}); + + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Search for a Pokémon by name', + style: textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w100, + ), + ), + const SizedBox(height: 8), + TextField( + onChanged: onChanged, + decoration: InputDecoration( + hintText: 'Pokémon name', + hintStyle: textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.w100, + ), + prefixIcon: Icon(Icons.search), + border: InputBorder.none, + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceVariant, + ), + ) + ], + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(80); +} diff --git a/packages/clean_framework/example/lib/widgets/spotlight.dart b/packages/clean_framework/example/lib/widgets/spotlight.dart new file mode 100644 index 00000000..a1c63557 --- /dev/null +++ b/packages/clean_framework/example/lib/widgets/spotlight.dart @@ -0,0 +1,143 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:palette_generator/palette_generator.dart'; + +class Spotlight extends StatefulWidget { + const Spotlight({ + super.key, + required this.cacheKey, + required this.heroTag, + required this.builder, + this.placeholderBuilder, + this.width, + this.height, + }); + + final String cacheKey; + final String heroTag; + final WidgetBuilder builder; + final WidgetBuilder? placeholderBuilder; + final double? width; + final double? height; + + @override + State createState() => _SpotlightState(); +} + +class _SpotlightState extends State { + PaletteGenerator? _palette; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return FutureBuilder( + future: _loadFileFromCache(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + Positioned( + top: 0, + height: size.height, + width: size.width, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(48), + ), + gradient: SweepGradient( + center: FractionalOffset(0.9, 0.5), + colors: [ + _getColor((p) => p.dominantColor), + _getColor((p) => p.vibrantColor), + _getColor((p) => p.mutedColor), + _getColor((p) => p.lightMutedColor), + _getColor((p) => p.dominantColor), + ], + stops: [0.0, 0.2, 0.5, 0.7, 1.0], + transform: GradientRotation(pi * 1.5), + ), + ), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: RadialGradient( + center: FractionalOffset(0.5, 0.3), + colors: [ + for (var a = 0; a < 200; a++) + Theme.of(context) + .colorScheme + .background + .withAlpha(a), + ], + stops: [ + for (var stop = 0.0; stop < 1.0; stop += 1 / 200) stop + ], + radius: pi / 4, + ), + ), + child: const SizedBox(), + ), + ), + ), + Positioned.fill( + top: size.width / 1.5, + child: widget.builder(context), + ), + Positioned( + top: 0, + height: size.width * 1.2, + width: size.width, + child: Padding( + padding: const EdgeInsets.all(32), + child: Center( + child: Hero( + tag: widget.heroTag, + child: SvgPicture.string( + snapshot.data!, + placeholderBuilder: widget.placeholderBuilder, + height: widget.height, + width: widget.width, + ), + ), + ), + ), + ), + ], + ); + } + + return SizedBox(height: widget.height, width: widget.width); + }, + ); + } + + Future _loadFileFromCache() async { + final file = await DefaultCacheManager().getSingleFile( + '', + key: widget.cacheKey, + ); + final rawSvg = await file.readAsString(); + + if (rawSvg.isNotEmpty) { + final drawable = await svg.fromSvgString(rawSvg, widget.cacheKey); + final picture = drawable.toPicture(); + final image = await picture.toImage(100, 100); + _palette = await PaletteGenerator.fromImage(image); + } + + return rawSvg; + } + + Color _getColor(PaletteColor? Function(PaletteGenerator) generator) { + const fallbackColor = Colors.white; + + if (_palette == null) return fallbackColor; + return generator(_palette!)?.color ?? fallbackColor; + } +} diff --git a/packages/clean_framework/example/lib/widgets/svg_palette_card.dart b/packages/clean_framework/example/lib/widgets/svg_palette_card.dart new file mode 100644 index 00000000..3635b8b5 --- /dev/null +++ b/packages/clean_framework/example/lib/widgets/svg_palette_card.dart @@ -0,0 +1,122 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +class SvgPaletteCard extends StatefulWidget { + const SvgPaletteCard({ + super.key, + required this.url, + required this.builder, + this.duration = const Duration(milliseconds: 500), + this.cacheKey, + this.onTap, + this.placeholderBuilder, + this.backgroundColorBuilder, + this.width, + this.height, + this.margin = EdgeInsets.zero, + }); + + final String url; + final Widget Function(BuildContext, SvgPicture) builder; + final Duration duration; + final String? cacheKey; + final VoidCallback? onTap; + final WidgetBuilder? placeholderBuilder; + final Color? Function(BuildContext, PaletteGenerator)? backgroundColorBuilder; + final double? width; + final double? height; + final EdgeInsetsGeometry margin; + + @override + State createState() => _SvgPaletteCardState(); +} + +class _SvgPaletteCardState extends State { + Color? _color; + String _rawSvg = ''; + + @override + void initState() { + super.initState(); + _fetchSvg(); + } + + @override + void didUpdateWidget(SvgPaletteCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.url != widget.url) { + _fetchSvg(); + } + _generateColor(); + } + + @override + Widget build(BuildContext context) { + final borderRadius = BorderRadius.circular(12); + + return Card( + margin: widget.margin, + shape: RoundedRectangleBorder(borderRadius: borderRadius), + color: _color, + child: InkWell( + onTap: widget.onTap, + borderRadius: borderRadius, + child: AnimatedSwitcher( + duration: widget.duration, + child: _rawSvg.isEmpty + ? _buildPlaceHolder(context) + : widget.builder( + context, + SvgPicture.string( + _rawSvg, + placeholderBuilder: widget.placeholderBuilder, + height: widget.height, + width: widget.width, + ), + ), + ), + ), + ); + } + + Future _fetchSvg() async { + try { + final file = await DefaultCacheManager().getSingleFile( + widget.url, + key: widget.cacheKey, + ); + _rawSvg = await file.readAsString(); + + if (mounted) setState(() {}); + + _generateColor(); + } catch (e) { + log(e.toString(), name: 'SvgPaletteCard'); + } + } + + Future _generateColor() async { + if (_rawSvg.isNotEmpty) { + final drawable = await svg.fromSvgString(_rawSvg, widget.url); + final picture = drawable.toPicture(); + final image = await picture.toImage(100, 100); + final palette = await PaletteGenerator.fromImage(image); + + if (mounted) { + _color = widget.backgroundColorBuilder?.call(context, palette) ?? + palette.dominantColor?.color; + + setState(() {}); + } + } + } + + Widget _buildPlaceHolder(BuildContext context) { + return widget.placeholderBuilder?.call(context) ?? + SizedBox(height: widget.height, width: widget.width); + } +} diff --git a/packages/clean_framework/example/linux/.gitignore b/packages/clean_framework/example/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/packages/clean_framework/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/clean_framework/example/linux/CMakeLists.txt b/packages/clean_framework/example/linux/CMakeLists.txt new file mode 100644 index 00000000..fa46a1bb --- /dev/null +++ b/packages/clean_framework/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "clean_framework_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.acmesoftware.clean_framework_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/clean_framework/example/linux/flutter/CMakeLists.txt b/packages/clean_framework/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/packages/clean_framework/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/clean_framework/example/linux/flutter/generated_plugin_registrant.cc b/packages/clean_framework/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/packages/clean_framework/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/packages/clean_framework/example/linux/flutter/generated_plugin_registrant.h b/packages/clean_framework/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/clean_framework/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/clean_framework/example/linux/flutter/generated_plugins.cmake b/packages/clean_framework/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..2e1de87a --- /dev/null +++ b/packages/clean_framework/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/clean_framework/example/linux/main.cc b/packages/clean_framework/example/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/packages/clean_framework/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/clean_framework/example/linux/my_application.cc b/packages/clean_framework/example/linux/my_application.cc new file mode 100644 index 00000000..f844ae6e --- /dev/null +++ b/packages/clean_framework/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "clean_framework_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "clean_framework_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/clean_framework/example/linux/my_application.h b/packages/clean_framework/example/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/packages/clean_framework/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/clean_framework/example/macos/.gitignore b/packages/clean_framework/example/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/packages/clean_framework/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/clean_framework/example/macos/Podfile b/packages/clean_framework/example/macos/Podfile new file mode 100644 index 00000000..dade8dfa --- /dev/null +++ b/packages/clean_framework/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# 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/packages/clean_framework/example/macos/Runner.xcodeproj/project.pbxproj b/packages/clean_framework/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c243bd0e --- /dev/null +++ b/packages/clean_framework/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*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 */; }; +/* 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 */ + 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 /* clean_framework_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "clean_framework_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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* clean_framework_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 = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* clean_framework_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 */ + 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"; + }; +/* 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 = 10.11; + 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", + ); + 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 = 10.11; + 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 = 10.11; + 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", + ); + 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", + ); + 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/packages/clean_framework/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/clean_framework/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/clean_framework/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/clean_framework/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3ba234c --- /dev/null +++ b/packages/clean_framework/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/clean_framework/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/clean_framework/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/clean_framework/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/clean_framework/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/clean_framework/example/macos/Runner/AppDelegate.swift b/packages/clean_framework/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/packages/clean_framework/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/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/packages/clean_framework/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/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/packages/clean_framework/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/clean_framework/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/clean_framework/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/clean_framework/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/clean_framework/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..e2da662c --- /dev/null +++ b/packages/clean_framework/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 = clean_framework_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.acmesoftware.cleanFrameworkExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.acmesoftware. All rights reserved. diff --git a/packages/clean_framework/example/macos/Runner/Configs/Debug.xcconfig b/packages/clean_framework/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/clean_framework/example/macos/Runner/Configs/Release.xcconfig b/packages/clean_framework/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/clean_framework/example/macos/Runner/Configs/Warnings.xcconfig b/packages/clean_framework/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/packages/clean_framework/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/packages/clean_framework/example/macos/Runner/DebugProfile.entitlements b/packages/clean_framework/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/packages/clean_framework/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/packages/clean_framework/example/macos/Runner/Info.plist b/packages/clean_framework/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/packages/clean_framework/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/packages/clean_framework/example/macos/Runner/MainFlutterWindow.swift b/packages/clean_framework/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/packages/clean_framework/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/packages/clean_framework/example/macos/Runner/Release.entitlements b/packages/clean_framework/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/packages/clean_framework/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/clean_framework/example/pubspec.yaml b/packages/clean_framework/example/pubspec.yaml index 6ffeaec4..d80f53d2 100644 --- a/packages/clean_framework/example/pubspec.yaml +++ b/packages/clean_framework/example/pubspec.yaml @@ -1,29 +1,28 @@ name: clean_framework_example -description: Sandbox with example of all components from clean framework -version: 1.5.0 +description: An example application to demonstrate usage of clean_framework package. publish_to: none +version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' - flutter: '>=3.0.0' + sdk: '>=2.18.6 <3.0.0' dependencies: flutter: sdk: flutter - clean_framework: ^1.5.0-dev.0 - intl: ^0.17.0 + animations: ^2.0.7 + clean_framework: ^1.5.0 + clean_framework_router: ^0.1.0 + dio: ^4.0.6 + flutter_cache_manager: ^3.3.0 + flutter_svg: ^1.1.6 + palette_generator: ^0.3.3+2 dev_dependencies: - clean_framework_test: ^0.1.0-dev.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 + - assets/ \ No newline at end of file diff --git a/packages/clean_framework/example/web/icons/Icon-maskable-192.png b/packages/clean_framework/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/packages/clean_framework/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/clean_framework/example/web/icons/Icon-maskable-512.png b/packages/clean_framework/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/packages/clean_framework/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/clean_framework/example/web/index.html b/packages/clean_framework/example/web/index.html index 1460b5e9..0ddc5155 100644 --- a/packages/clean_framework/example/web/index.html +++ b/packages/clean_framework/example/web/index.html @@ -8,10 +8,13 @@ The path provided below has to start and end with a slash "/" in order for it to work correctly. - Fore more details: + For more details: * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base + + This is a placeholder for base href that will be replaced by the value of + the `--base-href` argument provided to `flutter build`. --> - + @@ -20,26 +23,36 @@ - + - example + clean_framework_example + + + + - - diff --git a/packages/clean_framework/example/web/manifest.json b/packages/clean_framework/example/web/manifest.json index 8c012917..9128dc36 100644 --- a/packages/clean_framework/example/web/manifest.json +++ b/packages/clean_framework/example/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "example", - "short_name": "example", + "name": "clean_framework_example", + "short_name": "clean_framework_example", "start_url": ".", "display": "standalone", "background_color": "#0175C2", @@ -18,6 +18,18 @@ "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/packages/clean_framework/example/windows/.gitignore b/packages/clean_framework/example/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/packages/clean_framework/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/clean_framework/example/windows/CMakeLists.txt b/packages/clean_framework/example/windows/CMakeLists.txt new file mode 100644 index 00000000..0b246d98 --- /dev/null +++ b/packages/clean_framework/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(clean_framework_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "clean_framework_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/clean_framework/example/windows/flutter/CMakeLists.txt b/packages/clean_framework/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/packages/clean_framework/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/clean_framework/example/windows/flutter/generated_plugin_registrant.cc b/packages/clean_framework/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..8b6d4680 --- /dev/null +++ b/packages/clean_framework/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/packages/clean_framework/example/windows/flutter/generated_plugin_registrant.h b/packages/clean_framework/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/packages/clean_framework/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/clean_framework/example/windows/flutter/generated_plugins.cmake b/packages/clean_framework/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..b93c4c30 --- /dev/null +++ b/packages/clean_framework/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/clean_framework/example/windows/runner/CMakeLists.txt b/packages/clean_framework/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..17411a8a --- /dev/null +++ b/packages/clean_framework/example/windows/runner/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/clean_framework/example/windows/runner/Runner.rc b/packages/clean_framework/example/windows/runner/Runner.rc new file mode 100644 index 00000000..3096bbbf --- /dev/null +++ b/packages/clean_framework/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.acmesoftware" "\0" + VALUE "FileDescription", "clean_framework_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "clean_framework_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.acmesoftware. All rights reserved." "\0" + VALUE "OriginalFilename", "clean_framework_example.exe" "\0" + VALUE "ProductName", "clean_framework_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/clean_framework/example/windows/runner/flutter_window.cpp b/packages/clean_framework/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/packages/clean_framework/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/clean_framework/example/windows/runner/flutter_window.h b/packages/clean_framework/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/packages/clean_framework/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/clean_framework/example/windows/runner/main.cpp b/packages/clean_framework/example/windows/runner/main.cpp new file mode 100644 index 00000000..d8ac73fa --- /dev/null +++ b/packages/clean_framework/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"clean_framework_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/clean_framework/example/windows/runner/resource.h b/packages/clean_framework/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/packages/clean_framework/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/clean_framework/example/windows/runner/resources/app_icon.ico b/packages/clean_framework/example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/packages/clean_framework/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/clean_framework/example/windows/runner/runner.exe.manifest b/packages/clean_framework/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/packages/clean_framework/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/clean_framework/example/windows/runner/utils.cpp b/packages/clean_framework/example/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/packages/clean_framework/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/clean_framework/example/windows/runner/utils.h b/packages/clean_framework/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/packages/clean_framework/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/clean_framework/example/windows/runner/win32_window.cpp b/packages/clean_framework/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/packages/clean_framework/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/clean_framework/example/windows/runner/win32_window.h b/packages/clean_framework/example/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/packages/clean_framework/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/clean_framework/lib/clean_framework.dart b/packages/clean_framework/lib/clean_framework.dart index 853d6f68..6b736a0d 100644 --- a/packages/clean_framework/lib/clean_framework.dart +++ b/packages/clean_framework/lib/clean_framework.dart @@ -2,15 +2,13 @@ library clean_framework; export 'package:clean_framework/src/app_providers_container.dart'; -export 'package:clean_framework/src/feature_state/feature.dart'; -export 'package:clean_framework/src/feature_state/feature_mapper.dart'; -export 'package:clean_framework/src/feature_state/feature_state_provider.dart'; -export 'package:clean_framework/src/feature_state/feature_widget.dart'; +export 'package:clean_framework/src/core/core.dart'; export 'package:clean_framework/src/logger.dart'; export 'package:clean_framework/src/open_feature/open_feature.dart'; +export 'package:clean_framework/src/presentation/presentation.dart'; export 'package:clean_framework/src/utilities/clean_framework_observer.dart'; export 'package:clean_framework/src/utilities/deserializer.dart'; +export 'package:clean_framework/src/utilities/either.dart'; export 'package:clean_framework/src/utilities/network_logger.dart'; export 'package:clean_framework/src/widgets/widgets.dart'; -export 'package:clean_framework_router/clean_framework_router.dart'; -export 'package:either_dart/either.dart'; +export 'package:flutter_riverpod/flutter_riverpod.dart' show ProviderContainer; diff --git a/packages/clean_framework/lib/clean_framework_defaults.dart b/packages/clean_framework/lib/clean_framework_defaults.dart index 40c9bfcd..c771c490 100644 --- a/packages/clean_framework/lib/clean_framework_defaults.dart +++ b/packages/clean_framework/lib/clean_framework_defaults.dart @@ -2,8 +2,3 @@ library clean_framework_defaults; export 'package:clean_framework/src/defaults/feature_provider/json_feature_provider.dart'; -export 'package:clean_framework/src/defaults/feature_state/feature_state.dart'; -export 'package:clean_framework_firestore/clean_framework_firestore.dart'; -export 'package:clean_framework_graphql/clean_framework_graphql.dart'; -export 'package:clean_framework_rest/clean_framework_rest.dart'; -export 'package:clean_framework_router/clean_framework_router.dart'; diff --git a/packages/clean_framework/lib/clean_framework_legacy.dart b/packages/clean_framework/lib/clean_framework_legacy.dart new file mode 100644 index 00000000..f8cb9a03 --- /dev/null +++ b/packages/clean_framework/lib/clean_framework_legacy.dart @@ -0,0 +1,26 @@ +/// Clean Framework v1 (Legacy) +library clean_framework_legacy; + +export 'package:clean_framework/src/app_providers_container.dart'; +export 'package:clean_framework/src/core/external_interface/request.dart'; +export 'package:clean_framework/src/core/external_interface/response.dart'; +export 'package:clean_framework/src/core/use_case/entity.dart'; +export 'package:clean_framework/src/core/use_case/use_case.dart'; +export 'package:clean_framework/src/logger.dart'; +export 'package:clean_framework/src/open_feature/open_feature.dart'; +export 'package:clean_framework/src/presentation/presenter/presenter.dart' + show PresenterBuilder; +export 'package:clean_framework/src/presentation/presenter/view_model.dart'; +export 'package:clean_framework/src/providers/bridge_gateway_provider.dart'; +export 'package:clean_framework/src/providers/external_interface.dart'; +export 'package:clean_framework/src/providers/external_interface_provider.dart'; +export 'package:clean_framework/src/providers/gateway.dart'; +export 'package:clean_framework/src/providers/gateway_provider.dart'; +export 'package:clean_framework/src/providers/presenter.dart'; +export 'package:clean_framework/src/providers/ui.dart'; +export 'package:clean_framework/src/providers/use_case_provider.dart'; +export 'package:clean_framework/src/utilities/clean_framework_observer.dart'; +export 'package:clean_framework/src/utilities/deserializer.dart'; +export 'package:clean_framework/src/utilities/either.dart'; +export 'package:clean_framework/src/utilities/network_logger.dart'; +export 'package:clean_framework/src/widgets/widgets.dart'; diff --git a/packages/clean_framework/lib/clean_framework_providers.dart b/packages/clean_framework/lib/clean_framework_providers.dart deleted file mode 100644 index 27f4e24a..00000000 --- a/packages/clean_framework/lib/clean_framework_providers.dart +++ /dev/null @@ -1,17 +0,0 @@ -/// Clean Framework Providers -library clean_framework_providers; - -export 'package:clean_framework/src/providers/bridge_gateway_provider.dart'; -export 'package:clean_framework/src/providers/entity.dart'; -export 'package:clean_framework/src/providers/external_interface.dart'; -export 'package:clean_framework/src/providers/external_interface_provider.dart'; -export 'package:clean_framework/src/providers/gateway.dart'; -export 'package:clean_framework/src/providers/gateway_provider.dart'; -export 'package:clean_framework/src/providers/presenter.dart'; -export 'package:clean_framework/src/providers/ui.dart'; -export 'package:clean_framework/src/providers/use_case.dart'; -export 'package:clean_framework/src/providers/use_case.dart'; -export 'package:clean_framework/src/providers/use_case_provider.dart'; -export 'package:clean_framework/src/providers/use_case_provider.dart'; -export 'package:clean_framework/src/providers/view_model.dart'; -export 'package:clean_framework/src/providers/view_model.dart'; diff --git a/packages/clean_framework/lib/src/core/app_provider_scope.dart b/packages/clean_framework/lib/src/core/app_provider_scope.dart new file mode 100644 index 00000000..1a0161c7 --- /dev/null +++ b/packages/clean_framework/lib/src/core/app_provider_scope.dart @@ -0,0 +1,81 @@ +import 'package:clean_framework/clean_framework.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stack_trace/stack_trace.dart'; + +class AppProviderScope extends StatelessWidget { + const AppProviderScope({ + super.key, + required this.child, + this.externalInterfaceProviders = const [], + this.overrides = const [], + this.observers, + this.parent, + }); + + final Widget child; + + final ProviderContainer? parent; + + /// The listeners that subscribes to changes on providers + /// stored on this [ProviderScope]. + final List? observers; + + /// Information on how to override a provider/family. + final List overrides; + + final List externalInterfaceProviders; + + @override + Widget build(BuildContext context) { + return ProviderScope( + parent: parent, + observers: observers, + overrides: overrides, + child: Builder( + builder: (context) { + return _ProviderInitializer( + container: ProviderScope.containerOf(context), + externalInterfaceProviders: externalInterfaceProviders, + child: child, + ); + }, + ), + ); + } +} + +class _ProviderInitializer extends StatefulWidget { + const _ProviderInitializer({ + required this.container, + required this.child, + required this.externalInterfaceProviders, + }); + + final ProviderContainer container; + final Widget child; + final List externalInterfaceProviders; + + @override + State<_ProviderInitializer> createState() => _ProviderInitializerState(); +} + +class _ProviderInitializerState extends State<_ProviderInitializer> { + @override + void initState() { + super.initState(); + + FlutterError.demangleStackTrace = (StackTrace stack) { + if (stack is Trace) return stack.vmTrace; + if (stack is Chain) return stack.toTrace().vmTrace; + return stack; + }; + + for (final provider in widget.externalInterfaceProviders) { + provider.initializeFor(widget.container); + } + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/packages/clean_framework/lib/src/core/clean_framework_provider.dart b/packages/clean_framework/lib/src/core/clean_framework_provider.dart new file mode 100644 index 00000000..2829f8ab --- /dev/null +++ b/packages/clean_framework/lib/src/core/clean_framework_provider.dart @@ -0,0 +1,9 @@ +import 'package:riverpod/riverpod.dart'; + +abstract class CleanFrameworkProvider

> { + CleanFrameworkProvider({required P provider}) : _provider = provider; + + final P _provider; + + P call() => _provider; +} diff --git a/packages/clean_framework/lib/src/core/core.dart b/packages/clean_framework/lib/src/core/core.dart new file mode 100644 index 00000000..1f558294 --- /dev/null +++ b/packages/clean_framework/lib/src/core/core.dart @@ -0,0 +1,10 @@ +export 'app_provider_scope.dart'; +export 'external_interface/external_interface.dart'; +export 'external_interface/external_interface_provider.dart'; +export 'external_interface/request.dart'; +export 'external_interface/response.dart'; +export 'gateway/gateway.dart'; +export 'gateway/gateway_provider.dart'; +export 'use_case/entity.dart'; +export 'use_case/use_case.dart'; +export 'use_case/use_case_provider.dart'; diff --git a/packages/clean_framework/lib/src/core/external_interface/external_interface.dart b/packages/clean_framework/lib/src/core/external_interface/external_interface.dart new file mode 100644 index 00000000..7ccda033 --- /dev/null +++ b/packages/clean_framework/lib/src/core/external_interface/external_interface.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:clean_framework/src/core/external_interface/request.dart'; +import 'package:clean_framework/src/core/external_interface/response.dart'; +import 'package:clean_framework/src/core/gateway/gateway.dart'; +import 'package:clean_framework/src/core/gateway/gateway_provider.dart'; +import 'package:clean_framework/src/utilities/either.dart'; +import 'package:meta/meta.dart'; +import 'package:riverpod/riverpod.dart'; + +abstract class ExternalInterface { + ExternalInterface() { + handleRequest(); + } + + @internal + void attach( + ProviderRef ref, { + required List providers, + }) { + for (final gatewayProvider in providers) { + _initTransporter(ref.read(gatewayProvider())); + } + } + + void _initTransporter(Gateway gateway) { + // ignore: invalid_use_of_visible_for_testing_member + gateway.feedResponse( + (request) async { + final req = request as R; + + if (gateway is WatcherGateway) { + final requestCompleter = _StreamRequestCompleter( + req, + gateway.yieldResponse, + ); + + _requestController.add(requestCompleter); + return requestCompleter.future; + } else { + return this.request(req); + } + }, + ); + } + + final _RequestController _requestController = + StreamController.broadcast(); + + @visibleForTesting + Future> request(R request) { + final requestCompleter = RequestCompleter(request); + + _requestController.add(requestCompleter); + return requestCompleter.future; + } + + void handleRequest(); + + FailureResponse onError(Object error); + + void on( + RequestHandler handler, + ) { + _requestController.stream.where((e) => e.request is E).listen( + (e) async { + final request = e.request as E; + + try { + if (e is _StreamRequestCompleter) { + final event = e as _StreamRequestCompleter; + final handlerCall = handler( + request, + (response) { + if (!event.isCompleted) event.complete(response); + event.emitSuccess(response); + }, + ); + + if (handlerCall is Future) { + unawaited( + handlerCall.catchError( + (Object error) => e.completeFailure(_onError(error, request)), + ), + ); + } + } else { + await handler(request, e.complete); + } + } catch (error) { + e.completeFailure(_onError(error, request)); + } + }, + ); + } + + Never sendError(Object error) => throw error; + + FailureResponse _onError(Object error, R request) { + //CleanFrameworkObserver.instance.onExternalError(this, request, error); + final failure = onError(error); + //CleanFrameworkObserver.instance.onFailureResponse(this, request, failure); + return failure; + } +} + +typedef _RequestController + = StreamController>; + +typedef ResponseSender = void Function(S response); + +typedef RequestHandler + = FutureOr Function(E request, ResponseSender send); + +class RequestCompleter { + RequestCompleter(this.request) : _completer = Completer(); + + final R request; + final Completer> _completer; + + Future> get future => _completer.future; + + bool get isCompleted => _completer.isCompleted; + + void complete(S success) => _completer.complete(Either.right(success)); + + void completeFailure(FailureResponse failure) { + _completer.complete(Either.left(failure)); + } +} + +class _StreamRequestCompleter + extends RequestCompleter { + _StreamRequestCompleter(super.request, this.emitSuccess); + + final void Function(S) emitSuccess; +} diff --git a/packages/clean_framework/lib/src/core/external_interface/external_interface_provider.dart b/packages/clean_framework/lib/src/core/external_interface/external_interface_provider.dart new file mode 100644 index 00000000..070e9aba --- /dev/null +++ b/packages/clean_framework/lib/src/core/external_interface/external_interface_provider.dart @@ -0,0 +1,22 @@ +import 'package:clean_framework/src/core/clean_framework_provider.dart'; +import 'package:clean_framework/src/core/external_interface/external_interface.dart'; +import 'package:clean_framework/src/core/gateway/gateway_provider.dart'; +import 'package:meta/meta.dart'; +import 'package:riverpod/riverpod.dart'; + +class ExternalInterfaceProvider + extends CleanFrameworkProvider> { + ExternalInterfaceProvider( + E Function() create, { + List gateways = const [], + }) : super( + provider: Provider( + (ref) => create()..attach(ref, providers: gateways), + ), + ); + + @visibleForTesting + E read(ProviderContainer container) => container.read(call()); + + void initializeFor(ProviderContainer container) => read(container); +} diff --git a/packages/clean_framework/lib/src/core/external_interface/request.dart b/packages/clean_framework/lib/src/core/external_interface/request.dart new file mode 100644 index 00000000..21f7103b --- /dev/null +++ b/packages/clean_framework/lib/src/core/external_interface/request.dart @@ -0,0 +1,6 @@ +import 'package:meta/meta.dart'; + +@immutable +abstract class Request { + const Request(); +} diff --git a/packages/clean_framework/lib/src/core/external_interface/response.dart b/packages/clean_framework/lib/src/core/external_interface/response.dart new file mode 100644 index 00000000..c6c45657 --- /dev/null +++ b/packages/clean_framework/lib/src/core/external_interface/response.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class Response extends Equatable { + const Response(); + + @override + bool get stringify => true; +} + +class SuccessResponse extends Response { + const SuccessResponse(); + + @override + List get props => []; +} + +abstract class FailureResponse extends Response { + const FailureResponse({this.message = ''}); + + final String message; + + @override + List get props => [message]; +} + +class TypedFailureResponse extends FailureResponse { + const TypedFailureResponse({ + required this.type, + this.errorData = const {}, + super.message, + }); + + final T type; + final Map errorData; + + @override + List get props => [...super.props, type, errorData]; +} + +class UnknownFailureResponse extends FailureResponse { + UnknownFailureResponse([Object? error]) : super(message: error.toString()); +} diff --git a/packages/clean_framework/lib/src/core/gateway/gateway.dart b/packages/clean_framework/lib/src/core/gateway/gateway.dart new file mode 100644 index 00000000..61024aba --- /dev/null +++ b/packages/clean_framework/lib/src/core/gateway/gateway.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:clean_framework/src/core/external_interface/request.dart'; +import 'package:clean_framework/src/core/external_interface/response.dart'; +import 'package:clean_framework/src/core/use_case/use_case.dart'; +import 'package:clean_framework/src/core/use_case/use_case_provider.dart'; +import 'package:clean_framework/src/utilities/clean_framework_observer.dart'; +import 'package:clean_framework/src/utilities/either.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meta/meta.dart'; + +abstract class Gateway { + void attach( + ProviderRef ref, { + required List providers, + }) { + _ref = ref; + _useCaseProviders = providers; + + for (final useCaseProvider in providers) { + useCaseProvider.notifier.listen( + (notifier) { + ref + .read(notifier) + .subscribe((output) => buildInput(output as O)); + }, + ); + } + } + + late final ProviderRef _ref; + late final List _useCaseProviders; + + @visibleForTesting + @nonVirtual + // ignore: use_setters_to_change_properties + void feedResponse(Responder feeder) => _responder = feeder; + + @visibleForTesting + @nonVirtual + Future> buildInput(O output) { + return _processRequest(buildRequest(output)); + } + + late final Responder _responder; + + S onSuccess(covariant P response); + FailureInput onFailure(covariant FailureResponse failureResponse); + R buildRequest(O output); + + Future> _processRequest(R request) async { + final either = await _responder(request); + return either.fold( + (failureResponse) => Either.left(_onFailure(failureResponse)), + (response) => Either.right(onSuccess(response)), + ); + } + + FailureInput _onFailure(FailureResponse failureResponse) { + final failureInput = onFailure(failureResponse); + CleanFrameworkObserver.instance.onFailureInput(failureInput); + return failureInput; + } +} + +abstract class WatcherGateway< + O extends Output, + R extends Request, + P extends SuccessResponse, + S extends SuccessInput> extends Gateway { + @override + FailureInput onFailure(FailureResponse failureResponse) { + return FailureInput(message: failureResponse.message); + } + + @nonVirtual + void yieldResponse(P response) { + for (final useCaseProvider in _useCaseProviders) { + useCaseProvider.notifier.listen( + (notifier) { + _ref.read(notifier).setInput(onSuccess(response)); + }, + ); + } + } +} + +typedef Responder + = FutureOr> Function(R request); diff --git a/packages/clean_framework/lib/src/core/gateway/gateway_provider.dart b/packages/clean_framework/lib/src/core/gateway/gateway_provider.dart new file mode 100644 index 00000000..da004cb0 --- /dev/null +++ b/packages/clean_framework/lib/src/core/gateway/gateway_provider.dart @@ -0,0 +1,20 @@ +import 'package:clean_framework/src/core/clean_framework_provider.dart'; +import 'package:clean_framework/src/core/gateway/gateway.dart'; +import 'package:clean_framework/src/core/use_case/use_case_provider.dart'; +import 'package:meta/meta.dart'; +import 'package:riverpod/riverpod.dart'; + +class GatewayProvider + extends CleanFrameworkProvider> { + GatewayProvider( + G Function() create, { + List useCases = const [], + }) : super( + provider: Provider( + (ref) => create()..attach(ref, providers: useCases), + ), + ); + + @visibleForTesting + G read(ProviderContainer container) => container.read(call()); +} diff --git a/packages/clean_framework/lib/src/providers/entity.dart b/packages/clean_framework/lib/src/core/use_case/entity.dart similarity index 78% rename from packages/clean_framework/lib/src/providers/entity.dart rename to packages/clean_framework/lib/src/core/use_case/entity.dart index b5166e32..a8f52f56 100644 --- a/packages/clean_framework/lib/src/providers/entity.dart +++ b/packages/clean_framework/lib/src/core/use_case/entity.dart @@ -3,6 +3,10 @@ import 'package:flutter/foundation.dart'; @immutable abstract class Entity extends Equatable { + const Entity(); + @override bool get stringify => true; + + external Entity copyWith(); } diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/input.dart b/packages/clean_framework/lib/src/core/use_case/helpers/input.dart new file mode 100644 index 00000000..c9708d9c --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/helpers/input.dart @@ -0,0 +1,22 @@ +import 'package:clean_framework/src/core/use_case/helpers/output.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class Input { + const Input(); +} + +class SuccessInput extends Input { + const SuccessInput(); +} + +class FailureInput extends Input { + const FailureInput({this.message = ''}); + + final String message; +} + +class NoSubscriptionFailureInput extends FailureInput { + const NoSubscriptionFailureInput() + : super(message: 'No subscription exists for this request of $O'); +} diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart b/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart new file mode 100644 index 00000000..9f48e580 --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/helpers/input_filter_map.dart @@ -0,0 +1,27 @@ +part of 'use_case_transformer.dart'; + +typedef InputProcessor = E Function(dynamic, E); + +typedef InputFilterMap = Map>; + +extension InputFilterMapExtension on InputFilterMap { + E call(E entity, I input) { + final processor = this[I]; + + if (processor == null) { + throw StateError( + '\n\nInput filter not defined for "$I".\n' + 'Filters available for: ${keys.isEmpty ? 'none' : keys.join(', ')}\n' + 'Dependency: $E\n\n', + ); + } + + return processor(input, entity); + } + + void addTransformers(List transformers) { + addEntries( + transformers.whereType>().map((f) => f._entry), + ); + } +} diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/output.dart b/packages/clean_framework/lib/src/core/use_case/helpers/output.dart new file mode 100644 index 00000000..c85f253c --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/helpers/output.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract class Output extends Equatable { + const Output(); + + @override + bool get stringify => true; +} diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart b/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart new file mode 100644 index 00000000..3fd3b7fc --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/helpers/output_filter_map.dart @@ -0,0 +1,29 @@ +part of 'use_case_transformer.dart'; + +typedef OutputBuilder = Output Function(E); + +typedef OutputFilterMap = Map>; + +extension OutputFilterMapExtension on OutputFilterMap { + O call(E entity) { + final builder = this[O]; + + if (builder == null) { + throw StateError( + '\n\nOutput filter not defined for "$O".\n' + 'Filters available for: ${keys.isEmpty ? 'none' : keys.join(', ')}\n' + 'Dependency: $E\n\n', + ); + } + + return builder(entity) as O; + } + + void addTransformers(List transformers) { + addEntries( + transformers + .whereType>() + .map((f) => f._entry), + ); + } +} diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart b/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart new file mode 100644 index 00000000..26762d43 --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/helpers/request_subscription_map.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:clean_framework/src/core/use_case/helpers/input.dart'; +import 'package:clean_framework/src/core/use_case/helpers/output.dart'; +import 'package:clean_framework/src/utilities/either.dart'; + +typedef RequestSubscriptionMap + = Map>; + +typedef Result = FutureOr>; + +typedef RequestSubscription = Result Function(dynamic); + +extension RequestSubscriptionMapExtension + on RequestSubscriptionMap { + void add(RequestSubscription subscription) { + this[O] = subscription; + } + + Result call( + O output, + ) async { + final subscription = this[O]; + + if (subscription == null) { + throw StateError( + '\n\nNo subscription for "$O" exists.\n\n' + 'Please follow the steps below in order to fix this issue:\n' + '1. Ensure that the use case that requests "$O" is attached ' + 'the appropriate gateway.\n' + ' AppropriateGatewayProvider(\n' + ' ...,\n' + ' useCases: [<>],\n' + ' )\n' + '2. Ensure that the gateway that belongs to "$O" is attached ' + 'the appropriate external interface.\n' + ' AppropriateExternalInterfaceProvider(\n' + ' ...,\n' + ' gateways: [<>],\n' + ' )\n' + '3. Ensure that the associated external interface is initialized.\n' + ' AppProviderScope(\n' + ' ...,\n' + ' externalInterfaceProviders: [\n' + ' <>,\n' + ' ],\n' + ' )\n', + ); + } + + final result = await subscription(output); + return result as Either; + } +} diff --git a/packages/clean_framework/lib/src/core/use_case/helpers/use_case_transformer.dart b/packages/clean_framework/lib/src/core/use_case/helpers/use_case_transformer.dart new file mode 100644 index 00000000..6fd09d87 --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/helpers/use_case_transformer.dart @@ -0,0 +1,61 @@ +import 'package:clean_framework/src/core/use_case/entity.dart'; +import 'package:clean_framework/src/core/use_case/helpers/input.dart'; +import 'package:clean_framework/src/core/use_case/helpers/output.dart'; +import 'package:meta/meta.dart'; + +part 'input_filter_map.dart'; +part 'output_filter_map.dart'; + +abstract class UseCaseTransformer {} + +abstract class OutputTransformer + implements UseCaseTransformer { + const OutputTransformer() : _transformer = null; + + factory OutputTransformer.from(O Function(E) transformer) = + _OutputFilter; + + const OutputTransformer._(this._transformer); + + final O Function(E)? _transformer; + + MapEntry> get _entry => MapEntry(O, transform); + + @protected + O transform(E entity); +} + +abstract class InputTransformer + implements UseCaseTransformer { + const InputTransformer() : _transformer = null; + + factory InputTransformer.from(E Function(E, I) transformer) = + _InputFilter; + + const InputTransformer._(this._transformer); + + final E Function(E, I)? _transformer; + + MapEntry> get _entry { + return MapEntry(I, (i, e) => transform(e, i as I)); + } + + @protected + E transform(E entity, I input); +} + +class _OutputFilter + extends OutputTransformer { + const _OutputFilter(super.transformer) : super._(); + + @override + O transform(E entity) => _transformer!(entity); +} + +class _InputFilter + extends InputTransformer { + const _InputFilter(super.transformer) : super._(); + + @override + E transform(E entity, I input) => _transformer!(entity, input); +} diff --git a/packages/clean_framework/lib/src/core/use_case/use_case.dart b/packages/clean_framework/lib/src/core/use_case/use_case.dart new file mode 100644 index 00000000..d8d2199a --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/use_case.dart @@ -0,0 +1,70 @@ +import 'dart:async'; + +import 'package:clean_framework/src/core/use_case/entity.dart'; +import 'package:clean_framework/src/core/use_case/use_case_debounce_mixin.dart'; +import 'package:clean_framework/src/core/use_case/use_case_helpers.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meta/meta.dart'; + +export 'package:clean_framework/src/core/use_case/use_case_helpers.dart'; + +typedef InputCallback = E Function(I); + +abstract class UseCase extends StateNotifier + with UseCaseDebounceMixin { + UseCase({ + required E entity, + List>? transformers, + @Deprecated('Use transformers instead') OutputFilterMap? outputFilters, + @Deprecated('Use transformers instead') InputFilterMap? inputFilters, + }) : _outputFilters = Map.of(outputFilters ?? const {}), + _inputFilters = Map.of(inputFilters ?? const {}), + super(entity) { + if (transformers != null && transformers.isNotEmpty) { + _outputFilters.addTransformers(transformers); + _inputFilters.addTransformers(transformers); + } + } + + final OutputFilterMap _outputFilters; + final InputFilterMap _inputFilters; + final RequestSubscriptionMap _requestSubscriptions = {}; + + @visibleForTesting + @protected + E get entity => super.state; + + @visibleForTesting + @protected + set entity(E newEntity) => super.state = newEntity; + + O getOutput() => _outputFilters(entity); + + void setInput(I input) { + entity = _inputFilters(entity, input); + } + + void subscribe( + RequestSubscription subscription, + ) { + _requestSubscriptions.add(subscription); + } + + @visibleForTesting + @protected + Future request( + O output, { + required InputCallback onSuccess, + required InputCallback onFailure, + }) async { + final input = await _requestSubscriptions(output); + + entity = input.fold(onFailure, onSuccess); + } + + @override + void dispose() { + clearDebounce(); + super.dispose(); + } +} diff --git a/packages/clean_framework/lib/src/core/use_case/use_case_debounce_mixin.dart b/packages/clean_framework/lib/src/core/use_case/use_case_debounce_mixin.dart new file mode 100644 index 00000000..2bb4e12c --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/use_case_debounce_mixin.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +mixin UseCaseDebounceMixin { + final Map _debounceTimers = {}; + + /// Executes the [action] so that it will only be executed + /// when there is no further repeated actions with same [tag] + /// in a given frame of [duration]. + /// + /// If [immediate] is false, then then first action will also be debounced. + @visibleForTesting + @protected + void debounce({ + required void Function() action, + required String tag, + Duration duration = const Duration(milliseconds: 300), + bool immediate = true, + }) { + final timer = _debounceTimers[tag]; + + final timerPending = timer?.isActive ?? false; + final canExecute = immediate && !timerPending; + + timer?.cancel(); + _debounceTimers[tag] = Timer( + duration, + () { + _debounceTimers.remove(tag); + if (!immediate) action(); + }, + ); + + if (canExecute) action(); + } + + @protected + void clearDebounce() { + for (final debounceTimer in _debounceTimers.values) { + debounceTimer.cancel(); + } + _debounceTimers.clear(); + } +} diff --git a/packages/clean_framework/lib/src/core/use_case/use_case_helpers.dart b/packages/clean_framework/lib/src/core/use_case/use_case_helpers.dart new file mode 100644 index 00000000..59875296 --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/use_case_helpers.dart @@ -0,0 +1,4 @@ +export 'helpers/input.dart'; +export 'helpers/output.dart'; +export 'helpers/request_subscription_map.dart'; +export 'helpers/use_case_transformer.dart'; diff --git a/packages/clean_framework/lib/src/core/use_case/use_case_provider.dart b/packages/clean_framework/lib/src/core/use_case/use_case_provider.dart new file mode 100644 index 00000000..2daa00db --- /dev/null +++ b/packages/clean_framework/lib/src/core/use_case/use_case_provider.dart @@ -0,0 +1,121 @@ +import 'dart:async'; + +import 'package:clean_framework/clean_framework.dart'; +import 'package:clean_framework/src/core/clean_framework_provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meta/meta.dart'; + +abstract class UseCaseProviderBase, + N extends ProviderBase> extends CleanFrameworkProvider { + UseCaseProviderBase({required super.provider}); + + final StreamController> _notifierController = + StreamController.broadcast(); + + Stream> get notifier => _notifierController.stream; + + @visibleForOverriding + Refreshable buildNotifier(); + + void init() { + _notifierController.add(buildNotifier()); + } + + O subscribe(WidgetRef ref) { + return ref.watch(_listenForOutputChange(ref)); + } + + U getUseCase(WidgetRef ref) => ref.read(buildNotifier()); + + void listen(WidgetRef ref, void Function(O?, O) listener) { + ref.listen(_listenForOutputChange(ref), listener); + } + + ProviderListenable _listenForOutputChange( + WidgetRef ref, + ) { + final useCase = getUseCase(ref); + return call().select((e) => useCase.getOutput()); + } + + @visibleForTesting + U read(ProviderContainer container) => container.read(buildNotifier()); +} + +class UseCaseProvider> + extends UseCaseProviderBase> { + UseCaseProvider( + U Function() create, [ + UseCaseProviderConnector? connector, + ]) : super( + provider: StateNotifierProvider( + (ref) { + final useCase = create(); + connector?.call( + UseCaseProviderBridge._(useCase, ref), + ); + return useCase; + }, + ), + ); + + static const autoDispose = AutoDisposeUseCaseProviderBuilder(); + + @override + Refreshable buildNotifier() => call().notifier; +} + +class AutoDisposeUseCaseProvider> + extends UseCaseProviderBase> { + AutoDisposeUseCaseProvider( + U Function() create, [ + UseCaseProviderConnector? connector, + ]) : super( + provider: StateNotifierProvider.autoDispose( + (ref) { + final useCase = create(); + connector?.call( + UseCaseProviderBridge._(useCase, ref), + ); + return useCase; + }, + ), + ); + + @override + Refreshable buildNotifier() => call().notifier; +} + +class AutoDisposeUseCaseProviderBuilder { + const AutoDisposeUseCaseProviderBuilder(); + + AutoDisposeUseCaseProvider call>( + U Function() create, [ + UseCaseProviderConnector? connector, + ]) { + return AutoDisposeUseCaseProvider(create, connector); + } +} + +typedef UseCaseProviderConnector> = void + Function(UseCaseProviderBridge bridge); + +class UseCaseProviderBridge> { + UseCaseProviderBridge._(this.useCase, Ref ref) : _ref = ref; + + final BU useCase; + final Ref _ref; + + void connect, T>( + UseCaseProvider provider, + void Function(T? previous, T next) connector, { + required T Function(E entity) selector, + bool fireImmediately = false, + }) { + _ref.listen( + provider().select(selector), + connector, + fireImmediately: fireImmediately, + ); + } +} diff --git a/packages/clean_framework/lib/src/defaults/feature_state/feature_state.dart b/packages/clean_framework/lib/src/defaults/feature_state/feature_state.dart deleted file mode 100644 index aae1e858..00000000 --- a/packages/clean_framework/lib/src/defaults/feature_state/feature_state.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// The feature state. -enum FeatureState { - /// Hidden - hidden, - - /// Visible - visible, -} diff --git a/packages/clean_framework/lib/src/feature_state/feature.dart b/packages/clean_framework/lib/src/feature_state/feature.dart deleted file mode 100644 index 614b6721..00000000 --- a/packages/clean_framework/lib/src/feature_state/feature.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; - -/// Feature instances are used to identify groups of components that are part -/// of the same flow, share data and are used to encapsule a common behavior. -/// It is recommended that instances of this class are constant, and keep the -/// instances globally shared to simplify its use. -@immutable -class Feature extends Equatable { - const Feature({required this.name, String? version}) - : version = version ?? '1.0'; - - final String name; - final String version; - - @override - List get props => [name, version]; -} diff --git a/packages/clean_framework/lib/src/feature_state/feature_mapper.dart b/packages/clean_framework/lib/src/feature_state/feature_mapper.dart deleted file mode 100644 index 3f1e1ee8..00000000 --- a/packages/clean_framework/lib/src/feature_state/feature_mapper.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:clean_framework/src/feature_state/feature.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -/// This class is a requirement to be able to use a FeatureStateProvider. It -/// specifies how a JSON that contains the list of features and their states -/// would be parsed. - -/// Since the developer has the freedom to choose any data type to represent -/// the states, this is the only class that maps the JSON values into those -/// data types. -abstract class FeatureMapper extends StateNotifier> { - FeatureMapper() : super({}); - - @override - void dispose() { - super.dispose(); - } - - Map _parseJson(Map json) { - final newStates = {}; - - if (json['features'] is! List) { - throw StateError('Feature States JSON parse error, not a list'); - } - - final features = json['features'] as List; - - for (final feature in features) { - final name = feature['name'].toString(); - final version = feature['version']?.toString(); - final state = feature['state'].toString(); - - if (name.isNotEmpty && state.isNotEmpty) { - newStates[Feature(name: name, version: version)] = parseState(state); - } - } - return newStates; - } - - /// This method creates the internal mapping of states, and the JSON - /// that serves as the input should have the following structure: - /// - /// ```json - /// { - /// "features": [ - /// {"name": "example", "state": "STATE_VALUE"} - /// ] - /// } - /// ``` - /// - void load(Map json) { - state = _parseJson(json); - } - - /// If the initial mapping already exists, this method combines a new JSON - /// map with the existing one. This is a normal map join, so adding entries - /// with the same name as existing ones will replace the value - void append(Map json) { - state = {} - ..addAll(state) - ..addAll(_parseJson(json)); - } - - /// This is the method called by other classes in the app, specially the - /// FeatureWidget, to obtain the current state of any feature in the map - S getStateFor(Feature feature) => state[feature] ?? defaultState; - - /// This override is required to map correctly between the string - /// values in the JSON and the state values of the chosen data type - S parseState(String rawStateName); - - /// This override is used to determine the default value among the possible - /// states according to the chosen data type. This value will be used - /// when the parsing process finds string states that don't match to any - /// possible states, and also while trying to retrieve the state of features - /// that don't exist in the map or whose name don't match - S get defaultState; -} diff --git a/packages/clean_framework/lib/src/feature_state/feature_state_provider.dart b/packages/clean_framework/lib/src/feature_state/feature_state_provider.dart deleted file mode 100644 index 31d9a1a8..00000000 --- a/packages/clean_framework/lib/src/feature_state/feature_state_provider.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:clean_framework/src/feature_state/feature.dart'; -import 'package:clean_framework/src/feature_state/feature_mapper.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -/// An instance of this class creates a [FeatureMapper] implementation -/// that uses a specific data type to represent the states of each -/// [Feature]. -/// -/// You can create a global instance to allow any FeatureWidget to -/// build a specific UI hierarchy based on the current state for -/// the [Feature] associated to it. -/// -/// This is a callable class, but the callable method is only useful -/// for the internal code of FeatureWidget and there is no practical -/// use for it. Developers should be calling [featuresMap] to retrieve -/// the mapper and obtain the states that way. -class FeatureStateProvider> { - FeatureStateProvider(this.create) - : _provider = StateNotifierProvider>(create); - - final StateNotifierProvider> _provider; - final F Function(Ref) create; - - // Direct call to retrieve the state, which is just the map, - // for example when using ref.watch - StateNotifierProvider> call() => _provider; - - // Used to have access to the Mapper class, for example to call load() - AlwaysAliveRefreshable get featuresMap => _provider.notifier; -} diff --git a/packages/clean_framework/lib/src/feature_state/feature_widget.dart b/packages/clean_framework/lib/src/feature_state/feature_widget.dart deleted file mode 100644 index 419e85f1..00000000 --- a/packages/clean_framework/lib/src/feature_state/feature_widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:clean_framework/src/feature_state/feature.dart'; -import 'package:clean_framework/src/feature_state/feature_mapper.dart'; -import 'package:clean_framework/src/feature_state/feature_state_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -/// For each feature entry point, a FeatureWidget instance is used to control -/// the visibility and behavior of the children. One FeatureWidget could have -/// many UI widgets, one per each state. -/// -/// The S on the generics declaration stands for the class that is going to use -/// to determine the possible value for a given state. The most common data -/// type used here is an enum. You are free to use anything that provides a -/// similar behavior. -/// -/// The objects of this class need an existing Feature object and a -/// FeatureProvider object. The provider is used internally to extract the -/// current state for the given Feature, and use it as part of the [builder] -/// method. -abstract class FeatureWidget extends ConsumerStatefulWidget { - const FeatureWidget({ - super.key, - required this.provider, - required this.feature, - }); - final FeatureStateProvider> provider; - final Feature feature; - - /// The override of this method should return the proper widget depending - /// on the currentState value. A common pattern is to have states that - /// instead of returning a visible widget, return an empty container. - /// - /// The developer can use hidden widget to return a simple empty container - /// with a key that can by checked during tests. - Widget builder(BuildContext context, S currentState); - - @override - ConsumerState createState() => - _FeatureWidgetState(); -} - -class _FeatureWidgetState extends ConsumerState> { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - ref.watch(widget.provider()); - } - - @override - Widget build(BuildContext context) { - final mapper = ref.read(widget.provider.featuresMap); - final currentState = mapper.getStateFor(widget.feature); - - // TODO(sarbagyastha): THIS SHOULDN'T BE NEEDED, - // FIGURE OUT WHY THE REBUILD DOESN'T HAPPEN - ref.listen(widget.provider(), (_, __) => setState(() {})); - - return widget.builder(context, currentState); - } -} diff --git a/packages/clean_framework/lib/src/presentation/presentation.dart b/packages/clean_framework/lib/src/presentation/presentation.dart new file mode 100644 index 00000000..eb64c2bc --- /dev/null +++ b/packages/clean_framework/lib/src/presentation/presentation.dart @@ -0,0 +1,3 @@ +export 'presenter/presenter.dart'; +export 'presenter/view_model.dart'; +export 'ui/ui.dart'; diff --git a/packages/clean_framework/lib/src/presentation/presenter/presenter.dart b/packages/clean_framework/lib/src/presentation/presenter/presenter.dart new file mode 100644 index 00000000..6a832ee2 --- /dev/null +++ b/packages/clean_framework/lib/src/presentation/presenter/presenter.dart @@ -0,0 +1,96 @@ +import 'package:clean_framework/src/core/core.dart'; +import 'package:clean_framework/src/presentation/presenter/view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +abstract class Presenter extends ConsumerStatefulWidget { + const Presenter({ + super.key, + required UseCaseProviderBase provider, + required this.builder, + }) : _provider = provider; + final UseCaseProviderBase _provider; + final PresenterBuilder builder; + + @override + ConsumerState> createState() => _PresenterState(); + + @protected + V createViewModel(U useCase, O output); + + /// Called when this presenter is inserted into the tree. + @protected + void onLayoutReady(BuildContext context, U useCase) {} + + /// Called whenever the [output] updates. + @protected + void onOutputUpdate(BuildContext context, O output) {} + + /// Called whenever the presenter configuration changes. + @protected + void didUpdatePresenter( + BuildContext context, + covariant Presenter old, + U useCase, + ) {} + + /// Called when this presenter is removed from the tree. + @protected + void onDestroy(U useCase) {} + + @visibleForTesting + O subscribe(WidgetRef ref) => _provider.subscribe(ref); +} + +class _PresenterState + extends ConsumerState> { + U? _useCase; + + @override + WidgetRef get ref => context as WidgetRef; + + @override + void initState() { + super.initState(); + widget._provider + ..notifier.first.then((_) { + widget.onLayoutReady(context, _useCase!); + }) + ..init(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _useCase ??= widget._provider.getUseCase(ref) as U; + } + + @override + void didUpdateWidget(covariant Presenter oldWidget) { + super.didUpdateWidget(oldWidget); + widget.didUpdatePresenter(context, oldWidget, _useCase!); + } + + @override + Widget build(BuildContext context) { + widget._provider.listen(ref, _onOutputChanged); + final output = widget.subscribe(ref); + return widget.builder(widget.createViewModel(_useCase!, output)); + } + + void _onOutputChanged(O? previous, O next) { + if (previous != next) widget.onOutputUpdate(context, next); + } + + @override + void dispose() { + SchedulerBinding.instance.addPostFrameCallback((_) { + widget.onDestroy(_useCase!); + }); + super.dispose(); + } +} + +typedef PresenterBuilder = Widget Function(V viewModel); diff --git a/packages/clean_framework/lib/src/providers/view_model.dart b/packages/clean_framework/lib/src/presentation/presenter/view_model.dart similarity index 100% rename from packages/clean_framework/lib/src/providers/view_model.dart rename to packages/clean_framework/lib/src/presentation/presenter/view_model.dart diff --git a/packages/clean_framework/lib/src/presentation/ui/ui.dart b/packages/clean_framework/lib/src/presentation/ui/ui.dart new file mode 100644 index 00000000..bfdf097b --- /dev/null +++ b/packages/clean_framework/lib/src/presentation/ui/ui.dart @@ -0,0 +1,38 @@ +import 'package:clean_framework/src/presentation/presentation.dart'; +import 'package:flutter/material.dart'; + +abstract class UI extends StatefulWidget { + UI({ + super.key, + PresenterCreator? create, + }) { + _create = create ?? this.create; + } + late final PresenterCreator? _create; + + Widget build(BuildContext context, V viewModel); + + Presenter create(PresenterBuilder builder); + + @override + // ignore: library_private_types_in_public_api + _UIState createState() => _UIState(); +} + +class _UIState extends State> { + @override + Widget build(BuildContext context) { + return widget._create!.call( + (viewModel) => widget.build(context, viewModel), + ); + } +} + +typedef PresenterCreator = Presenter Function( + PresenterBuilder builder, +); + +typedef UIBuilder = Widget Function( + BuildContext context, + V viewModel, +); diff --git a/packages/clean_framework/lib/src/providers/external_interface.dart b/packages/clean_framework/lib/src/providers/external_interface.dart index 3740b0b1..ce12b226 100644 --- a/packages/clean_framework/lib/src/providers/external_interface.dart +++ b/packages/clean_framework/lib/src/providers/external_interface.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'package:clean_framework/src/core/external_interface/request.dart'; +import 'package:clean_framework/src/core/external_interface/response.dart'; import 'package:clean_framework/src/providers/gateway.dart'; import 'package:clean_framework/src/utilities/clean_framework_observer.dart'; -import 'package:either_dart/either.dart'; +import 'package:clean_framework/src/utilities/either.dart'; abstract class ExternalInterface { ExternalInterface(List> gatewayConnections) { @@ -87,10 +89,10 @@ class _RequestCompleter { bool get isCompleted => _completer.isCompleted; - void complete(S success) => _completer.complete(Right(success)); + void complete(S success) => _completer.complete(Either.right(success)); void completeFailure(FailureResponse failure) { - _completer.complete(Left(failure)); + _completer.complete(Either.left(failure)); } } diff --git a/packages/clean_framework/lib/src/providers/external_interface_provider.dart b/packages/clean_framework/lib/src/providers/external_interface_provider.dart index 7af57cd0..e6112338 100644 --- a/packages/clean_framework/lib/src/providers/external_interface_provider.dart +++ b/packages/clean_framework/lib/src/providers/external_interface_provider.dart @@ -1,5 +1,4 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework/src/providers/overridable_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/packages/clean_framework/lib/src/providers/gateway.dart b/packages/clean_framework/lib/src/providers/gateway.dart index ab5f4200..9397408b 100644 --- a/packages/clean_framework/lib/src/providers/gateway.dart +++ b/packages/clean_framework/lib/src/providers/gateway.dart @@ -1,8 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:clean_framework/src/app_providers_container.dart'; -import 'package:clean_framework/src/utilities/clean_framework_observer.dart'; -import 'package:either_dart/either.dart'; -import 'package:equatable/equatable.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:meta/meta.dart'; abstract class Gateway _processRequest(buildRequest(output)), + _useCase.subscribe( + (output) => _processRequest(buildRequest(output as O)), ); } @@ -35,8 +30,8 @@ abstract class Gateway> _processRequest(R request) async { final either = await transport(request); return either.fold( - (failureResponse) => Left(_onFailure(failureResponse)), - (response) => Right(onSuccess(response)), + (failureResponse) => Either.left(_onFailure(failureResponse)), + (response) => Either.right(onSuccess(response)), ); } @@ -54,10 +49,9 @@ abstract class BridgeGateway( + _subscriberUseCase.subscribe( + (output) { + return Either.right( onResponse( _publisherUseCase.getOutput(), ), @@ -82,7 +76,9 @@ abstract class WatcherGateway< }) : super(context: context, provider: provider); @override - FailureInput onFailure(FailureResponse failureResponse) => FailureInput(); + FailureInput onFailure(FailureResponse failureResponse) { + return FailureInput(message: failureResponse.message); + } @nonVirtual void yieldResponse(P response) { @@ -92,49 +88,3 @@ abstract class WatcherGateway< typedef Transport = Future> Function(R request); - -@immutable -abstract class Request { - const Request(); -} - -@immutable -abstract class Response extends Equatable { - const Response(); - @override - bool get stringify => true; -} - -class SuccessResponse extends Response { - const SuccessResponse(); - - @override - List get props => []; -} - -abstract class FailureResponse extends Response { - const FailureResponse({this.message = ''}); - - final String message; - - @override - List get props => [message]; -} - -class TypedFailureResponse extends FailureResponse { - const TypedFailureResponse({ - required this.type, - this.errorData = const {}, - super.message, - }); - - final T type; - final Map errorData; - - @override - List get props => [...super.props, type, errorData]; -} - -class UnknownFailureResponse extends FailureResponse { - UnknownFailureResponse([Object? error]) : super(message: error.toString()); -} diff --git a/packages/clean_framework/lib/src/providers/gateway_provider.dart b/packages/clean_framework/lib/src/providers/gateway_provider.dart index 161dc92c..edee3a90 100644 --- a/packages/clean_framework/lib/src/providers/gateway_provider.dart +++ b/packages/clean_framework/lib/src/providers/gateway_provider.dart @@ -1,5 +1,4 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/src/providers/gateway.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework/src/providers/overridable_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/packages/clean_framework/lib/src/providers/presenter.dart b/packages/clean_framework/lib/src/providers/presenter.dart index e4a8d591..8e40bc00 100644 --- a/packages/clean_framework/lib/src/providers/presenter.dart +++ b/packages/clean_framework/lib/src/providers/presenter.dart @@ -1,4 +1,7 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/src/core/use_case/use_case.dart'; +import 'package:clean_framework/src/presentation/presenter/presenter.dart'; +import 'package:clean_framework/src/presentation/presenter/view_model.dart'; +import 'package:clean_framework/src/providers/use_case_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -89,5 +92,3 @@ class _PresenterState super.dispose(); } } - -typedef PresenterBuilder = Widget Function(V viewModel); diff --git a/packages/clean_framework/lib/src/providers/ui.dart b/packages/clean_framework/lib/src/providers/ui.dart index 803993f6..58f0581c 100644 --- a/packages/clean_framework/lib/src/providers/ui.dart +++ b/packages/clean_framework/lib/src/providers/ui.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:flutter/material.dart'; abstract class UI extends StatefulWidget { diff --git a/packages/clean_framework/lib/src/providers/use_case.dart b/packages/clean_framework/lib/src/providers/use_case.dart deleted file mode 100644 index 73fe8ad2..00000000 --- a/packages/clean_framework/lib/src/providers/use_case.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:async'; - -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:either_dart/either.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:meta/meta.dart'; - -typedef OutputBuilder = Output Function(T); - -abstract class UseCase extends StateNotifier { - UseCase({ - required E entity, - Map>? outputFilters, - Map? inputFilters, - }) : _outputFilters = outputFilters ?? const {}, - _inputFilters = inputFilters ?? const {}, - super(entity); - - final Map> _outputFilters; - final Map _inputFilters; - - final Map _requestSubscriptions = {}; - final Map _debounceTimers = {}; - - @override - void dispose() { - for (final debounceTimer in _debounceTimers.values) { - debounceTimer.cancel(); - } - _debounceTimers.clear(); - super.dispose(); - } - - @visibleForTesting - @protected - E get entity => super.state; - - @protected - set entity(E newEntity) => super.state = newEntity; - - /// Executes the [action] so that it will only be executed - /// when there is no further repeated actions with same [tag] - /// in a given frame of [duration]. - /// - /// If [immediate] is false, then then first action will also be debounced. - @protected - void debounce({ - required void Function() action, - required String tag, - Duration duration = const Duration(milliseconds: 300), - bool immediate = true, - }) { - final timer = _debounceTimers[tag]; - - final timerPending = timer?.isActive ?? false; - final canExecute = immediate && !timerPending; - - timer?.cancel(); - _debounceTimers[tag] = Timer( - duration, - () { - _debounceTimers.remove(tag); - if (!immediate) action(); - }, - ); - - if (canExecute) action(); - } - - O getOutput() { - final filter = _outputFilters[O]; - if (filter == null) { - throw StateError('Output filter not defined for $O'); - } - return filter(entity) as O; - } - - void setInput(I input) { - if (_inputFilters[I] == null) { - throw StateError('Input processor not defined for $I'); - } - final processor = _inputFilters[I]! as InputProcessor; - entity = processor(input, entity); - } - - void subscribe(Type outputType, Function callback) { - if (_requestSubscriptions[outputType] != null) { - throw StateError('A subscription for $outputType already exists'); - } - _requestSubscriptions[outputType] = callback; - } - - @protected - Future request( - O output, { - required E Function(S successInput) onSuccess, - required E Function(FailureInput failureInput) onFailure, - }) async { - final callback = _requestSubscriptions[O] ?? - (_) => Left( - NoSubscriptionFailureInput(O), - ); - - // ignore: avoid_dynamic_calls - final either = await callback(output) as Either; - entity = either.fold(onFailure, onSuccess); - } -} - -typedef InputCallback = void Function(I input); - -typedef InputProcessor = E Function( - I input, - E entity, -); - -typedef SubscriptionFilter = V Function( - E entity, -); - -@immutable -abstract class Output extends Equatable { - @override - bool get stringify => true; -} - -@immutable -abstract class Input {} - -class SuccessInput extends Input {} - -class FailureInput extends Input { - FailureInput({this.message = ''}); - final String message; -} - -class NoSubscriptionFailureInput extends FailureInput { - NoSubscriptionFailureInput(Type t) - : super(message: 'No subscription exists for this request of $t'); -} diff --git a/packages/clean_framework/lib/src/providers/use_case_provider.dart b/packages/clean_framework/lib/src/providers/use_case_provider.dart index 262fbb43..4ea46edf 100644 --- a/packages/clean_framework/lib/src/providers/use_case_provider.dart +++ b/packages/clean_framework/lib/src/providers/use_case_provider.dart @@ -1,5 +1,5 @@ import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework/src/providers/overridable_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/packages/clean_framework/lib/src/utilities/clean_framework_observer.dart b/packages/clean_framework/lib/src/utilities/clean_framework_observer.dart index 6308af13..ca367130 100644 --- a/packages/clean_framework/lib/src/utilities/clean_framework_observer.dart +++ b/packages/clean_framework/lib/src/utilities/clean_framework_observer.dart @@ -1,8 +1,9 @@ import 'dart:developer'; +import 'package:clean_framework/src/core/external_interface/request.dart'; +import 'package:clean_framework/src/core/external_interface/response.dart'; +import 'package:clean_framework/src/core/use_case/helpers/input.dart'; import 'package:clean_framework/src/providers/external_interface.dart'; -import 'package:clean_framework/src/providers/gateway.dart'; -import 'package:clean_framework/src/providers/use_case.dart'; /// The class to observe failures, route changes and other events. class CleanFrameworkObserver { diff --git a/packages/clean_framework/lib/src/utilities/either.dart b/packages/clean_framework/lib/src/utilities/either.dart new file mode 100644 index 00000000..4ef52323 --- /dev/null +++ b/packages/clean_framework/lib/src/utilities/either.dart @@ -0,0 +1,99 @@ +import 'package:meta/meta.dart'; + +@Deprecated('Use Either.left') +typedef Left = _Left; + +@Deprecated('Use Either.right') +typedef Right = _Right; + +/// Signature for a function that maps +/// either the left or the right side of this disjunction. +typedef EitherMapper = T Function(E); + +@sealed +@immutable + +/// [Either] represents a value of two possible types. +/// An Either is either an [Either.left] or an [Either.right]. +abstract class Either { + /// Constructs an [Either]. + const Either(); + + /// The Left version of an [Either]. + const factory Either.left(L value) = _Left; + + /// The Right version of an [Either]. + const factory Either.right(R value) = _Right; + + /// Returns whether this [Either] is an [Either.left]. + bool get isLeft => this is _Left; + + /// Returns whether this [Either] is an [Either.right]. + bool get isRight => this is _Right; + + /// Gets the right value if this is an [Either.left] + /// or throws if this is a [Either.right]. + L get left { + return fold((left) => left, _noSuchElementException); + } + + /// Gets the right value if this is an [Either.right] + /// or throws if this is an [Either.left]. + R get right { + return fold(_noSuchElementException, (right) => right); + } + + /// Folds either the left or the right side of this disjunction. + T fold(EitherMapper leftMapper, EitherMapper rightMapper); + + Never _noSuchElementException(value) { + throw NoSuchElementException( + 'You should check ${isLeft ? 'isLeft' : 'isRight'} before calling.', + ); + } +} + +class _Left extends Either { + const _Left(this.value); + + /// The Left value of an [Either]. + final L value; + + @override + T fold(EitherMapper leftMapper, EitherMapper rightMapper) { + return leftMapper(value); + } + + @override + bool operator ==(Object other) => other is _Left && value == other.value; + + @override + int get hashCode => value.hashCode; +} + +class _Right extends Either { + const _Right(this.value); + + /// The Right value of an [Either]. + final R value; + + @override + T fold(EitherMapper leftMapper, EitherMapper rightMapper) { + return rightMapper(value); + } + + @override + bool operator ==(Object other) => other is _Right && value == other.value; + + @override + int get hashCode => value.hashCode; +} + +/// [Exception] that indicates the element being requested does not exist. +class NoSuchElementException implements Exception { + /// Creates a [NoSuchElementException] with an optional error [message]. + const NoSuchElementException([this.message = '']); + + /// The message describing the exception. + final String message; +} diff --git a/packages/clean_framework/pubspec.yaml b/packages/clean_framework/pubspec.yaml index e441df37..599d6b70 100644 --- a/packages/clean_framework/pubspec.yaml +++ b/packages/clean_framework/pubspec.yaml @@ -1,6 +1,6 @@ name: clean_framework description: Clean Architecture components library, inspired on the guidelines created by Uncle Bob. -version: 1.5.0 +version: 2.0.0-dev.0 homepage: https://acmesoftware.com/ repository: https://github.com/MattHamburger/clean_framework @@ -9,17 +9,13 @@ environment: flutter: '>=3.0.0' dependencies: - clean_framework_firestore: ^0.1.0 - clean_framework_graphql: ^0.1.0 - clean_framework_rest: ^0.1.0 - clean_framework_router: ^0.1.0 - either_dart: ^0.2.0 equatable: ^2.0.5 flutter: sdk: flutter - flutter_riverpod: ^2.1.0 + flutter_riverpod: ^2.1.3 meta: '>=1.8.0 <1.9.0' - riverpod: ^2.0.2 + riverpod: ^2.1.3 + stack_trace: ">=1.10.0 <1.12.0" dev_dependencies: clean_framework_test: ^0.1.0 diff --git a/packages/clean_framework/test/core/gateway/gateway_test.dart b/packages/clean_framework/test/core/gateway/gateway_test.dart new file mode 100644 index 00000000..f58a2d24 --- /dev/null +++ b/packages/clean_framework/test/core/gateway/gateway_test.dart @@ -0,0 +1,136 @@ +import 'package:clean_framework/clean_framework.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Gateway tests |', () { + test('success', () async { + final gateway = TestGateway() + ..feedResponse( + (request) async => Either.right( + TestSuccessResponse('Hello ${request.name}!'), + ), + ); + + final input = await gateway.buildInput( + const TestGatewayOutput('Acme Software'), + ); + + expect(input.isRight, isTrue); + expect(input.right.message, 'Hello Acme Software!'); + }); + + test('failure', () async { + final gateway = TestGateway() + ..feedResponse( + (request) async => const Either.left( + TypedFailureResponse( + message: 'Something went wrong', + type: 'failure', + ), + ), + ); + + final input = await gateway.buildInput( + const TestGatewayOutput('Acme Software'), + ); + + expect(input.isLeft, isTrue); + expect(input.left.message, 'Something went wrong'); + }); + }); + + group('Watcher Gateway', () { + test('success', () async { + final gateway = TestWatcherGateway() + ..feedResponse( + (request) async => Either.right( + TestSuccessResponse('Hello ${request.name}!'), + ), + ); + + final input = await gateway.buildInput( + const TestGatewayOutput('Acme Software'), + ); + + expect(input.isRight, isTrue); + expect(input.right.message, 'Hello Acme Software!'); + }); + + test('failure', () async { + final gateway = TestWatcherGateway() + ..feedResponse( + (request) async => const Either.left( + TypedFailureResponse( + message: 'Something went wrong', + type: 'failure', + ), + ), + ); + + final input = await gateway.buildInput( + const TestGatewayOutput('Acme Software'), + ); + + expect(input.isLeft, isTrue); + expect(input.left.message, 'Something went wrong'); + }); + }); +} + +class TestGateway extends Gateway { + @override + TestRequest buildRequest(TestGatewayOutput output) { + return TestRequest(output.name); + } + + @override + FailureInput onFailure(FailureResponse failureResponse) { + return FailureInput(message: failureResponse.message); + } + + @override + TestSuccessInput onSuccess(TestSuccessResponse response) { + return TestSuccessInput(response.message); + } +} + +class TestWatcherGateway extends WatcherGateway { + @override + TestRequest buildRequest(TestGatewayOutput output) { + return TestRequest(output.name); + } + + @override + TestSuccessInput onSuccess(covariant TestSuccessResponse response) { + return TestSuccessInput(response.message); + } +} + +class TestGatewayOutput extends Output { + const TestGatewayOutput(this.name); + + final String name; + + @override + List get props => [name]; +} + +class TestSuccessInput extends SuccessInput { + const TestSuccessInput(this.message); + + final String message; +} + +class TestSuccessResponse extends SuccessResponse { + const TestSuccessResponse(this.message); + + final String message; +} + +class TestRequest extends Request { + const TestRequest(this.name); + + final String name; +} diff --git a/packages/clean_framework/test/core/gateway/gateyway_provider_test.dart b/packages/clean_framework/test/core/gateway/gateyway_provider_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/packages/clean_framework/test/core/gateway/gateyway_provider_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/packages/clean_framework/test/core/use_case/use_case_test.dart b/packages/clean_framework/test/core/use_case/use_case_test.dart new file mode 100644 index 00000000..1d384fa5 --- /dev/null +++ b/packages/clean_framework/test/core/use_case/use_case_test.dart @@ -0,0 +1,301 @@ +import 'package:clean_framework/clean_framework.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late TestUseCase useCase; + + group('UseCase tests |', () { + setUp(() { + useCase = TestUseCase(); + }); + + tearDown(() { + useCase.dispose(); + }); + + test('entity updates with setter', () { + expect(useCase.entity, const TestEntity()); + + useCase.entity = useCase.entity.copyWith(foo: 'bar'); + expect(useCase.entity, const TestEntity(foo: 'bar')); + }); + + test('entity updates with setInput', () { + expect(useCase.entity, const TestEntity()); + + useCase.setInput(const TestInput(foo: 'input')); + expect(useCase.entity, const TestEntity(foo: 'input')); + }); + + test( + 'entity update fails w/ setInput if no appropriate transformer is found', + () { + expect(useCase.entity, const TestEntity()); + expect( + () => useCase.setInput(const NoTransformerTestInput(foo: 'input')), + throwsStateError, + ); + }, + ); + + test( + 'getOutput() success', + () { + expect(useCase.entity, const TestEntity()); + + useCase.setInput(const TestInput(foo: 'input')); + + final output = useCase.getOutput(); + expect(output, const TestOutput(foo: 'input')); + }, + ); + + test( + 'getOutput() fails when no appropriate transformer is found', + () { + expect(useCase.entity, const TestEntity()); + + useCase.setInput(const TestInput(foo: 'input')); + + expect(useCase.getOutput, throwsStateError); + }, + ); + + test( + 'successful request', + () async { + expect(useCase.entity, const TestEntity()); + + useCase.subscribe( + (output) async { + final out = output as TestGatewayOutput; + return Either.right( + TestSuccessInput(message: 'Hello ${out.name}!'), + ); + }, + ); + + await useCase.request( + const TestGatewayOutput(name: 'World'), + onSuccess: (success) => TestEntity(foo: success.message), + onFailure: (failure) => const TestEntity(foo: 'failure'), + ); + + expect(useCase.entity, const TestEntity(foo: 'Hello World!')); + }, + ); + + test( + 'throws if there is no appropriate subscription present', + () async { + expect( + () => useCase.request( + const TestGatewayOutput(name: 'World'), + onSuccess: (success) => const TestEntity(), + onFailure: (failure) => const TestEntity(), + ), + throwsStateError, + ); + }, + ); + + group('debounce', () { + test( + 'performs action immediately first ' + 'and then only after the duration elapses', + () async { + useCase.entity = const TestEntity(foo: '@'); + + String getChar() => useCase.entity.foo; + + void increment() { + useCase.debounce( + action: () { + useCase.entity = useCase.entity.copyWith( + foo: String.fromCharCode(getChar().codeUnitAt(0) + 1), + ); + }, + tag: 'increment', + duration: const Duration(milliseconds: 100), + ); + } + + increment(); + expect(getChar(), equals('A')); + + await Future.delayed(const Duration(milliseconds: 110)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 90)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 75)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 50)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 60)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 105)); + increment(); + expect(getChar(), equals('C')); + + await Future.delayed(const Duration(milliseconds: 95)); + increment(); + expect(getChar(), equals('C')); + + await Future.delayed(const Duration(milliseconds: 105)); + increment(); + expect(getChar(), equals('D')); + }, + ); + + test( + 'performs action only after the duration elapses; ' + 'when immediate is false', + () async { + useCase.entity = const TestEntity(foo: 'A'); + + String getChar() => useCase.entity.foo; + + void increment() { + useCase.debounce( + action: () { + useCase.entity = useCase.entity.copyWith( + foo: String.fromCharCode(getChar().codeUnitAt(0) + 1), + ); + }, + tag: 'increment', + duration: const Duration(milliseconds: 100), + immediate: false, + ); + } + + increment(); + expect(getChar(), equals('A')); + + await Future.delayed(const Duration(milliseconds: 110)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 90)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 75)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 50)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 60)); + increment(); + expect(getChar(), equals('B')); + + await Future.delayed(const Duration(milliseconds: 105)); + increment(); + expect(getChar(), equals('C')); + + await Future.delayed(const Duration(milliseconds: 95)); + increment(); + expect(getChar(), equals('C')); + + await Future.delayed(const Duration(milliseconds: 105)); + increment(); + expect(getChar(), equals('D')); + }, + ); + }); + }); +} + +class TestUseCase extends UseCase { + TestUseCase() + : super( + entity: const TestEntity(), + transformers: [ + TestInputTransformer(), + TestOutputTransformer(), + ], + ); +} + +class TestEntity extends Entity { + const TestEntity({this.foo = ''}); + + final String foo; + + @override + List get props => [foo]; + + @override + TestEntity copyWith({String? foo}) => TestEntity(foo: foo ?? this.foo); +} + +class TestInput extends Input { + const TestInput({required this.foo}); + + final String foo; +} + +class NoTransformerTestInput extends Input { + const NoTransformerTestInput({required this.foo}); + + final String foo; +} + +class TestOutput extends Output { + const TestOutput({required this.foo}); + + final String foo; + + @override + List get props => [foo]; +} + +class TestGatewayOutput extends Output { + const TestGatewayOutput({required this.name}); + + final String name; + + @override + List get props => [name]; +} + +class TestSuccessInput extends SuccessInput { + const TestSuccessInput({required this.message}); + + final String message; +} + +class NoTransformerTestOutput extends Output { + const NoTransformerTestOutput({required this.foo}); + + final String foo; + + @override + List get props => [foo]; +} + +class TestInputTransformer extends InputTransformer { + @override + TestEntity transform(TestEntity entity, TestInput input) { + return entity.copyWith(foo: input.foo); + } +} + +class TestOutputTransformer extends OutputTransformer { + @override + TestOutput transform(TestEntity entity) { + return TestOutput(foo: entity.foo); + } +} diff --git a/packages/clean_framework/test/core/use_case/use_case_transformer_test.dart b/packages/clean_framework/test/core/use_case/use_case_transformer_test.dart new file mode 100644 index 00000000..f3884d47 --- /dev/null +++ b/packages/clean_framework/test/core/use_case/use_case_transformer_test.dart @@ -0,0 +1,223 @@ +import 'package:clean_framework/clean_framework.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late TestUseCase useCase; + + group('UseCase | transformer tests |', () { + setUp(() { + return useCase = TransformerTestUseCase(); + }); + + tearDown(() { + useCase.dispose(); + }); + + test('output transformer', () { + useCase + ..updateFoo('hello') + ..updateBar(3); + + expect(useCase.entity.foo, 'hello'); + expect(useCase.entity.bar, 3); + + expect(useCase.getOutput().foo, 'hello'); + expect(useCase.getOutput().bar, 3); + }); + + test('input transformer', () { + useCase.setInput(const FooInput('hello')); + + expect(useCase.entity.foo, 'hello'); + + expect(useCase.getOutput().foo, 'hello'); + }); + }); + + group('UseCase | inline transformer tests |', () { + setUp(() { + return useCase = InlineTransformerTestUseCase(); + }); + + tearDown(() { + useCase.dispose(); + }); + + test('output transformer', () { + useCase + ..updateFoo('hello') + ..updateBar(3); + + expect(useCase.entity.foo, 'hello'); + expect(useCase.entity.bar, 3); + + expect(useCase.getOutput().foo, 'hello'); + expect(useCase.getOutput().bar, 3); + }); + + test('input transformer', () { + useCase.setInput(const FooInput('hello')); + + expect(useCase.entity.foo, 'hello'); + + expect(useCase.getOutput().foo, 'hello'); + }); + }); + + group('UseCase | legacy filter tests |', () { + setUp(() { + return useCase = FilterTestUseCase(); + }); + + tearDown(() { + useCase.dispose(); + }); + + test('output filter', () { + useCase + ..updateFoo('hello') + ..updateBar(3); + + expect(useCase.entity.foo, 'hello'); + expect(useCase.entity.bar, 3); + + expect(useCase.getOutput().foo, 'hello'); + expect(useCase.getOutput().bar, 3); + }); + + test('input filter', () { + useCase.setInput(const FooInput('hello')); + + expect(useCase.entity.foo, 'hello'); + + expect(useCase.getOutput(), const FooOutput('hello')); + }); + }); +} + +abstract class TestUseCase extends UseCase { + TestUseCase({ + super.transformers, + super.inputFilters, + super.outputFilters, + }) : super(entity: const TestEntity()); + + void updateFoo(String foo) { + entity = entity.copyWith(foo: foo); + } + + void updateBar(int bar) { + entity = entity.copyWith(bar: bar); + } +} + +class TransformerTestUseCase extends TestUseCase { + TransformerTestUseCase() + : super( + transformers: [ + FooOutputTransformer(), + BarOutputTransformer(), + FooInputTransformer(), + ], + ); +} + +class InlineTransformerTestUseCase extends TestUseCase { + InlineTransformerTestUseCase() + : super( + transformers: [ + OutputTransformer.from((entity) => FooOutput(entity.foo)), + OutputTransformer.from((entity) => BarOutput(entity.bar)), + InputTransformer.from( + (entity, input) => entity.copyWith(foo: input.foo), + ), + ], + ); +} + +class FilterTestUseCase extends TestUseCase { + FilterTestUseCase() + : super( + outputFilters: { + FooOutput: (entity) => FooOutput(entity.foo), + BarOutput: (entity) => BarOutput(entity.bar), + }, + inputFilters: { + FooInput: (input, entity) { + return entity.copyWith(foo: (input as FooInput).foo); + }, + }, + ); +} + +class TestSuccessInput extends SuccessInput { + const TestSuccessInput(this.foo); + + final String foo; +} + +class TestEntity extends Entity { + const TestEntity({ + this.foo = '', + this.bar = 0, + }); + + final String foo; + final int bar; + + @override + List get props => [foo, bar]; + + @override + TestEntity copyWith({ + String? foo, + int? bar, + }) { + return TestEntity( + foo: foo ?? this.foo, + bar: bar ?? this.bar, + ); + } +} + +class FooInput extends Input { + const FooInput(this.foo); + final String foo; +} + +class FooOutput extends Output { + const FooOutput(this.foo); + final String foo; + + @override + List get props => [foo]; +} + +class BarOutput extends Output { + const BarOutput(this.bar); + final int bar; + + @override + List get props => [bar]; +} + +class FooOutputTransformer extends OutputTransformer { + @override + FooOutput transform(TestEntity entity) { + return FooOutput(entity.foo); + } +} + +class BarOutputTransformer extends OutputTransformer { + @override + BarOutput transform(TestEntity entity) { + return BarOutput(entity.bar); + } +} + +class FooInputTransformer extends InputTransformer { + @override + TestEntity transform(TestEntity entity, FooInput input) { + return entity.copyWith(foo: input.foo); + } +} diff --git a/packages/clean_framework/test/features/feature_states_handler_unit_test.dart b/packages/clean_framework/test/features/feature_states_handler_unit_test.dart deleted file mode 100644 index 8d042781..00000000 --- a/packages/clean_framework/test/features/feature_states_handler_unit_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_defaults.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('FeatureStatesHandler load and append json', () { - final states = TestFeatureStateMapper() - ..load( - { - 'features': [ - {'name': 'login', 'version': '1.0', 'state': 'VISIBLE'}, - ] - }, - ); - - expect( - states.getStateFor(const Feature(name: 'login')), - FeatureState.visible, - ); - - expect( - states.getStateFor(const Feature(name: 'non-existant')), - FeatureState.hidden, - ); - - states.append({ - 'features': [ - {'name': 'biometrics', 'version': '1.5', 'state': 'VISIBLE'}, - ] - }); - - expect( - states.getStateFor(const Feature(name: 'login')), - FeatureState.visible, - ); - - expect( - states.getStateFor(const Feature(name: 'biometrics', version: '1.5')), - FeatureState.visible, - ); - }); -} - -class TestFeatureStateMapper extends FeatureMapper { - static const Map _jsonStateToFeatureStateMap = { - 'HIDDEN': FeatureState.hidden, - 'VISIBLE': FeatureState.visible, - }; - - @override - FeatureState parseState(String state) { - return _jsonStateToFeatureStateMap[state] ?? defaultState; - } - - @override - FeatureState get defaultState => FeatureState.hidden; -} diff --git a/packages/clean_framework/test/features/feature_widget_test.dart b/packages/clean_framework/test/features/feature_widget_test.dart deleted file mode 100644 index 61b21f33..00000000 --- a/packages/clean_framework/test/features/feature_widget_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:clean_framework/clean_framework_defaults.dart'; -import 'package:clean_framework/src/feature_state/feature.dart'; -import 'package:clean_framework/src/feature_state/feature_mapper.dart'; -import 'package:clean_framework/src/feature_state/feature_state_provider.dart'; -import 'package:clean_framework/src/feature_state/feature_widget.dart'; -import 'package:clean_framework_test/clean_framework_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets( - 'FeatureStatesProvider hidden, then visible with load, then hide again', - (tester) async { - final featureStateProvider = - FeatureStateProvider( - (_) => TestFeatureStateMapper(), - ); - - final featureTester = FeatureTester(featureStateProvider); - - final testWidget = MaterialApp( - home: Column( - children: [ - TestFeatureWidget( - featureStateProvider, - ), - ElevatedButton( - key: const Key('loadButton'), - child: const Text('load'), - onPressed: () { - featureTester.featuresMap.append({ - 'features': [ - {'name': 'login', 'version': '1.0', 'state': 'VISIBLE'}, - ] - }); - }, - ), - ElevatedButton( - key: const Key('hideButton'), - child: const Text('hide'), - onPressed: () { - featureTester.featuresMap.append({ - 'features': [ - {'name': 'login', 'version': '1.0', 'state': 'HIDDEN'}, - ] - }); - }, - ) - ], - ), - ); - - await featureTester.pumpWidget(tester, testWidget); - - expect(find.byType(TestFeatureWidget), findsOneWidget); - expect(find.text('visible'), findsNothing); - expect(find.byKey(const Key('empty')), findsOneWidget); - - await tester.tap(find.byKey(const Key('loadButton'))); - await tester.pumpAndSettle(); - - expect(find.text('visible'), findsOneWidget); - expect(find.byKey(const Key('empty')), findsNothing); - - await tester.tap(find.byKey(const Key('hideButton'))); - await tester.pump(); - - expect(find.text('visible'), findsNothing); - expect(find.byKey(const Key('empty')), findsOneWidget); - - featureTester.dispose(); - }); - - testWidgets('FeatureStatesProvider load error', (tester) async { - final featureStateProvider = - FeatureStateProvider( - (_) => TestFeatureStateMapper(), - ); - - final featureTester = FeatureTester(featureStateProvider); - - final testWidget = MaterialApp( - home: Column( - children: [ - TestFeatureWidget( - featureStateProvider, - ), - ElevatedButton( - key: const Key('loadButton'), - child: const Text('load'), - onPressed: () { - try { - featureTester.featuresMap.append({}); - } catch (e) { - // no-op - } - }, - ), - ], - ), - ); - - await featureTester.pumpWidget(tester, testWidget); - - expect(find.byType(TestFeatureWidget), findsOneWidget); - expect(find.text('visible'), findsNothing); - expect(find.byKey(const Key('empty')), findsOneWidget); - - await tester.tap(find.byKey(const Key('loadButton'))); - await tester.pumpAndSettle(); - - expect(find.text('visible'), findsNothing); - expect(find.byKey(const Key('empty')), findsOneWidget); - - featureTester.dispose(); - }); -} - -class TestFeatureWidget extends FeatureWidget { - const TestFeatureWidget( - FeatureStateProvider> provider, { - super.key, - }) : super( - feature: const Feature(name: 'login'), - provider: provider, - ); - - @override - Widget builder(BuildContext context, FeatureState currentState) { - switch (currentState) { - case FeatureState.visible: - return const Text('visible'); - case FeatureState.hidden: - return Container(key: const Key('empty')); - } - } -} - -class TestFeatureStateMapper extends FeatureMapper { - static const Map _jsonStateToFeatureStateMap = { - 'HIDDEN': FeatureState.hidden, - 'VISIBLE': FeatureState.visible, - }; - - @override - FeatureState parseState(String state) { - return _jsonStateToFeatureStateMap[state] ?? defaultState; - } - - @override - FeatureState get defaultState => FeatureState.hidden; -} diff --git a/packages/clean_framework/test/providers/entity_test.dart b/packages/clean_framework/test/providers/entity_test.dart index 1defcc10..52134477 100644 --- a/packages/clean_framework/test/providers/entity_test.dart +++ b/packages/clean_framework/test/providers/entity_test.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/clean_framework/test/providers/external_interface_integration_test.dart b/packages/clean_framework/test/providers/external_interface_integration_test.dart index 09557af2..7c53e923 100644 --- a/packages/clean_framework/test/providers/external_interface_integration_test.dart +++ b/packages/clean_framework/test/providers/external_interface_integration_test.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:clean_framework/src/app_providers_container.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,7 +11,7 @@ void main() { useCaseTest( 'Interface using direct gateway', context: context, - build: (_) => TestUseCase(TestEntity(foo: 'bar')), + build: (_) => TestUseCase(const TestEntity(foo: 'bar')), setup: (provider) { final gatewayProvider = GatewayProvider( (_) => TestDirectGateway(provider), @@ -22,14 +21,14 @@ void main() { execute: (useCase) => useCase.fetchDataImmediately(), verify: (useCase) { final output = useCase.getOutput(); - expect(output, TestOutput('success')); + expect(output, const TestOutput('success')); }, ); useCaseTest( 'Interface with failure', context: context, - build: (_) => TestUseCase(TestEntity(foo: 'bar')), + build: (_) => TestUseCase(const TestEntity(foo: 'bar')), setup: (provider) { final gatewayProvider = GatewayProvider( (_) => TestWatcherGatewayWitFailure(provider), @@ -39,14 +38,14 @@ void main() { execute: (useCase) => useCase.fetchDataImmediately(), verify: (useCase) { final output = useCase.getOutput(); - expect(output, TestOutput('failure')); + expect(output, const TestOutput('failure')); }, ); useCaseTest( 'Interface using watcher gateway', context: context, - build: (_) => TestUseCase(TestEntity(foo: 'bar')), + build: (_) => TestUseCase(const TestEntity(foo: 'bar')), setup: (provider) { final gatewayProvider = GatewayProvider( (_) => TestYieldGateway(provider), @@ -55,10 +54,10 @@ void main() { }, execute: (useCase) => useCase.fetchDataEventually(), expect: () => [ - TestOutput('0'), - TestOutput('1'), - TestOutput('2'), - TestOutput('3'), + const TestOutput('0'), + const TestOutput('1'), + const TestOutput('2'), + const TestOutput('3'), ], ); } @@ -115,7 +114,7 @@ class TestDirectGateway extends Gateway { TestUseCase(TestEntity entity) : super( entity: entity, - outputFilters: { - TestOutput: (entity) => TestOutput(entity.foo), - }, - inputFilters: { - TestSuccessInput: (TestSuccessInput input, TestEntity entity) => - entity.merge(foo: input.foo), - }, + transformers: [ + OutputTransformer.from((entity) => TestOutput(entity.foo)), + InputTransformer.from( + (entity, input) => entity.merge(foo: input.foo), + ), + ], ); Future fetchDataImmediately() async { await request( - TestDirectOutput('123'), + const TestDirectOutput('123'), onFailure: (_) => entity.merge(foo: 'failure'), onSuccess: (success) => entity.merge(foo: success.foo), ); @@ -177,7 +175,7 @@ class TestUseCase extends UseCase { Future fetchDataImmediatelyWithFailure() async { await request( - TestDirectOutput('123'), + const TestDirectOutput('123'), onFailure: (_) => entity.merge(foo: 'failure'), onSuccess: (success) => entity.merge(foo: success.foo), ); @@ -185,7 +183,7 @@ class TestUseCase extends UseCase { Future fetchDataEventually() async { await request( - TestSubscriptionOutput('123'), + const TestSubscriptionOutput('123'), onFailure: (_) => entity.merge(foo: 'failure'), onSuccess: (_) => entity, // no changes on the entity are needed, // the changes should happen on the inputFilter. @@ -219,12 +217,12 @@ class TestResponse extends SuccessResponse { } class TestSuccessInput extends SuccessInput { - TestSuccessInput(this.foo); + const TestSuccessInput(this.foo); final String foo; } class TestDirectOutput extends Output { - TestDirectOutput(this.id); + const TestDirectOutput(this.id); final String id; @override @@ -232,7 +230,7 @@ class TestDirectOutput extends Output { } class TestSubscriptionOutput extends Output { - TestSubscriptionOutput(this.id); + const TestSubscriptionOutput(this.id); final String id; @override @@ -240,7 +238,7 @@ class TestSubscriptionOutput extends Output { } class TestEntity extends Entity { - TestEntity({required this.foo}); + const TestEntity({required this.foo}); final String foo; @override @@ -250,7 +248,7 @@ class TestEntity extends Entity { } class TestOutput extends Output { - TestOutput(this.foo); + const TestOutput(this.foo); final String foo; @override diff --git a/packages/clean_framework/test/providers/gateway_integration_test.dart b/packages/clean_framework/test/providers/gateway_integration_test.dart index b62b1154..b8808452 100644 --- a/packages/clean_framework/test/providers/gateway_integration_test.dart +++ b/packages/clean_framework/test/providers/gateway_integration_test.dart @@ -1,6 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:clean_framework/src/app_providers_container.dart'; -import 'package:either_dart/either.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:flutter_test/flutter_test.dart'; final context = ProvidersContext(); @@ -8,47 +6,43 @@ final context = ProvidersContext(); void main() { test('Gateway transport direct request with success', () async { final provider = UseCaseProvider( - (_) => TestUseCase(TestEntity(foo: 'bar')), + (_) => TestUseCase(const TestEntity(foo: 'bar')), ); TestDirectGateway(provider).transport = (request) async { - return const Right( - TestResponse('success'), - ); + return const Either.right(TestResponse('success')); }; final useCase = provider.getUseCaseFromContext(context); - await useCase.fetchDataImmediatelly(); + await useCase.fetchDataImmediately(); final output = useCase.getOutput(); - expect(output, TestOutput('success')); + expect(output, const TestOutput('success')); }); test('Gateway transport direct request with failure', () async { final provider = UseCaseProvider( - (_) => TestUseCase(TestEntity(foo: 'bar')), + (_) => TestUseCase(const TestEntity(foo: 'bar')), ); TestDirectGateway(provider).transport = (request) async { - return Left(UnknownFailureResponse()); + return Either.left(UnknownFailureResponse()); }; final useCase = provider.getUseCaseFromContext(context); - await useCase.fetchDataImmediatelly(); + await useCase.fetchDataImmediately(); final output = useCase.getOutput(); - expect(output, TestOutput('failure')); + expect(output, const TestOutput('failure')); }); test('Gateway transport delayed request with a yielded success', () async { final provider = UseCaseProvider( - (_) => TestUseCase(TestEntity(foo: 'bar')), + (_) => TestUseCase(const TestEntity(foo: 'bar')), ); final gateway = TestYieldGateway(provider) ..transport = (request) async { - return const Right( - TestResponse('success'), - ); + return const Either.right(TestResponse('success')); }; final useCase = provider.getUseCaseFromContext(context); @@ -56,24 +50,24 @@ void main() { await useCase.fetchDataEventually(); final output = useCase.getOutput(); - expect(output, TestOutput('bar')); + expect(output, const TestOutput('bar')); gateway.yieldResponse(const TestResponse('with yield')); final output2 = useCase.getOutput(); - expect(output2, TestOutput('with yield')); + expect(output2, const TestOutput('with yield')); }); test('BridgeGateway transfer of data', () async { - final useCase1 = TestUseCase(TestEntity(foo: 'bar')); - final useCase2 = TestUseCase(TestEntity(foo: 'to be replaced')); + final useCase1 = TestUseCase(const TestEntity(foo: 'bar')); + final useCase2 = TestUseCase(const TestEntity(foo: 'to be replaced')); TestBridgeGateway(subscriberUseCase: useCase2, publisherUseCase: useCase1); await useCase2.fetchStateFromOtherUseCase(); final output = useCase2.getOutput(); - expect(output, TestOutput('bar')); + expect(output, const TestOutput('bar')); }); } @@ -98,7 +92,7 @@ class TestDirectGateway extends Gateway { TestUseCase(TestEntity entity) : super( entity: entity, - outputFilters: { - TestOutput: (entity) => TestOutput(entity.foo), - }, - inputFilters: { - TestSuccessInput: (TestSuccessInput input, TestEntity entity) => - entity.merge(foo: input.foo), - }, + transformers: [ + OutputTransformer.from((entity) => TestOutput(entity.foo)), + InputTransformer.from( + (entity, input) => entity.merge(foo: input.foo), + ), + ], ); - Future fetchDataImmediatelly() async { + Future fetchDataImmediately() async { await request( - TestDirectOutput('123'), + const TestDirectOutput('123'), onFailure: (_) => entity.merge(foo: 'failure'), onSuccess: (success) => entity.merge(foo: success.foo), ); @@ -150,7 +143,7 @@ class TestUseCase extends UseCase { Future fetchDataEventually() async { await request( - TestSubscriptionOutput('123'), + const TestSubscriptionOutput('123'), onFailure: (_) => entity.merge(foo: 'failure'), onSuccess: (_) => entity, // no changes on the entity are needed, // the changes should happen on the inputFilter. @@ -159,7 +152,7 @@ class TestUseCase extends UseCase { Future fetchStateFromOtherUseCase() async { await request( - TestDirectOutput(''), + const TestDirectOutput(''), onFailure: (_) => entity, onSuccess: (input) { return entity.merge(foo: input.foo); @@ -182,12 +175,12 @@ class TestResponse extends SuccessResponse { } class TestSuccessInput extends SuccessInput { - TestSuccessInput(this.foo); + const TestSuccessInput(this.foo); final String foo; } class TestDirectOutput extends Output { - TestDirectOutput(this.id); + const TestDirectOutput(this.id); final String id; @override @@ -195,7 +188,7 @@ class TestDirectOutput extends Output { } class TestSubscriptionOutput extends Output { - TestSubscriptionOutput(this.id); + const TestSubscriptionOutput(this.id); final String id; @override @@ -203,7 +196,7 @@ class TestSubscriptionOutput extends Output { } class TestEntity extends Entity { - TestEntity({required this.foo}); + const TestEntity({required this.foo}); final String foo; @override @@ -213,7 +206,7 @@ class TestEntity extends Entity { } class TestOutput extends Output { - TestOutput(this.foo); + const TestOutput(this.foo); final String foo; @override diff --git a/packages/clean_framework/test/providers/gateway_unit_test.dart b/packages/clean_framework/test/providers/gateway_unit_test.dart index 0b6d65d9..020dfbed 100644 --- a/packages/clean_framework/test/providers/gateway_unit_test.dart +++ b/packages/clean_framework/test/providers/gateway_unit_test.dart @@ -1,7 +1,5 @@ -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:clean_framework/src/app_providers_container.dart'; +import 'package:clean_framework/clean_framework_legacy.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(); @@ -11,24 +9,24 @@ void main() { final useCase = UseCaseFake(); final provider = UseCaseProvider((_) => useCase); TestDirectGateway(provider).transport = (request) async { - return const Right(TestResponse('success')); + return const Either.right(TestResponse('success')); }; - await useCase.doFakeRequest(TestDirectOutput('123')); + await useCase.doFakeRequest(const TestDirectOutput('123')); - expect(useCase.entity, EntityFake(value: 'success')); + expect(useCase.entity, const EntityFake(value: 'success')); }); test('Gateway unit test for failure on direct output', () async { final useCase = UseCaseFake(); final provider = UseCaseProvider((_) => useCase); TestDirectGateway(provider).transport = (request) async { - return Left(UnknownFailureResponse()); + return Either.left(UnknownFailureResponse()); }; - await useCase.doFakeRequest(TestDirectOutput('123')); + await useCase.doFakeRequest(const TestDirectOutput('123')); - expect(useCase.entity, EntityFake(value: 'failure')); + expect(useCase.entity, const EntityFake(value: 'failure')); }); test('Gateway unit test for success on yield output', () async { @@ -36,28 +34,28 @@ void main() { final provider = UseCaseProvider((_) => useCase); final gateway = TestYieldGateway(provider) ..transport = (request) async { - return const Right(TestResponse('success')); + return const Either.right(TestResponse('success')); }; - await useCase.doFakeRequest(TestSubscriptionOutput('123')); + await useCase.doFakeRequest(const TestSubscriptionOutput('123')); - expect(useCase.entity, EntityFake(value: 'success')); + expect(useCase.entity, const EntityFake(value: 'success')); gateway.yieldResponse(const TestResponse('with yield')); - expect(useCase.entity, EntityFake(value: 'success with input')); + expect(useCase.entity, const EntityFake(value: 'success with input')); }); test('Gateway unit test for failure on yield output', () async { final useCase = UseCaseFake(); final provider = UseCaseProvider((_) => useCase); TestYieldGateway(provider).transport = (request) async { - return Left(UnknownFailureResponse()); + return Either.left(UnknownFailureResponse()); }; - await useCase.doFakeRequest(TestSubscriptionOutput('123')); + await useCase.doFakeRequest(const TestSubscriptionOutput('123')); - expect(useCase.entity, EntityFake(value: 'failure')); + expect(useCase.entity, const EntityFake(value: 'failure')); }); test('props', () { @@ -78,7 +76,7 @@ class TestDirectGateway extends Gateway { class TestUseCase extends UseCase { TestUseCase() : super( - entity: EntityFake(), - outputFilters: { - TestOutput: (EntityFake entity) => TestOutput(entity.value), - }, + entity: const EntityFake(), + transformers: [ + OutputTransformer.from((entity) => TestOutput(entity.value)), + ], ); Future fetch() async { - entity = EntityFake(value: 'a'); + entity = const EntityFake(value: 'a'); await Future.delayed(const Duration(milliseconds: 100)); - entity = EntityFake(value: 'b'); + entity = const EntityFake(value: 'b'); } } class TestOutput extends Output { - TestOutput(this.foo); + const TestOutput(this.foo); final String foo; @override diff --git a/packages/clean_framework/test/providers/providers_test.dart b/packages/clean_framework/test/providers/providers_test.dart index 1a592ae9..774c8736 100644 --- a/packages/clean_framework/test/providers/providers_test.dart +++ b/packages/clean_framework/test/providers/providers_test.dart @@ -1,5 +1,4 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -100,7 +99,7 @@ class TestBridgeGateway required super.publisherUseCase, }); @override - SuccessInput onResponse(TestOutput output) => SuccessInput(); + SuccessInput onResponse(TestOutput output) => const SuccessInput(); } class TestGateway extends Gateway { @@ -111,12 +110,12 @@ class TestGateway extends Gateway { @override FailureInput onFailure(FailureResponse failureResponse) { - return FailureInput(message: 'backend error'); + return const FailureInput(message: 'backend error'); } @override SuccessInput onSuccess(SuccessResponse response) { - return SuccessInput(); + return const SuccessInput(); } } diff --git a/packages/clean_framework/test/providers/ui_test.dart b/packages/clean_framework/test/providers/ui_test.dart index 6a5def6b..83340013 100644 --- a/packages/clean_framework/test/providers/ui_test.dart +++ b/packages/clean_framework/test/providers/ui_test.dart @@ -1,5 +1,4 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,17 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final router = AppRouter( - routes: [ - AppRoute( - name: 'test', - path: '/', - builder: (_, __) => TestUI(), - ), - ], - errorBuilder: (_, __) => Container(), - ); - uiTest( 'LastLogin without setup', builder: TestUI.new, @@ -31,13 +19,10 @@ void main() { screenSize: const Size(800, 600), ); - setupUITest( - context: ProvidersContext(), - router: router, - ); - uiTest( 'LastLogin using router', + builder: TestUI.new, + context: ProvidersContext(), postFrame: (tester) async { await tester.pump(); }, @@ -51,6 +36,7 @@ void main() { uiTest( 'LastLogin', builder: TestUI.new, + context: ProvidersContext(), verify: (tester) async { expect(find.byType(type()), findsOneWidget); expect(find.text('bar'), findsOneWidget); @@ -85,7 +71,7 @@ class PresenterFake extends Presenter { ); @override - TestOutput subscribe(_) => TestOutput('bar'); + TestOutput subscribe(_) => const TestOutput('bar'); @override TestViewModel createViewModel(_, TestOutput output) { @@ -102,7 +88,7 @@ class TestViewModel extends ViewModel { } class TestOutput extends Output { - TestOutput(this.foo); + const TestOutput(this.foo); final String foo; @override diff --git a/packages/clean_framework/test/providers/use_case_transformer_test.dart b/packages/clean_framework/test/providers/use_case_transformer_test.dart new file mode 100644 index 00000000..355533cb --- /dev/null +++ b/packages/clean_framework/test/providers/use_case_transformer_test.dart @@ -0,0 +1,111 @@ +import 'package:clean_framework/clean_framework_legacy.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('UseCase Transformer tests', () { + test('output transformer', () { + final useCase = TestUseCase() + ..updateFoo('hello') + ..updateBar(3); + + expect(useCase.entity.foo, 'hello'); + expect(useCase.entity.bar, 3); + + expect(useCase.getOutput().foo, 'hello'); + expect(useCase.getOutput().bar, 3); + }); + + test('input transformer', () { + final useCase = TestUseCase()..setInput(const FooInput('hello')); + + expect(useCase.entity.foo, 'hello'); + + expect(useCase.getOutput().foo, 'hello'); + }); + }); +} + +class TestSuccessInput extends SuccessInput { + const TestSuccessInput(this.foo); + + final String foo; +} + +class TestEntity extends Entity { + const TestEntity({ + this.foo = '', + this.bar = 0, + }); + + final String foo; + final int bar; + + @override + List get props => [foo, bar]; + + @override + TestEntity copyWith({ + String? foo, + int? bar, + }) { + return TestEntity( + foo: foo ?? this.foo, + bar: bar ?? this.bar, + ); + } +} + +class TestUseCase extends UseCase { + TestUseCase() + : super( + entity: const TestEntity(), + transformers: [ + FooOutputTransformer(), + FooInputTransformer(), + OutputTransformer.from((entity) => BarOutput(entity.bar)), + ], + ); + + void updateFoo(String foo) { + entity = entity.copyWith(foo: foo); + } + + void updateBar(int bar) { + entity = entity.copyWith(bar: bar); + } +} + +class FooInput extends Input { + const FooInput(this.foo); + final String foo; +} + +class FooOutput extends Output { + const FooOutput(this.foo); + final String foo; + + @override + List get props => [foo]; +} + +class BarOutput extends Output { + const BarOutput(this.bar); + final int bar; + + @override + List get props => [bar]; +} + +class FooOutputTransformer extends OutputTransformer { + @override + FooOutput transform(TestEntity entity) { + return FooOutput(entity.foo); + } +} + +class FooInputTransformer extends InputTransformer { + @override + TestEntity transform(TestEntity entity, FooInput input) { + return entity.copyWith(foo: input.foo); + } +} diff --git a/packages/clean_framework/test/providers/use_case_unit_test.dart b/packages/clean_framework/test/providers/use_case_unit_test.dart deleted file mode 100644 index f9579cb0..00000000 --- a/packages/clean_framework/test/providers/use_case_unit_test.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:either_dart/either.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('UseCase instance, request failure due to not having a subscription', - () async { - final useCase = TestUseCase(TestEntity(foo: '')); - final viewModel = useCase.getOutput(); - - expect(viewModel.foo, ''); - - await useCase.fetchDataImmediatelly(); - - expect(useCase.entity, TestEntity(foo: 'failure')); - - useCase.dispose(); - }); - - test('UseCase subscription with successful request', () async { - final useCase = TestUseCase(TestEntity(foo: '')); - - final successInput = TestSuccessInput('success'); - expect(SuccessInput() == successInput, isFalse); - - useCase.subscribe(TestDirectOutput, (output) { - return Right(successInput); - }); - - expect(() => useCase.subscribe(TestDirectOutput, (_) {}), throwsStateError); - - await useCase.fetchDataImmediatelly(); - - expect(useCase.entity, TestEntity(foo: 'success')); - - useCase.dispose(); - }); - - test('UseCase subscription with delayed response on input filter', () async { - final useCase = TestUseCase(TestEntity(foo: '')) - ..subscribe(TestSubscriptionOutput, (output) { - return Right(SuccessInput()); - }); - - await useCase.fetchDataEventually(); - - //no data change at this point - expect(useCase.entity, TestEntity(foo: '')); - - useCase.setInput(TestSuccessInput('from input filter')); - - expect(useCase.entity, TestEntity(foo: 'from input filter')); - - useCase.dispose(); - }); - - test('UseCase instance, request failure due to not having a subscription', - () async { - final useCase = TestUseCase(TestEntity(foo: '')); - - expect(() => useCase.getOutput(), throwsStateError); - expect( - () => useCase.setInput(FailureInput()), - throwsStateError, - ); - - useCase.dispose(); - }); - - test('UseCase debounce test with immediate', () async { - final useCase = DebouncedUseCase(immediate: true); - - int getCount() => useCase.entity.count; - - useCase.increment(); - expect(getCount(), equals(1)); - - await Future.delayed(const Duration(milliseconds: 110)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 90)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 75)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 50)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 60)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 105)); - useCase.increment(); - expect(getCount(), equals(3)); - - await Future.delayed(const Duration(milliseconds: 95)); - useCase.increment(); - expect(getCount(), equals(3)); - - await Future.delayed(const Duration(milliseconds: 105)); - useCase.increment(); - expect(getCount(), equals(4)); - - useCase.dispose(); - }); - - test('UseCase debounce test without immediate', () async { - final useCase = DebouncedUseCase(immediate: false); - - int getCount() => useCase.entity.count; - - useCase.increment(); - expect(getCount(), equals(0)); - - await Future.delayed(const Duration(milliseconds: 40)); - useCase.increment(); - expect(getCount(), equals(0)); - - await Future.delayed(const Duration(milliseconds: 100)); - useCase.increment(); - expect(getCount(), equals(1)); - - await Future.delayed(const Duration(milliseconds: 110)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 90)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 75)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 50)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 60)); - useCase.increment(); - expect(getCount(), equals(2)); - - await Future.delayed(const Duration(milliseconds: 105)); - useCase.increment(); - expect(getCount(), equals(3)); - - await Future.delayed(const Duration(milliseconds: 95)); - useCase.increment(); - expect(getCount(), equals(3)); - - await Future.delayed(const Duration(milliseconds: 105)); - useCase.increment(); - expect(getCount(), equals(4)); - - useCase.dispose(); - }); -} - -class TestUseCase extends UseCase { - TestUseCase(TestEntity entity) - : super( - entity: entity, - outputFilters: { - TestOutput: (entity) => TestOutput(entity.foo), - }, - inputFilters: { - TestSuccessInput: (TestSuccessInput input, TestEntity entity) => - entity.merge(foo: input.foo), - }, - ); - - Future fetchDataImmediatelly() async { - await request( - TestDirectOutput('123'), - onFailure: (_) => entity.merge(foo: 'failure'), - onSuccess: (success) => entity.merge(foo: success.foo), - ); - } - - Future fetchDataEventually() async { - await request( - TestSubscriptionOutput('123'), - onFailure: (_) => entity.merge(foo: 'failure'), - onSuccess: (_) => entity, // no changes on the entity are needed, - // the changes should happen on the inputFilter. - ); - } -} - -class TestSuccessInput extends SuccessInput { - TestSuccessInput(this.foo); - final String foo; -} - -class TestDirectOutput extends Output { - TestDirectOutput(this.id); - final String id; - - @override - List get props => [id]; -} - -class TestSubscriptionOutput extends Output { - TestSubscriptionOutput(this.id); - final String id; - - @override - List get props => [id]; -} - -class TestEntity extends Entity { - TestEntity({required this.foo}); - final String foo; - - @override - List get props => [foo]; - - TestEntity merge({String? foo}) => TestEntity(foo: foo ?? this.foo); -} - -class TestOutput extends Output { - TestOutput(this.foo); - final String foo; - - @override - List get props => [foo]; -} - -class DebouncedUseCase extends UseCase { - DebouncedUseCase({required this.immediate}) - : super(entity: DebouncedEntity(count: 0)); - - final bool immediate; - - void increment() { - return debounce( - action: () { - entity = entity.merge(count: entity.count + 1); - }, - tag: 'increment', - duration: const Duration(milliseconds: 100), - immediate: immediate, - ); - } -} - -class DebouncedEntity extends Entity { - DebouncedEntity({required this.count}); - - final int count; - - @override - List get props => [count]; - - DebouncedEntity merge({int? count}) { - return DebouncedEntity(count: count ?? this.count); - } -} diff --git a/packages/clean_framework/test/utilities/either_test.dart b/packages/clean_framework/test/utilities/either_test.dart new file mode 100644 index 00000000..cf4713fa --- /dev/null +++ b/packages/clean_framework/test/utilities/either_test.dart @@ -0,0 +1,72 @@ +import 'package:clean_framework/clean_framework.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Either tests | ', () { + test('left side', () { + const direction = Either.left('left'); + + expect(direction.isLeft, isTrue); + expect(direction.isRight, isFalse); + expect(direction, equals(const Either.left('left'))); + }); + + test('right side', () { + const direction = Either.right('right'); + + expect(direction.isLeft, isFalse); + expect(direction.isRight, isTrue); + expect(direction, equals(const Either.right('right'))); + }); + + test('throws if no left side is resolved', () { + const direction = Either.right('right'); + + expect(direction.isLeft, isFalse); + expect(direction.isRight, isTrue); + + expect(() => direction.left, throwsA(isA())); + }); + + test('throws if no right side is resolved', () { + const direction = Either.left('left'); + + expect(direction.isLeft, isTrue); + expect(direction.isRight, isFalse); + + expect(() => direction.right, throwsA(isA())); + }); + + test('equality check', () { + expect( + const Either.left(false), + const Either.left(false), + ); + + expect( + const Either.right(true), + const Either.right(true), + ); + + expect( + const Either.left(false), + isNot(const Either.right(true)), + ); + + expect( + const Either.right(true), + isNot(const Either.left(false)), + ); + + expect( + const Either.left(false).hashCode, + const Either.left(false).hashCode, + ); + + expect( + const Either.right(true).hashCode, + const Either.right(true).hashCode, + ); + }); + }); +} diff --git a/packages/clean_framework_firestore/CHANGELOG.md b/packages/clean_framework_firestore/CHANGELOG.md index a324bab9..adce22a0 100644 --- a/packages/clean_framework_firestore/CHANGELOG.md +++ b/packages/clean_framework_firestore/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## 0.2.0-dev.0 +**Nov 1, 2022** +- Upgraded to `clean_framework: ^2.0.0-dev.0`. + ## 0.1.0 **Nov 1, 2022** - Initial Release diff --git a/packages/clean_framework_firestore/lib/src/firebase_external_interface.dart b/packages/clean_framework_firestore/lib/src/firebase_external_interface.dart index b2a3172e..4989e706 100644 --- a/packages/clean_framework_firestore/lib/src/firebase_external_interface.dart +++ b/packages/clean_framework_firestore/lib/src/firebase_external_interface.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_firestore/src/firebase_client.dart'; import 'package:clean_framework_firestore/src/firebase_requests.dart'; diff --git a/packages/clean_framework_firestore/lib/src/firebase_gateway.dart b/packages/clean_framework_firestore/lib/src/firebase_gateway.dart index 7ee478e8..da48ce5f 100644 --- a/packages/clean_framework_firestore/lib/src/firebase_gateway.dart +++ b/packages/clean_framework_firestore/lib/src/firebase_gateway.dart @@ -1,5 +1,4 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_firestore/src/firebase_requests.dart'; import 'package:clean_framework_firestore/src/firebase_responses.dart'; @@ -12,5 +11,7 @@ abstract class FirebaseGateway FailureInput(); + FailureInput onFailure(FailureResponse failureResponse) { + return FailureInput(message: failureResponse.message); + } } diff --git a/packages/clean_framework_firestore/lib/src/firebase_requests.dart b/packages/clean_framework_firestore/lib/src/firebase_requests.dart index b07766ec..18f79934 100644 --- a/packages/clean_framework_firestore/lib/src/firebase_requests.dart +++ b/packages/clean_framework_firestore/lib/src/firebase_requests.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; class FirebaseRequest extends Request { const FirebaseRequest({required this.path}); diff --git a/packages/clean_framework_firestore/lib/src/firebase_responses.dart b/packages/clean_framework_firestore/lib/src/firebase_responses.dart index 51cc41fc..0683bde2 100644 --- a/packages/clean_framework_firestore/lib/src/firebase_responses.dart +++ b/packages/clean_framework_firestore/lib/src/firebase_responses.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; class FirebaseSuccessResponse extends SuccessResponse { const FirebaseSuccessResponse(this.json); diff --git a/packages/clean_framework_firestore/lib/src/firebase_watcher_gateway.dart b/packages/clean_framework_firestore/lib/src/firebase_watcher_gateway.dart index 0359093c..8c2ec384 100644 --- a/packages/clean_framework_firestore/lib/src/firebase_watcher_gateway.dart +++ b/packages/clean_framework_firestore/lib/src/firebase_watcher_gateway.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_firestore/src/firebase_requests.dart'; import 'package:clean_framework_firestore/src/firebase_responses.dart'; diff --git a/packages/clean_framework_firestore/pubspec.yaml b/packages/clean_framework_firestore/pubspec.yaml index 13c57637..8322b26e 100644 --- a/packages/clean_framework_firestore/pubspec.yaml +++ b/packages/clean_framework_firestore/pubspec.yaml @@ -1,6 +1,6 @@ name: clean_framework_firestore description: A wrapper around cloud_firestore to make it easier to use with clean_framework. -version: 0.1.0 +version: 0.2.0-dev.0 homepage: https://acmesoftware.com/ repository: https://github.com/MattHamburger/clean_framework/packages/clean_framework_firestore @@ -9,14 +9,14 @@ environment: flutter: '>=3.0.0' dependencies: - clean_framework: ^1.5.0 - cloud_firestore: ^4.0.3 + clean_framework: ^2.0.0-dev.0 + cloud_firestore: ^4.3.0 equatable: ^2.0.5 flutter: sdk: flutter dev_dependencies: - clean_framework_test: ^0.1.0 + clean_framework_test: ^0.2.0-dev.1 flutter_test: sdk: flutter mocktail: ^0.3.0 diff --git a/packages/clean_framework_firestore/test/firebase_external_interface_test.dart b/packages/clean_framework_firestore/test/firebase_external_interface_test.dart index 9a207007..ad428da2 100644 --- a/packages/clean_framework_firestore/test/firebase_external_interface_test.dart +++ b/packages/clean_framework_firestore/test/firebase_external_interface_test.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_firestore/clean_framework_firestore.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/clean_framework_firestore/test/firebase_gateway_test.dart b/packages/clean_framework_firestore/test/firebase_gateway_test.dart index b9e34100..9fad0b1b 100644 --- a/packages/clean_framework_firestore/test/firebase_gateway_test.dart +++ b/packages/clean_framework_firestore/test/firebase_gateway_test.dart @@ -1,5 +1,4 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_firestore/clean_framework_firestore.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,26 +9,28 @@ void main() { final useCase = UseCaseFake(); final provider = UseCaseProvider((_) => useCase); TestGateway(context, provider).transport = (request) async { - return const Right(FirebaseSuccessResponse({'content': 'success'})); + return const Either.right( + FirebaseSuccessResponse({'content': 'success'}), + ); }; - await useCase.doFakeRequest(TestOutput('123')); + await useCase.doFakeRequest(const TestOutput('123')); - expect(useCase.entity, EntityFake(value: 'success')); + expect(useCase.entity, const EntityFake(value: 'success')); }); test('FirebaseGateway transport failure', () async { final useCase = UseCaseFake(); final provider = UseCaseProvider((_) => useCase); TestGateway(context, provider).transport = (request) async { - return const Left( + return const Either.left( FirebaseFailureResponse(type: FirebaseFailureType.noContent), ); }; - await useCase.doFakeRequest(TestOutput('123')); + await useCase.doFakeRequest(const TestOutput('123')); - expect(useCase.entity, EntityFake(value: 'failure')); + expect(useCase.entity, const EntityFake(value: 'failure')); }); } @@ -54,7 +55,8 @@ class TestGateway extends FirebaseGateway useCase); TestGateway(context, provider).transport = (request) async { - return const Right(FirebaseSuccessResponse({'content': 'success'})); + return const Either.right( + FirebaseSuccessResponse({'content': 'success'}), + ); }; - await useCase.doFakeRequest(TestOutput('123')); + await useCase.doFakeRequest(const TestOutput('123')); - expect(useCase.entity, EntityFake(value: 'success')); + expect(useCase.entity, const EntityFake(value: 'success')); }); test('FirebaseWatcherGateway transport failure', () async { final useCase = UseCaseFake(); final provider = UseCaseProvider((_) => useCase); TestGateway(context, provider).transport = (request) async { - return const Left( + return const Either.left( FirebaseFailureResponse(type: FirebaseFailureType.noContent), ); }; - await useCase.doFakeRequest(TestOutput('123')); + await useCase.doFakeRequest(const TestOutput('123')); - expect(useCase.entity, EntityFake(value: 'failure')); + expect(useCase.entity, const EntityFake(value: 'failure')); }); } @@ -54,7 +55,8 @@ class TestGateway extends FirebaseWatcherGateway=3.0.0' dependencies: - clean_framework: ^1.5.0 + clean_framework: ^2.0.0-dev.0 flutter: sdk: flutter gql: ^0.14.0 graphql: ^5.1.1 dev_dependencies: - clean_framework_test: ^0.1.0 + clean_framework_test: ^0.2.0-dev.1 flutter_test: sdk: flutter mocktail: ^0.3.0 diff --git a/packages/clean_framework_graphql/test/graphql_external_interface_test.dart b/packages/clean_framework_graphql/test/graphql_external_interface_test.dart index 3d443a96..7a88926a 100644 --- a/packages/clean_framework_graphql/test/graphql_external_interface_test.dart +++ b/packages/clean_framework_graphql/test/graphql_external_interface_test.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_graphql/clean_framework_graphql.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -203,7 +203,7 @@ class GatewayFake extends GraphQLGateway { @override SuccessInput onSuccess(GraphQLSuccessResponse response) { - return SuccessInput(); + return const SuccessInput(); } } @@ -217,7 +217,7 @@ class MutationGatewayFake extends GraphQLGateway { @override SuccessInput onSuccess(GraphQLSuccessResponse response) { - return SuccessInput(); + return const SuccessInput(); } } diff --git a/packages/clean_framework_graphql/test/graphql_gateway_test.dart b/packages/clean_framework_graphql/test/graphql_gateway_test.dart index 7c05897f..caa1a061 100644 --- a/packages/clean_framework_graphql/test/graphql_gateway_test.dart +++ b/packages/clean_framework_graphql/test/graphql_gateway_test.dart @@ -1,5 +1,5 @@ import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_graphql/clean_framework_graphql.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -9,13 +9,13 @@ void main() { final useCase = UseCaseFake(); final gateway = TestGateway(useCase) ..transport = (request) async { - return const Right( + return const Either.right( GraphQLSuccessResponse(data: {}), ); }; await useCase.doFakeRequest(TestOutput()); - expect(useCase.entity, EntityFake(value: 'success')); + expect(useCase.entity, const EntityFake(value: 'success')); final request = gateway.buildRequest(TestOutput()); expect(request.variables, null); @@ -24,13 +24,13 @@ void main() { test('GraphQLGateway failure response', () async { final useCase = UseCaseFake(); TestGateway(useCase).transport = (request) async { - return const Left( + return const Either.left( GraphQLFailureResponse(type: GraphQLFailureType.operation), ); }; await useCase.doFakeRequest(TestOutput()); - expect(useCase.entity, EntityFake(value: 'failure')); + expect(useCase.entity, const EntityFake(value: 'failure')); }); } @@ -45,7 +45,7 @@ class TestGateway @override SuccessInput onSuccess(GraphQLSuccessResponse response) { - return SuccessInput(); + return const SuccessInput(); } } diff --git a/packages/clean_framework_graphql/test/graphql_service_test.dart b/packages/clean_framework_graphql/test/graphql_service_test.dart index 3058dfe0..69d779ff 100644 --- a/packages/clean_framework_graphql/test/graphql_service_test.dart +++ b/packages/clean_framework_graphql/test/graphql_service_test.dart @@ -224,7 +224,7 @@ void main() { when( () => mock.query(any()), ).thenAnswer((_) async { - await Future.delayed(const Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 100)); return successResult; }); diff --git a/packages/clean_framework_rest/CHANGELOG.md b/packages/clean_framework_rest/CHANGELOG.md index 019d4fa6..f20808ea 100644 --- a/packages/clean_framework_rest/CHANGELOG.md +++ b/packages/clean_framework_rest/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## 0.2.0-dev.0 +**Nov 1, 2022** +- Upgraded to `clean_framework: ^2.0.0-dev.0`. + ## 0.1.0 **Nov 1, 2022** - Initial Release \ No newline at end of file diff --git a/packages/clean_framework_rest/lib/src/rest_external_interface.dart b/packages/clean_framework_rest/lib/src/rest_external_interface.dart index 491a1a2b..6484926a 100644 --- a/packages/clean_framework_rest/lib/src/rest_external_interface.dart +++ b/packages/clean_framework_rest/lib/src/rest_external_interface.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_rest/src/rest_requests.dart'; import 'package:clean_framework_rest/src/rest_responses.dart'; import 'package:clean_framework_rest/src/rest_service.dart'; diff --git a/packages/clean_framework_rest/lib/src/rest_gateway.dart b/packages/clean_framework_rest/lib/src/rest_gateway.dart index bf12478b..5fb4a530 100644 --- a/packages/clean_framework_rest/lib/src/rest_gateway.dart +++ b/packages/clean_framework_rest/lib/src/rest_gateway.dart @@ -1,4 +1,4 @@ -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_rest/src/rest_requests.dart'; import 'package:clean_framework_rest/src/rest_responses.dart'; @@ -13,6 +13,6 @@ abstract class RestGateway extends SuccessResponse { const RestSuccessResponse({required this.data}); diff --git a/packages/clean_framework_rest/pubspec.yaml b/packages/clean_framework_rest/pubspec.yaml index 7aa8be02..f51c95a4 100644 --- a/packages/clean_framework_rest/pubspec.yaml +++ b/packages/clean_framework_rest/pubspec.yaml @@ -1,6 +1,6 @@ name: clean_framework_rest description: A wrapper around http to make it easier to use with clean_framework. -version: 0.1.0 +version: 0.2.0-dev.0 homepage: https://acmesoftware.com/ repository: https://github.com/MattHamburger/clean_framework/packages/clean_framework_rest @@ -9,7 +9,7 @@ environment: flutter: '>=3.0.0' dependencies: - clean_framework: ^1.5.0 + clean_framework: ^2.0.0-dev.0 cross_file: ^0.3.3+2 flutter: sdk: flutter @@ -18,7 +18,7 @@ dependencies: path: '>=1.8.2 <1.9.0' dev_dependencies: - clean_framework_test: ^0.1.0 + clean_framework_test: ^0.2.0-dev.1 flutter_test: sdk: flutter mocktail: ^0.3.0 diff --git a/packages/clean_framework_rest/test/rest_external_interface_test.dart b/packages/clean_framework_rest/test/rest_external_interface_test.dart index a4f2fa49..2cf6eecc 100644 --- a/packages/clean_framework_rest/test/rest_external_interface_test.dart +++ b/packages/clean_framework_rest/test/rest_external_interface_test.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:typed_data'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_rest/clean_framework_rest.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/clean_framework_rest/test/rest_gateway_test.dart b/packages/clean_framework_rest/test/rest_gateway_test.dart index 6eb61f7a..a7ff6a7b 100644 --- a/packages/clean_framework_rest/test/rest_gateway_test.dart +++ b/packages/clean_framework_rest/test/rest_gateway_test.dart @@ -1,5 +1,5 @@ import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:clean_framework_rest/clean_framework_rest.dart'; import 'package:clean_framework_test/clean_framework_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -9,13 +9,11 @@ void main() { final useCase = UseCaseFake(); final gateway = TestGateway(useCase) ..transport = (request) async { - return const Right( - RestSuccessResponse(data: {}), - ); + return const Either.right(RestSuccessResponse(data: {})); }; await useCase.doFakeRequest(TestOutput()); - expect(useCase.entity, EntityFake(value: 'success')); + expect(useCase.entity, const EntityFake(value: 'success')); final request = gateway.buildRequest(TestOutput()); expect(request.params, request.data); @@ -25,13 +23,11 @@ void main() { test('RestGateway failure response', () async { final useCase = UseCaseFake(); TestGateway(useCase).transport = (request) async { - return Left( - UnknownFailureResponse(), - ); + return Either.left(UnknownFailureResponse()); }; await useCase.doFakeRequest(TestOutput()); - expect(useCase.entity, EntityFake(value: 'failure')); + expect(useCase.entity, const EntityFake(value: 'failure')); }); test('other requests', () { @@ -54,7 +50,7 @@ class TestGateway extends RestGateway { @override SuccessInput onSuccess(RestSuccessResponse response) { - return SuccessInput(); + return const SuccessInput(); } } diff --git a/packages/clean_framework_router/CHANGELOG.md b/packages/clean_framework_router/CHANGELOG.md index 019d4fa6..3363f8ed 100644 --- a/packages/clean_framework_router/CHANGELOG.md +++ b/packages/clean_framework_router/CHANGELOG.md @@ -1,4 +1,19 @@ # Changelog +## 0.2.0-dev.2 +**Nov 3, 2022** +- Added `AppRouterState` alias. + +## 0.2.0-dev.1 +**Nov 2, 2022 (Breaking)** +- Upgraded to `go_router` v5. +- `AppRouter` is now abstract, in order to make it extensible. +- Routes now need to be enum mixed with `RoutesMixin`. +- Added `AppRouterScope`. + +## 0.2.0-dev.0 +**Nov 1, 2022** +- Upgraded to `clean_framework: ^2.0.0-dev.0`. + ## 0.1.0 **Nov 1, 2022** - Initial Release \ No newline at end of file diff --git a/packages/clean_framework_router/lib/clean_framework_router.dart b/packages/clean_framework_router/lib/clean_framework_router.dart index 17b644ed..7cfa984e 100644 --- a/packages/clean_framework_router/lib/clean_framework_router.dart +++ b/packages/clean_framework_router/lib/clean_framework_router.dart @@ -1 +1,4 @@ +export 'src/app_route.dart'; export 'src/app_router.dart'; +export 'src/app_router_base.dart' show RouterConfiguration; +export 'src/app_router_scope.dart'; diff --git a/packages/clean_framework_router/lib/src/app_route.dart b/packages/clean_framework_router/lib/src/app_route.dart new file mode 100644 index 00000000..7febf1ec --- /dev/null +++ b/packages/clean_framework_router/lib/src/app_route.dart @@ -0,0 +1,58 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; + +export 'package:go_router/go_router.dart' show ShellRoute; + +/// Signature for router's `builder` and `errorBuilder` callback. +typedef RouteWidgetBuilder = Widget Function(BuildContext, GoRouterState); + +/// Signature of the page builder callback for a matched AppRoute. +typedef RoutePageBuilder = Page Function(BuildContext, GoRouterState); + +class AppRoute extends GoRoute { + AppRoute({ + required this.route, + super.builder, + super.routes, + super.redirect, + }) : super(path: route.path, name: (route as Enum).name); + + AppRoute.page({ + required this.route, + RoutePageBuilder? builder, + super.routes, + super.redirect, + }) : super( + path: route.path, + name: (route as Enum).name, + pageBuilder: builder, + ); + + AppRoute.custom({ + required this.route, + RouteWidgetBuilder? builder, + RouteTransitionsBuilder? transitionsBuilder, + super.routes, + super.redirect, + }) : super( + path: route.path, + name: (route as Enum).name, + pageBuilder: builder == null + ? null + : (context, state) { + final transBuilder = + transitionsBuilder ?? (_, __, ___, child) => child; + + return CustomTransitionPage( + child: builder(context, state), + transitionsBuilder: transBuilder, + ); + }, + ); + + final RoutesMixin route; +} + +mixin RoutesMixin { + String get path; +} diff --git a/packages/clean_framework_router/lib/src/app_router.dart b/packages/clean_framework_router/lib/src/app_router.dart index 8d482a6a..65d281e8 100644 --- a/packages/clean_framework_router/lib/src/app_router.dart +++ b/packages/clean_framework_router/lib/src/app_router.dart @@ -1,375 +1,141 @@ import 'package:clean_framework/clean_framework.dart'; -import 'package:flutter/material.dart'; +import 'package:clean_framework_router/src/app_router_base.dart'; +import 'package:clean_framework_router/src/app_router_scope.dart'; +import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -/// Signature for router's `builder` and `errorBuilder` callback. -typedef RouteWidgetBuilder = Widget Function(BuildContext, AppRouteState); +typedef AppRouterState = GoRouterState; -/// Signature of the page builder callback for a matched AppRoute. -typedef RoutePageBuilder = Page Function(BuildContext, AppRouteState); - -/// Signature of the page transitions builder callback for a matched AppRoute. -typedef RouteTransitionsBuilder = Widget Function( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -); - -/// Signature for router's `redirect` callback. -typedef AppRouterRedirect = String? Function(AppRouteState); - -/// Signature of the navigatorBuilder callback. -typedef AppRouterNavigatorBuilder = Widget Function( - BuildContext context, - AppRouteState state, - Widget child, -); - -/// The router class providing high level interface for Navigator 2 APIs, -/// backed by [go_router](https://pub.dev/packages/go_router). -/// -/// ```dart -/// enum Routes {login, home, feed} -/// -/// final router = AppRouter( -/// routes: [ -/// AppRoute( -/// name: Route.login, -/// path: '/login', -/// builder: (context, state) => LoginPage(), -/// ), -/// AppRoute( -/// name: Route.home, -/// path: '/', -/// builder: (context, state) => HomePage(), -/// routes: [ -/// AppRoute( -/// name: Route.feed, -/// path: 'feed', -/// builder: (context, state) => FeedPage(), -/// ), -/// ], -/// ), -/// ], -/// ); -/// ``` -/// -class AppRouter { - /// Default constructor to configure a AppRouter with a routes builder, - /// an error builder and redirections. - AppRouter({ - required List routes, - required RouteWidgetBuilder errorBuilder, - bool enableLogging = false, - this.initialLocation = '/', - AppRouterRedirect? redirect, - AppRouterNavigatorBuilder? navigatorBuilder, - this.observers, - }) : _routes = routes, - _errorBuilder = errorBuilder, - _enableLogging = enableLogging, - _redirect = redirect, - _navigatorBuilder = navigatorBuilder { - _initInnerRouter(); +/// Wrapper class around [GoRouter]. +abstract class AppRouter implements AppRouterBase { + AppRouter() { + _router = configureRouter(); } - late GoRouter _router; - final List _routes; - final RouteWidgetBuilder _errorBuilder; - final bool _enableLogging; - final AppRouterRedirect? _redirect; - AppRouterNavigatorBuilder? _navigatorBuilder; - - /// NavigatorObserver used to receive change notifications - /// when navigation changes. - final List? observers; - - /// The initial location for the router. - /// - /// Default is '/'. - final String initialLocation; - - /// A delegate that is used by the [Router] widget to build and configure a - /// navigating widget. - RouterDelegate get delegate => _router.routerDelegate; - - /// A delegate that is used by the [Router] widget to parse - /// a route information into a configuration of type T. - RouteInformationParser get informationParser { - return _router.routeInformationParser; - } + late final RouterConfiguration _router; - /// The route information provider used by the inner router. - RouteInformationProvider get informationProvider { - return _router.routeInformationProvider; - } - - /// The current location of the router. - String get location => _router.location; - - /// Navigates to specified [route]. - /// - /// Arguments can be passed to the specified route in three ways: - /// - /// 1. Route Parameters - /// ```dart - /// path = '/home/:id'; - /// - /// router.to(Routes.home, params: {'id': '112'}); // location = /home/112 - /// ``` - /// - /// 2. Query Parameters - /// ```dart - /// path = '/home'; - /// - /// router.to(Routes.home, queryParams: {'id': '112'}); // location = /home?id=112 - /// ``` - /// - /// 3. Extra(Not recommended when targeting Flutter Web; as the data get lost, - /// but useful when a complex object is to be passed) - /// ```dart - /// path = '/home'; - /// - /// router.to(Routes.home, extra: 112); // location = /home | extra = 112 - /// ``` - /// - void to( + @override + void go( R route, { - Map params = const {}, - Map queryParams = const {}, + RouterParams params = const {}, + RouterParams queryParams = const {}, Object? extra, }) { - _router.goNamed( - route is Enum ? route.name : route.toString(), + return _router.goNamed( + route.name, params: params, queryParams: queryParams, extra: extra, ); } - /// Navigates to specified [route] by maintaining route stack. + @override void push( R route, { - Map params = const {}, - Map queryParams = const {}, + RouterParams params = const {}, + RouterParams queryParams = const {}, Object? extra, }) { - _router.pushNamed( - route is Enum ? route.name : route.toString(), + return _router.pushNamed( + route.name, params: params, queryParams: queryParams, extra: extra, ); } - /// Navigates to specified [location]; similar to how web behaves. - /// Useful for handling deep links & dynamic links. - /// - /// Similar to [to], an [extra] parameter can be passed alongside the route. - /// - /// ```dart - /// path = '/home/:id'; - /// - /// router.open('/home/123'); - /// ``` - /// - void open(String location, {Object? extra}) { - _router.go(location, extra: extra); + @override + void pushReplacement( + R route, { + RouterParams params = const {}, + RouterParams queryParams = const {}, + Object? extra, + }) { + return _router.pushReplacementNamed( + route.name, + params: params, + queryParams: queryParams, + extra: extra, + ); } - /// Pops the topmost route. - void back() => _router.navigator!.pop(); + @override + void pushLocation(String location, {Object? extra}) { + return _router.push(location, extra: extra); + } - /// Register a closure to be called when the navigation stack changes. - /// - /// Adding a listener will provide a function which can be called off - /// to remove the added listener. - /// - /// ```dart - /// final removeListener = router.addListener( - /// (){ - /// // do something - /// }, - /// ); - /// - /// removeListener(); // removes the listener added above - /// ``` - void Function() addListener(VoidCallback listener) { - _router.addListener(listener); - return () => _router.removeListener(listener); + @override + void goLocation(String location, {Object? extra}) { + return _router.go(location, extra: extra); } - /// Overrides the provided navigatorBuilder for tests. - @visibleForTesting - // ignore: avoid_setters_without_getters - set navigatorBuilder(AppRouterNavigatorBuilder? builder) { - _navigatorBuilder = builder; + @override + void pushReplacementLocation(String location, {Object? extra}) { + return _router.pushReplacement(location, extra: extra); } - /// Resets the router by creating a new instance of underlying router. - @visibleForTesting - void reset() => _initInnerRouter(); + @override + void pop() => _router.pop(); - void _initInnerRouter() { - _router = GoRouter( - routes: _routes, - errorPageBuilder: (context, state) { - return MaterialPage( - key: state.pageKey, - child: _errorBuilder(context, AppRouteState._fromGoRouteState(state)), - ); - }, - initialLocation: initialLocation, - observers: observers, - navigatorBuilder: (context, state, child) { - final navigatorChild = _navigatorBuilder?.call( - context, - AppRouteState._fromGoRouteState(state), - child, - ); - return navigatorChild ?? child; - }, - debugLogDiagnostics: _enableLogging, - redirect: _redirect == null - ? null - : (state) => _redirect!(AppRouteState._fromGoRouteState(state)), - )..addListener(_onLocationChanged); + @override + String locationOf( + R route, { + RouterParams params = const {}, + RouterParams queryParams = const {}, + }) { + return _router.namedLocation( + route.name, + params: params, + queryParams: queryParams, + ); } - void _onLocationChanged() { - CleanFrameworkObserver.instance.onLocationChanged(location); + @override + VoidCallback addListener(VoidCallback listener) { + _router.addListener(listener); + return () => _router.removeListener(listener); } -} - -class _AppRoute extends GoRoute { - _AppRoute({ - required R name, - required super.path, - RouteWidgetBuilder? builder, - RoutePageBuilder? pageBuilder, - super.routes, - super.redirect, - }) : super( - name: name is Enum ? name.name : name.toString(), - builder: (context, state) { - final child = builder?.call( - context, - AppRouteState._fromGoRouteState(state), - ); - - return child ?? const SizedBox.shrink(); - }, - pageBuilder: pageBuilder == null - ? null - : (context, state) { - return pageBuilder( - context, - AppRouteState._fromGoRouteState(state), - ); - }, - ); -} -class AppRoute extends _AppRoute { - AppRoute({ - required super.name, - required super.path, - required super.builder, - super.routes, - super.redirect, - }); + void refresh() => _router.refresh(); - AppRoute.custom({ - required super.name, - required super.path, - required RouteWidgetBuilder builder, - RouteTransitionsBuilder? transitionsBuilder, - super.routes, - super.redirect, - }) : super( - pageBuilder: (context, state) { - return CustomTransitionPage( - child: builder(context, state), - transitionsBuilder: - transitionsBuilder ?? (_, __, ___, child) => child, - ); - }, - ); + @override + String get location => _router.location; - AppRoute.page({ - required super.name, - required super.path, - required RoutePageBuilder builder, - super.routes, - super.redirect, - }) : super(pageBuilder: builder); -} + RouterConfig get config => _router; -/// The state associated with an [AppRoute]. -class AppRouteState { - /// Default constructor to configure an AppRouteState. - AppRouteState({ - required this.location, - required this.subloc, - required this.name, - this.path, - this.fullpath, + @Deprecated('Use go instead.') + void to( + R route, { Map params = const {}, - this.queryParams = const {}, - this.extra, - this.error, - }) : _params = params; - - factory AppRouteState._fromGoRouteState(GoRouterState state) { - return AppRouteState( - location: state.location, - subloc: state.subloc, - name: state.name, - path: state.path, - fullpath: state.fullpath, - params: state.params, - queryParams: state.queryParams, - extra: state.extra, - error: state.error, + Map queryParams = const {}, + Object? extra, + }) { + return go( + route, + params: params, + queryParams: queryParams, + extra: extra, ); } - /// The full location of the route - final String location; - - /// The location of this sub-route, e.g. /family/f2 - final String subloc; - - /// The optional name of the route. - final String? name; - - /// The specified path for the route as configures in [AppRoute]. - final String? path; - - /// The full path to this sub-route, e.g. /family/:fid - final String? fullpath; - - /// The query parameters associated with the route. - final Map queryParams; + @Deprecated('Use goLocation instead.') + void open(String location, {Object? extra}) { + goLocation(location, extra: extra); + } - /// The extra value associated with the route. - final Object? extra; + @Deprecated('Use pop instead.') + void back() => _router.pop(); - /// The error thrown by the builder. - final Exception? error; + @override + void dispose() { + _router.removeListener(_onLocationChanged); + } - final Map _params; + static AppRouter of(BuildContext context) { + return AppRouterScope.of(context).router; + } - /// Get the route param for specified [key]. - /// - /// Throws an assertion error if the route param doesn't contain value - /// for provided `key`. - String getParam(String key) { - assert( - _params.containsKey(key), - 'No route param with "$key" key was passed', - ); - return _params[key]!; + void _onLocationChanged() { + CleanFrameworkObserver.instance.onLocationChanged(location); } } diff --git a/packages/clean_framework_router/lib/src/app_router_base.dart b/packages/clean_framework_router/lib/src/app_router_base.dart new file mode 100644 index 00000000..87f09b82 --- /dev/null +++ b/packages/clean_framework_router/lib/src/app_router_base.dart @@ -0,0 +1,89 @@ +import 'package:flutter/foundation.dart'; +import 'package:go_router/go_router.dart'; + +typedef RouterParams = Map; +typedef RouterConfiguration = GoRouter; + +abstract class AppRouterBase { + RouterConfiguration configureRouter(); + + /// Navigates to the given [route] without respecting previous routes. + /// i.e. navigation stack is not maintained and previous routes + /// might be cleared based on the composition of new [route]. + void go( + R route, { + RouterParams params = const {}, + RouterParams queryParams = const {}, + Object? extra, + }); + + /// Navigates to the given [route] + /// by pushing it at the top of the previous route. + /// i.e navigation stack is maintained and previous routes are preserved. + void push( + R route, { + RouterParams params = const {}, + RouterParams queryParams = const {}, + Object? extra, + }); + + /// Replaces the top-most page of the page stack with the given [route]. + void pushReplacement( + R route, { + RouterParams params = const {}, + RouterParams queryParams = const {}, + Object? extra, + }); + + /// Navigates to the given [location] by replacing the current route. + void goLocation( + String location, { + Object? extra, + }); + + /// Navigates to the given [location] + /// by pushing it at the top of the previous route. + void pushLocation( + String location, { + Object? extra, + }); + + /// Replaces the top-most page of the page stack with the given URL [location] + /// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`. + void pushReplacementLocation( + String location, { + Object? extra, + }); + + /// Navigates the page back to the previous route if available. + void pop(); + + /// Constructs the full location of the given [route] + /// with [params] and [queryParams]. + String locationOf( + R route, { + RouterParams params = const {}, + RouterParams queryParams = const {}, + }); + + /// Register a closure to be called when the navigation stack changes. + /// + /// Adding a listener will provide a function + /// which can be called off to remove the added listener. + /// + /// ```dart + /// final removeListener = router.addListener( + /// (){ + /// // do something + /// }, + /// ); + /// + /// removeListener(); // removes the listener added above + /// ``` + VoidCallback addListener(VoidCallback listener); + + void dispose(); + + /// The current location. + String get location; +} diff --git a/packages/clean_framework_router/lib/src/app_router_scope.dart b/packages/clean_framework_router/lib/src/app_router_scope.dart new file mode 100644 index 00000000..f9cee779 --- /dev/null +++ b/packages/clean_framework_router/lib/src/app_router_scope.dart @@ -0,0 +1,69 @@ +import 'package:clean_framework_router/src/app_router.dart'; +import 'package:flutter/widgets.dart'; + +class AppRouterScope extends StatefulWidget { + const AppRouterScope({ + super.key, + required this.create, + required this.builder, + }); + + final ValueGetter create; + final WidgetBuilder builder; + + // ignore: library_private_types_in_public_api + static _AppRouterScope of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType<_AppRouterScope>(); + + assert( + scope != null, + 'No AppRouterScope found in context. ' + 'Please wrap the top level widget with AppRouterScope.', + ); + + return scope!; + } + + @override + State createState() => _AppRouterScopeState(); +} + +class _AppRouterScopeState extends State { + late final AppRouter _router; + + @override + void initState() { + super.initState(); + _router = widget.create(); + } + + @override + Widget build(BuildContext context) { + return _AppRouterScope( + router: _router, + child: Builder(builder: widget.builder), + ); + } + + @override + void dispose() { + _router.dispose(); + super.dispose(); + } +} + +class _AppRouterScope extends InheritedWidget { + const _AppRouterScope({ + required this.router, + required super.child, + }); + + final AppRouter router; + + @override + bool updateShouldNotify(_AppRouterScope old) => false; +} + +extension AppRouterExtension on BuildContext { + AppRouter get router => AppRouterScope.of(this).router; +} diff --git a/packages/clean_framework_router/pubspec.yaml b/packages/clean_framework_router/pubspec.yaml index 7c61bfae..d25b4346 100644 --- a/packages/clean_framework_router/pubspec.yaml +++ b/packages/clean_framework_router/pubspec.yaml @@ -1,6 +1,6 @@ name: clean_framework_router description: A wrapper around go_router to make it easier to use with clean_framework. -version: 0.1.0 +version: 0.2.0-dev.2 homepage: https://acmesoftware.com/ repository: https://github.com/MattHamburger/clean_framework/packages/clean_framework_router @@ -9,10 +9,10 @@ environment: flutter: '>=3.0.0' dependencies: - clean_framework: ^1.5.0 + clean_framework: ^2.0.0-dev.0 flutter: sdk: flutter - go_router: ^4.5.1 + go_router: ^6.0.1 dev_dependencies: flutter_test: diff --git a/packages/clean_framework_router/test/app_router_test.dart b/packages/clean_framework_router/test/app_router_test.dart index 524a6b41..07792175 100644 --- a/packages/clean_framework_router/test/app_router_test.dart +++ b/packages/clean_framework_router/test/app_router_test.dart @@ -2,34 +2,63 @@ import 'package:clean_framework_router/clean_framework_router.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; -enum Routes { - home, - detail, - moreDetail, +enum Routes with RoutesMixin { + home('/'), + detail('/detail'), + subDetail('detail'), + detailWithParam('/detail/:meta'), + subDetailWithParam('detail/:meta'), + moreDetail('more-detail'), + moreDetailRoot('/more-detail'); + + const Routes(this.path); + + @override + final String path; } late AppRouter testRouter; +class TestRouter extends AppRouter { + TestRouter({ + required this.routes, + this.redirect, + this.observers, + }); + + final List routes; + final GoRouterRedirect? redirect; + final List? observers; + + @override + RouterConfiguration configureRouter() { + return RouterConfiguration( + routes: routes, + errorBuilder: (_, __) => const Page404(), + redirect: redirect, + observers: observers, + ); + } +} + void main() { group('Router tests | ', () { testWidgets( 'route defined with "/" path is the initial route', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => const OnTapPage(id: 'Home'), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, __) => const OnTapPage(id: 'Detail'), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -40,23 +69,20 @@ void main() { testWidgets( 'navigating to sibling route replaces the older sibling', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.detail), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, __) => const OnTapPage(id: 'Detail'), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -77,25 +103,22 @@ void main() { testWidgets( 'navigating to child route pushes the child on top of the parent', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.subDetail), ), routes: [ AppRoute( - name: Routes.detail, - path: 'detail', + route: Routes.subDetail, builder: (_, __) => const OnTapPage(id: 'Detail'), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -111,7 +134,7 @@ void main() { expect(testRouter.location, '/detail'); - testRouter.back(); + testRouter.pop(); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); @@ -123,29 +146,26 @@ void main() { testWidgets( 'navigating with params', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to( - Routes.detail, - params: {'test': '123'}, + onTap: (context) => testRouter.go( + Routes.detailWithParam, + params: {'meta': '123'}, ), ), ), AppRoute( - name: Routes.detail, - path: '/detail/:test', + route: Routes.detailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: state.getParam('test'), + value: state.params['meta'], ), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -167,29 +187,26 @@ void main() { testWidgets( 'navigating with query parameters', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to( + onTap: (context) => testRouter.go( Routes.detail, queryParams: {'test': '123'}, ), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, state) => OnTapPage( id: 'Detail', value: state.queryParams['test'], ), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -211,29 +228,26 @@ void main() { testWidgets( 'navigating with extra argument', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to( + onTap: (context) => testRouter.go( Routes.detail, extra: 123, ), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, state) => OnTapPage( id: 'Detail', value: state.extra.toString(), ), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -256,32 +270,29 @@ void main() { testWidgets( 'navigating with every possible type of arguments', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to( - Routes.detail, - params: {'a': '123'}, + onTap: (context) => testRouter.go( + Routes.detailWithParam, + params: {'meta': '123'}, queryParams: {'b': '456'}, extra: 789, ), ), ), AppRoute( - name: Routes.detail, - path: '/detail/:a', + route: Routes.detailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: '${state.getParam('a')}${state.queryParams['b']}' + value: '${state.params['meta']}${state.queryParams['b']}' '${state.extra}', ), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -302,30 +313,27 @@ void main() { ); testWidgets('pop', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.subDetail), ), routes: [ AppRoute( - name: Routes.detail, - path: 'detail', + route: Routes.subDetail, builder: (_, state) => OnTapPage( id: 'Detail', - onTap: (context) => testRouter.to(Routes.moreDetail), + onTap: (context) => testRouter.go(Routes.moreDetail), ), routes: [ AppRoute( - name: Routes.moreDetail, - path: 'more-detail', + route: Routes.moreDetail, builder: (_, state) => OnTapPage( id: 'More Detail', - onTap: (context) => testRouter.back(), + onTap: (context) => testRouter.pop(), ), ), ], @@ -333,7 +341,6 @@ void main() { ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -377,34 +384,31 @@ void main() { testWidgets( 'navigating using push', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', onTap: (context) => testRouter.push( - Routes.detail, - params: {'a': '123'}, + Routes.subDetailWithParam, + params: {'meta': '123'}, queryParams: {'b': '456'}, extra: 789, ), ), routes: [ AppRoute( - name: Routes.detail, - path: 'detail/:a', + route: Routes.subDetailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: '${state.getParam('a')}${state.queryParams['b']}' + value: '${state.params['meta']}${state.queryParams['b']}' '${state.extra}', ), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -419,7 +423,7 @@ void main() { expect(testRouter.location, '/detail/123?b=456'); - testRouter.back(); + testRouter.pop(); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); @@ -428,34 +432,31 @@ void main() { ); testWidgets( - 'navigating using open', + 'navigating using goLocation', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.open( + onTap: (context) => testRouter.goLocation( '/detail/123?b=456', extra: 789, ), ), routes: [ AppRoute( - name: Routes.detail, - path: 'detail/:a', + route: Routes.subDetailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: '${state.getParam('a')}${state.queryParams['b']}' + value: '${state.params['meta']}${state.queryParams['b']}' '${state.extra}', ), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -470,7 +471,7 @@ void main() { expect(testRouter.location, '/detail/123?b=456'); - testRouter.back(); + testRouter.pop(); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); @@ -479,18 +480,16 @@ void main() { ); testWidgets('shows error widget when route is not found', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.open('/test'), + onTap: (context) => testRouter.goLocation('/test'), ), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -508,27 +507,24 @@ void main() { }); testWidgets( - 'calling reset() will reset the underlying router', + 'calling refresh() will refresh the underlying router', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.subDetail), ), routes: [ AppRoute( - name: Routes.detail, - path: 'detail', + route: Routes.subDetail, builder: (_, __) => const OnTapPage(id: 'Detail'), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -544,97 +540,32 @@ void main() { expect(testRouter.location, '/detail'); - testRouter.reset(); - expect(testRouter.delegate.currentConfiguration, isEmpty); - - // Just resets the underlying router; no change in UI - // Since this method is only intended for tests - expect(find.text('Home'), findsNothing); - expect(find.text('Detail'), findsOneWidget); - }, - ); - - testWidgets( - 'throws assertion error if querying key in not found in param', - (tester) async { - final flutterErrorHandler = FlutterError.onError; - FlutterError.onError = (details) async { - await expectLater( - details.exception.toString(), - contains('No route param with "c" key was passed'), - ); - - FlutterError.onError = flutterErrorHandler; - }; - - testRouter = AppRouter( - routes: [ - AppRoute( - name: Routes.home, - path: '/', - builder: (_, __) => OnTapPage( - id: 'Home', - onTap: (context) => testRouter.to( - Routes.detail, - params: {'a': 'b'}, - ), - ), - routes: [ - AppRoute( - name: Routes.detail, - path: 'detail/:a', - builder: (_, state) => OnTapPage( - id: 'Detail', - value: state.getParam('c'), - ), - ), - ], - ), - ], - errorBuilder: (_, state) { - expect( - state.error.toString(), - contains('No route param with "c" key was passed'), - ); - return const Page404(); - }, - ); - await pumpApp(tester); - - expect(find.text('Home'), findsOneWidget); - expect(find.text('Detail'), findsNothing); - - await tester.tap(find.byType(ElevatedButton)); - await tester.pumpAndSettle(); + testRouter.refresh(); }, ); testWidgets( 'local redirect', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.detail), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, __) => const OnTapPage(id: 'Detail'), - redirect: (state) => '/more-detail', + redirect: (context, state) => '/more-detail', ), AppRoute( - name: Routes.moreDetail, - path: '/more-detail', + route: Routes.moreDetailRoot, builder: (_, __) => const OnTapPage(id: 'More Detail'), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -657,29 +588,25 @@ void main() { testWidgets( 'global redirect', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.detail), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, __) => const OnTapPage(id: 'Detail'), ), AppRoute( - name: Routes.moreDetail, - path: '/more-detail', + route: Routes.moreDetailRoot, builder: (_, __) => const OnTapPage(id: 'More Detail'), ), ], - errorBuilder: (_, __) => const Page404(), - redirect: (state) { + redirect: (context, state) { if (state.location == '/detail') return '/more-detail'; return null; }, @@ -705,31 +632,27 @@ void main() { testWidgets( 'listening to changes in route using addListener', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.detail), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, __) => OnTapPage( id: 'Detail', - onTap: (context) => testRouter.to(Routes.moreDetail), + onTap: (context) => testRouter.go(Routes.moreDetailRoot), ), ), AppRoute( - name: Routes.moreDetail, - path: '/more-detail', + route: Routes.moreDetailRoot, builder: (_, __) => const OnTapPage(id: 'More Detail'), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -739,17 +662,15 @@ void main() { () { switch (count) { case 1: - case 2: expect(testRouter.location, '/detail'); break; - case 3: - case 4: + case 2: expect(testRouter.location, '/more-detail'); break; } count++; }, - count: 4, + count: 2, ), ); @@ -780,32 +701,28 @@ void main() { (tester) async { final observer = TestNavigatorObserver(); - testRouter = AppRouter( + testRouter = TestRouter( observers: [observer], routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', - onTap: (context) => testRouter.to(Routes.detail), + onTap: (context) => testRouter.go(Routes.detail), ), ), AppRoute( - name: Routes.detail, - path: '/detail', + route: Routes.detail, builder: (_, __) => OnTapPage( id: 'Detail', - onTap: (context) => testRouter.to(Routes.moreDetail), + onTap: (context) => testRouter.go(Routes.moreDetailRoot), ), ), AppRoute( - name: Routes.moreDetail, - path: '/more-detail', + route: Routes.moreDetailRoot, builder: (_, __) => const OnTapPage(id: 'More Detail'), ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -844,17 +761,16 @@ void main() { testWidgets( 'using page builder', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute.page( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => CupertinoPage( child: OnTapPage( id: 'Home', onTap: (context) => testRouter.push( - Routes.detail, - params: {'a': '123'}, + Routes.subDetailWithParam, + params: {'meta': '123'}, queryParams: {'b': '456'}, extra: 789, ), @@ -862,18 +778,16 @@ void main() { ), routes: [ AppRoute( - name: Routes.detail, - path: 'detail/:a', + route: Routes.subDetailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: '${state.getParam('a')}${state.queryParams['b']}' + value: '${state.params['meta']}${state.queryParams['b']}' '${state.extra}', ), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -891,7 +805,7 @@ void main() { expect(testRouter.location, '/detail/123?b=456'); - testRouter.back(); + testRouter.pop(); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); @@ -902,16 +816,15 @@ void main() { testWidgets( 'using custom transition', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute.custom( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', onTap: (context) => testRouter.push( - Routes.detail, - params: {'a': '123'}, + Routes.subDetailWithParam, + params: {'meta': '123'}, queryParams: {'b': '456'}, extra: 789, ), @@ -924,18 +837,16 @@ void main() { }, routes: [ AppRoute( - name: Routes.detail, - path: 'detail/:a', + route: Routes.subDetailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: '${state.getParam('a')}${state.queryParams['b']}' + value: '${state.params['meta']}${state.queryParams['b']}' '${state.extra}', ), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -952,7 +863,7 @@ void main() { expect(testRouter.location, '/detail/123?b=456'); - testRouter.back(); + testRouter.pop(); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); @@ -963,34 +874,31 @@ void main() { testWidgets( 'by default custom routes has no transition', (tester) async { - testRouter = AppRouter( + testRouter = TestRouter( routes: [ AppRoute( - name: Routes.home, - path: '/', + route: Routes.home, builder: (_, __) => OnTapPage( id: 'Home', onTap: (context) => testRouter.push( - Routes.detail, - params: {'a': '123'}, + Routes.subDetailWithParam, + params: {'meta': '123'}, queryParams: {'b': '456'}, extra: 789, ), ), routes: [ AppRoute.custom( - name: Routes.detail, - path: 'detail/:a', + route: Routes.subDetailWithParam, builder: (_, state) => OnTapPage( id: 'Detail', - value: '${state.getParam('a')}${state.queryParams['b']}' + value: '${state.params['meta']}${state.queryParams['b']}' '${state.extra}', ), ), ], ), ], - errorBuilder: (_, __) => const Page404(), ); await pumpApp(tester); @@ -1005,7 +913,7 @@ void main() { expect(testRouter.location, '/detail/123?b=456'); - testRouter.back(); + testRouter.pop(); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); @@ -1017,11 +925,7 @@ void main() { Future pumpApp(WidgetTester tester) { return tester.pumpWidget( - MaterialApp.router( - routerDelegate: testRouter.delegate, - routeInformationParser: testRouter.informationParser, - routeInformationProvider: testRouter.informationProvider, - ), + MaterialApp.router(routerConfig: testRouter.config), ); } diff --git a/packages/clean_framework_test/CHANGELOG.md b/packages/clean_framework_test/CHANGELOG.md index 019d4fa6..0e16b64f 100644 --- a/packages/clean_framework_test/CHANGELOG.md +++ b/packages/clean_framework_test/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog +## 0.2.0-dev.1 +**Nov 1, 2022** +- Upgraded to `clean_framework_router: ^0.2.0-dev.0`. + +## 0.2.0-dev.0 +**Nov 1, 2022** +- Upgraded to `clean_framework: ^2.0.0-dev.0`. + ## 0.1.0 **Nov 1, 2022** - Initial Release \ No newline at end of file diff --git a/packages/clean_framework_test/lib/clean_framework_test.dart b/packages/clean_framework_test/lib/clean_framework_test.dart index 7472f440..4bc086b6 100644 --- a/packages/clean_framework_test/lib/clean_framework_test.dart +++ b/packages/clean_framework_test/lib/clean_framework_test.dart @@ -1,4 +1,3 @@ -export 'src/feature_tester.dart'; export 'src/gateway_fake.dart'; export 'src/provider_tester.dart'; export 'src/test_helpers.dart'; diff --git a/packages/clean_framework_test/lib/src/feature_tester.dart b/packages/clean_framework_test/lib/src/feature_tester.dart deleted file mode 100644 index cbc04178..00000000 --- a/packages/clean_framework_test/lib/src/feature_tester.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:clean_framework/clean_framework.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FeatureTester { - FeatureTester(this._featureStateProvider); - - final ProviderContainer _container = ProviderContainer(); - final FeatureStateProvider> _featureStateProvider; - - Future pumpWidget(WidgetTester tester, Widget widget) { - return tester.pumpWidget( - UncontrolledProviderScope(container: _container, child: widget), - ); - } - - void dispose() => _container.dispose(); - - FeatureMapper get featuresMap { - return _container.read(_featureStateProvider.featuresMap); - } -} diff --git a/packages/clean_framework_test/lib/src/gateway_fake.dart b/packages/clean_framework_test/lib/src/gateway_fake.dart index d97ef2f4..bfa49c4d 100644 --- a/packages/clean_framework_test/lib/src/gateway_fake.dart +++ b/packages/clean_framework_test/lib/src/gateway_fake.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:flutter_test/flutter_test.dart'; class GatewayFake extends Fake diff --git a/packages/clean_framework_test/lib/src/test_helpers.dart b/packages/clean_framework_test/lib/src/test_helpers.dart index 963f7ce6..5dff2c8e 100644 --- a/packages/clean_framework_test/lib/src/test_helpers.dart +++ b/packages/clean_framework_test/lib/src/test_helpers.dart @@ -2,8 +2,8 @@ import 'dart:async'; -import 'package:clean_framework/clean_framework.dart'; -import 'package:clean_framework/clean_framework_providers.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; +import 'package:clean_framework_router/clean_framework_router.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -119,12 +119,15 @@ void uiTest( Widget child; if (wrapWithMaterialApp) { if (builder == null) { - resolvedRouter!.navigatorBuilder = (_, __, nav) => scopedChild(nav); - child = MaterialApp.router( - routeInformationParser: resolvedRouter.informationParser, - routerDelegate: resolvedRouter.delegate, - routeInformationProvider: resolvedRouter.informationProvider, - localizationsDelegates: localizationDelegates, + child = AppRouterScope( + create: () => resolvedRouter!, + builder: (context) { + return MaterialApp.router( + routerConfig: context.router.config, + localizationsDelegates: localizationDelegates, + builder: (context, child) => scopedChild(child!), + ); + }, ); } else { child = MaterialApp( diff --git a/packages/clean_framework_test/lib/src/use_case_fake.dart b/packages/clean_framework_test/lib/src/use_case_fake.dart index b8feb227..76b6ee51 100644 --- a/packages/clean_framework_test/lib/src/use_case_fake.dart +++ b/packages/clean_framework_test/lib/src/use_case_fake.dart @@ -1,5 +1,5 @@ -import 'package:clean_framework/clean_framework_providers.dart'; -import 'package:either_dart/either.dart'; +import 'package:clean_framework/clean_framework.dart'; +import 'package:clean_framework/clean_framework_legacy.dart'; import 'package:flutter_test/flutter_test.dart'; typedef UseCaseSubscription = Future> @@ -7,30 +7,26 @@ typedef UseCaseSubscription = Future> Output, ); -class UseCaseFake extends Fake +class UseCaseFake extends Fake implements UseCase { UseCaseFake({this.output}); - EntityFake _entity = EntityFake(); - late Function subscription; - I? successInput; + EntityFake _entity = const EntityFake(); + late RequestSubscription subscription; + S? successInput; final Output? output; @override EntityFake get entity => _entity; @override - Future request( + Future request( O output, { - required EntityFake Function(S successInput) onSuccess, - required EntityFake Function(FailureInput failureInput) onFailure, + required InputCallback onSuccess, + required InputCallback onFailure, }) async { - // ignore: avoid_dynamic_calls - final either = await subscription(output) as Either; - _entity = either.fold( - (FailureInput failureInput) => onFailure(failureInput), - (S successInput) => onSuccess(successInput), - ); + final either = await subscription(output) as Either; + _entity = either.fold(onFailure, onSuccess); } @override @@ -39,8 +35,10 @@ class UseCaseFake extends Fake } @override - void subscribe(Type outputType, Function callback) { - subscription = callback; + void subscribe( + RequestSubscription subscription, + ) { + this.subscription = subscription; } Future doFakeRequest(O output) async { @@ -48,7 +46,7 @@ class UseCaseFake extends Fake output, onFailure: (failure) => _entity.merge('failure'), onSuccess: (success) { - successInput = success as I?; + successInput = success as S?; return _entity.merge('success'); }, ); @@ -56,7 +54,8 @@ class UseCaseFake extends Fake } class EntityFake extends Entity { - EntityFake({this.value = 'initial'}); + const EntityFake({this.value = 'initial'}); + final String value; @override diff --git a/packages/clean_framework_test/pubspec.yaml b/packages/clean_framework_test/pubspec.yaml index 5eb3b53c..a2c016ee 100644 --- a/packages/clean_framework_test/pubspec.yaml +++ b/packages/clean_framework_test/pubspec.yaml @@ -1,6 +1,6 @@ name: clean_framework_test description: A collection test helpers to support clean_framework. -version: 0.1.0 +version: 0.2.0-dev.1 homepage: https://acmesoftware.com/ repository: https://github.com/MattHamburger/clean_framework/packages/clean_framework_test @@ -9,11 +9,11 @@ environment: flutter: '>=3.0.0' dependencies: - clean_framework: ^1.5.0 - either_dart: ^0.2.0 + clean_framework: ^2.0.0-dev.0 + clean_framework_router: ^0.2.0-dev.0 flutter: sdk: flutter - flutter_riverpod: ^2.1.0 + flutter_riverpod: ^2.1.3 flutter_test: sdk: flutter meta: '>=1.8.0 <1.9.0'