├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── iamageo │ │ │ │ └── fluttermvvmtemplate │ │ │ │ └── flutter_mvvm_template │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── main.dart ├── remote │ ├── api │ │ ├── api_endpoints.dart │ │ └── http_manager.dart │ ├── model │ │ ├── base_model.dart │ │ └── base_model.g.dart │ ├── repository │ │ ├── app_repository.dart │ │ └── app_repository_impl.dart │ └── response │ │ ├── api_response.dart │ │ └── api_response.freezed.dart ├── router │ ├── screen_controller.dart │ └── screen_name.dart └── ui │ ├── base_screen.dart │ ├── controller │ └── example_view_model.dart │ ├── get │ ├── binding │ │ └── get_example_binding.dart │ └── get_example_screen.dart │ └── post │ ├── binding │ └── post_example_binding.dart │ └── post_example_screen.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── example_view_model_test.dart └── mocks.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 17 | base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 18 | - platform: android 19 | create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 20 | base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 21 | - platform: ios 22 | create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 23 | base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Geovani Amaral 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MVVM with Repository pattern using GetX in Flutter 🚀 2 | This project implements the MVVM (Model-View-ViewModel) architecture with the Repository pattern in Flutter, using the GetX library for state management and dependency injection. 3 | 4 | ## About the architecture 🏗️ 5 | The MVVM architecture was chosen for its numerous advantages such as better separation of responsibilities, increased testability, and less coupling between UI logic and business logic. 6 | 7 | The main components of this architecture are: 8 | 9 | * Model: Represents the data and state of the app. 10 | * View: Is the visual representation of the user interface, and doesn't contain any business logic. 11 | * ViewModel: Is an abstraction of the View that exposes relevant data streams to the UI. It communicates with the Model to perform tasks and update the View's state. 12 | 13 | The Repository pattern is an integral part of this architecture. The repository is an abstraction layer between the data sources (such as network APIs, local databases, etc.) and the rest of the app. It allows the data sources to be changed without affecting the rest of the app's code. 14 | 15 |

16 | 17 |

18 | 19 | ## About GetX 📦 20 | GetX is an efficient and easy to use state management, navigation, and dependency injection library for Flutter. In the context of this architecture, GetX is used to bind the ViewModels to Widgets in a reactive way, making it easy to update the UI whenever the ViewModel's state changes. 21 | 22 | ## About Freezed 🧊 23 | In our Flutter MVVM template, we utilize the freezed library for creating immutable, safe, and more expressive classes. Freezed allows us to work with design patterns such as Union/Sealed classes, generating necessary boilerplate code for copyWith, hashCode, toString, and more. 24 | 25 | ### Key Advantages: 26 | 27 | * Simplifies the creation of immutable data models. 28 | * Supports union/sealed classes, making state management easier. 29 | * Integrates with json_serializable for efficient JSON conversions. 30 | 31 | ```flutter 32 | @freezed 33 | class MyModel with _$MyModel { 34 | const factory MyModel({ 35 | required String id, 36 | required String name, 37 | }) = _MyModel; 38 | 39 | factory MyModel.fromJson(Map json) => _$MyModelFromJson(json); 40 | } 41 | ``` 42 | 43 | ## About Json Annotation 📝 44 | Json_annotation is used in conjunction with freezed to provide a powerful and flexible way to handle JSON serialization and deserialization. This allows our data models to efficiently communicate with REST APIs by converting JSON data to and from our Dart models. 45 | 46 | ### How We Use It: 47 | 48 | * We define our data models with json_serializable annotations. 49 | * The necessary code for serializing/deserializing is automatically generated. 50 | * We integrate this code with freezed for a seamless development experience. 51 | 52 | ```flutter 53 | @JsonSerializable() 54 | class UserModel { 55 | String name; 56 | String email; 57 | 58 | UserModel({required this name, required this email}); 59 | 60 | factory UserModel.fromJson(Map json) => _$UserModelFromJson(json); 61 | Map toJson() => _$UserModelToJson(this); 62 | } 63 | ``` 64 | 65 | ## How to use this project 🛠️ 66 | * Clone the repository 67 | * Clone this repository to your local machine. 68 | * Install the dependencies 69 | * Run the app 70 | 71 | ## Basic Project Structure 📂 72 | The project is structured as follows: 73 | 74 | ``` 75 | lib 76 | │ main.dart 77 | └───ui 78 | │ └───controller 79 | │ │ example_view_model.dart 80 | │ └───get 81 | │ └───binding 82 | │ │ get_example_screen.dart 83 | │ └───post 84 | │ └───binding 85 | │ │ post_example_screen.dart 86 | └───remote 87 | │ └───api 88 | │ │ api_endpoints.dart 89 | │ │ http_manager.dart 90 | │ └───model 91 | │ │ base_model.dart 92 | │ │ base_model.g.dart 93 | │ └───repository 94 | │ │ app_repository.dart 95 | │ │ app_repository_impl.dart 96 | │ └───response 97 | │ │ api_response.dart 98 | │ │ api_response.freezed.dart 99 | │ │ ... 100 | └───router 101 | │ │ screen_controller.dart 102 | │ │ screen_name.dart 103 | 104 | ``` 105 | 106 | * lib - Contains the main source code of the app. 107 | * models - Contains data models. 108 | * ui - Contains user interface files. 109 | * controller - Contains GetXController (reference to viewModel). 110 | * remote - Contains API related files. 111 | * api - Contains all API-related classes. 112 | * model - Contains data models. 113 | * repository - Contains the Repository classes. 114 | * response - Contains response classes used for handling API responses. 115 | * router - Class organizes and centralizes the definitions of app screens, including their routes and bindings in GetX. 116 | 117 | ## Unit Tests 🧪 118 | To ensure code quality and maintainability, we have added unit tests that cover the ExampleViewModel (and, consequently, the usage of ApiResponse.when(...)). Below are the steps and examples: 119 | 120 | ```yml 121 | dev_dependencies: 122 | flutter_test: 123 | sdk: flutter 124 | 125 | # For generating and using mocks 126 | mockito: ^5.4.6 127 | build_runner: ^2.4.15 128 | 129 | # For serialization (already included in the template) 130 | json_serializable: ^6.9.5 131 | freezed: ^3.0.6 132 | 133 | flutter_lints: ^2.0.0 134 | ``` 135 | 136 | ### Preparing the Mocks 137 | Inside the test/ directory, create the file test/mocks.dart (if it doesn’t exist) with exactly the following content: 138 | 139 | ```dart 140 | // test/mocks.dart 141 | 142 | import 'package:mockito/annotations.dart'; 143 | import 'package:flutter_mvvm_template/remote/repository/app_repository.dart'; 144 | import 'package:flutter_mvvm_template/remote/api/http_manager.dart'; 145 | 146 | @GenerateMocks([ 147 | AppRepository, 148 | HttpManager, // include this if you want to test AppRepositoryImpl 149 | ]) 150 | void main() {} 151 | ``` 152 | 153 | In the terminal, clean up old artifacts and generate the mocks: 154 | 155 | ```bash 156 | flutter pub run build_runner clean 157 | dart run build_runner build --delete-conflicting-outputs 158 | ``` 159 | 160 | ### Writing Tests for the ViewModel 161 | 162 | ```dart 163 | // test/example_view_model_test.dart 164 | 165 | import 'package:flutter_test/flutter_test.dart'; 166 | import 'package:mockito/mockito.dart'; 167 | import 'package:flutter_mvvm_template/remote/model/base_model.dart'; 168 | import 'package:flutter_mvvm_template/remote/response/api_response.dart'; 169 | import 'package:flutter_mvvm_template/remote/repository/app_repository.dart'; 170 | import 'package:flutter_mvvm_template/ui/controller/example_view_model.dart'; 171 | 172 | // Import the generated mocks 173 | import 'mocks.mocks.dart'; 174 | 175 | void main() { 176 | late MockAppRepository mockRepository; 177 | late ExampleViewModel viewModel; 178 | 179 | setUp(() { 180 | // 1) Create the mock instance 181 | mockRepository = MockAppRepository(); 182 | 183 | // 2) Inject it into the ViewModel 184 | viewModel = ExampleViewModel(api: mockRepository); 185 | 186 | // 3) Initial state (optional) 187 | viewModel.getResponse.value = ApiResponse.loading(); 188 | }); 189 | 190 | test('getPosts returns Success and when() works correctly', () async { 191 | // 1) Fake data 192 | final fakeList = [ 193 | BaseModel(id: 1, title: 'Title 1', body: 'Body 1'), 194 | BaseModel(id: 2, title: 'Title 2', body: 'Body 2'), 195 | ]; 196 | 197 | // 2) Configure the mock to return ApiResponse.success 198 | when(mockRepository.getDataExample()) 199 | .thenAnswer((_) async => ApiResponse.success(data: fakeList)); 200 | 201 | // 3) Execute the ViewModel method 202 | await viewModel.getPosts(); 203 | 204 | // 4) Get the final state 205 | final state = viewModel.getResponse.value; 206 | 207 | // 5) Use when(...) to obtain a test String 208 | final text = state.when( 209 | loading: () => 'loading', 210 | success: (data) => 'success with ${data.length}', 211 | error: (msg) => 'error: $msg', 212 | ); 213 | 214 | // 6) Verify the result of when() 215 | expect(text, 'success with 2'); 216 | }); 217 | 218 | test('getPosts returns Error and when() handles the error correctly', () async { 219 | const errorMessage = 'Network failure'; 220 | 221 | // 1) Configure the mock to return ApiResponse.error 222 | when(mockRepository.getDataExample()) 223 | .thenAnswer((_) async => ApiResponse.error(message: errorMessage)); 224 | 225 | // 2) Execute 226 | await viewModel.getPosts(); 227 | 228 | // 3) Get the final state 229 | final state = viewModel.getResponse.value; 230 | 231 | // 4) Use when(...) to extract the error message 232 | final text = state.when( 233 | loading: () => 'loading', 234 | success: (_) => 'should not happen', 235 | error: (msg) => 'error: $msg', 236 | ); 237 | 238 | // 5) Verify the result of when() 239 | expect(text, 'error: Network failure'); 240 | }); 241 | } 242 | ``` 243 | 244 | ### Run all tests 245 | 246 | ```bash 247 | flutter test 248 | ``` 249 | 250 | You should see something like: 251 | 252 | ```bash 253 | 00:00 +2: test getPosts returns Success ... 254 | 00:00 +3: test getPosts returns Error ... 255 | ``` 256 | 257 | ## Note 📝 258 | A different approach is to create a GetX controller for each screen and inject this controller into the binding. In my template, I opted to create only one controller and use it for multiple screens to simplify the structure. 259 | 260 | I hope you find this project useful as a starting point for building Flutter apps using the MVVM architecture, Repository pattern, and GetX! 😃 261 | 262 | ## License 263 | ``` 264 | Copyright 2023 Geovani Amaral 265 | 266 | Licensed under the Apache License, Version 2.0 (the "License"); 267 | you may not use this file except in compliance with the License. 268 | You may obtain a copy of the License at 269 | 270 | http://www.apache.org/licenses/LICENSE-2.0 271 | 272 | Unless required by applicable law or agreed to in writing, software 273 | distributed under the License is distributed on an "AS IS" BASIS, 274 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 275 | See the License for the specific language governing permissions and 276 | limitations under the License. 277 | 278 | ``` 279 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.iamageo.fluttermvvmtemplate.flutter_mvvm_template" 10 | compileSdkVersion flutter.compileSdkVersion 11 | ndkVersion flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility JavaVersion.VERSION_17 15 | targetCompatibility JavaVersion.VERSION_17 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_17 20 | } 21 | 22 | sourceSets { 23 | main.java.srcDirs += 'src/main/kotlin' 24 | } 25 | 26 | defaultConfig { 27 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 28 | applicationId "com.iamageo.fluttermvvmtemplate.flutter_mvvm_template" 29 | // You can update the following values to match your application needs. 30 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 31 | minSdkVersion flutter.minSdkVersion 32 | targetSdkVersion flutter.targetSdkVersion 33 | versionCode flutter.versionCode 34 | versionName flutter.versionName 35 | } 36 | 37 | buildTypes { 38 | release { 39 | // TODO: Add your own signing config for the release build. 40 | // Signing with the debug keys for now, so `flutter run --release` works. 41 | signingConfig signingConfigs.debug 42 | } 43 | } 44 | } 45 | 46 | flutter { 47 | source '../..' 48 | } 49 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 11 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/iamageo/fluttermvvmtemplate/flutter_mvvm_template/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.iamageo.fluttermvvmtemplate.flutter_mvvm_template 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.3.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1300; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | alwaysOutOfDate = 1; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | ); 178 | inputPaths = ( 179 | ); 180 | name = "Thin Binary"; 181 | outputPaths = ( 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | shellPath = /bin/sh; 185 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 186 | }; 187 | 9740EEB61CF901F6004384FC /* Run Script */ = { 188 | isa = PBXShellScriptBuildPhase; 189 | alwaysOutOfDate = 1; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | ); 193 | inputPaths = ( 194 | ); 195 | name = "Run Script"; 196 | outputPaths = ( 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | shellPath = /bin/sh; 200 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 201 | }; 202 | /* End PBXShellScriptBuildPhase section */ 203 | 204 | /* Begin PBXSourcesBuildPhase section */ 205 | 97C146EA1CF9000F007C117D /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 210 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXSourcesBuildPhase section */ 215 | 216 | /* Begin PBXVariantGroup section */ 217 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 218 | isa = PBXVariantGroup; 219 | children = ( 220 | 97C146FB1CF9000F007C117D /* Base */, 221 | ); 222 | name = Main.storyboard; 223 | sourceTree = ""; 224 | }; 225 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | 97C147001CF9000F007C117D /* Base */, 229 | ); 230 | name = LaunchScreen.storyboard; 231 | sourceTree = ""; 232 | }; 233 | /* End PBXVariantGroup section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 242 | CLANG_CXX_LIBRARY = "libc++"; 243 | CLANG_ENABLE_MODULES = YES; 244 | CLANG_ENABLE_OBJC_ARC = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_EMPTY_BODY = YES; 252 | CLANG_WARN_ENUM_CONVERSION = YES; 253 | CLANG_WARN_INFINITE_RECURSION = YES; 254 | CLANG_WARN_INT_CONVERSION = YES; 255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 267 | ENABLE_NS_ASSERTIONS = NO; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | GCC_C_LANGUAGE_STANDARD = gnu99; 270 | GCC_NO_COMMON_BLOCKS = YES; 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 278 | MTL_ENABLE_DEBUG_INFO = NO; 279 | SDKROOT = iphoneos; 280 | SUPPORTED_PLATFORMS = iphoneos; 281 | TARGETED_DEVICE_FAMILY = "1,2"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Profile; 285 | }; 286 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 287 | isa = XCBuildConfiguration; 288 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | CLANG_ENABLE_MODULES = YES; 292 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 293 | DEVELOPMENT_TEAM = 434X5NLQGF; 294 | ENABLE_BITCODE = NO; 295 | INFOPLIST_FILE = Runner/Info.plist; 296 | LD_RUNPATH_SEARCH_PATHS = ( 297 | "$(inherited)", 298 | "@executable_path/Frameworks", 299 | ); 300 | PRODUCT_BUNDLE_IDENTIFIER = com.iamageo.fluttermvvmtemplate.flutterMvvmTemplate; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 303 | SWIFT_VERSION = 5.0; 304 | VERSIONING_SYSTEM = "apple-generic"; 305 | }; 306 | name = Profile; 307 | }; 308 | 97C147031CF9000F007C117D /* Debug */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ALWAYS_SEARCH_USER_PATHS = NO; 312 | CLANG_ANALYZER_NONNULL = YES; 313 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 314 | CLANG_CXX_LIBRARY = "libc++"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 318 | CLANG_WARN_BOOL_CONVERSION = YES; 319 | CLANG_WARN_COMMA = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNREACHABLE_CODE = YES; 335 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 336 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = dwarf; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | ENABLE_TESTABILITY = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu99; 342 | GCC_DYNAMIC_NO_PIC = NO; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_OPTIMIZATION_LEVEL = 0; 345 | GCC_PREPROCESSOR_DEFINITIONS = ( 346 | "DEBUG=1", 347 | "$(inherited)", 348 | ); 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 356 | MTL_ENABLE_DEBUG_INFO = YES; 357 | ONLY_ACTIVE_ARCH = YES; 358 | SDKROOT = iphoneos; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Debug; 362 | }; 363 | 97C147041CF9000F007C117D /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ALWAYS_SEARCH_USER_PATHS = NO; 367 | CLANG_ANALYZER_NONNULL = YES; 368 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 369 | CLANG_CXX_LIBRARY = "libc++"; 370 | CLANG_ENABLE_MODULES = YES; 371 | CLANG_ENABLE_OBJC_ARC = YES; 372 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 373 | CLANG_WARN_BOOL_CONVERSION = YES; 374 | CLANG_WARN_COMMA = YES; 375 | CLANG_WARN_CONSTANT_CONVERSION = YES; 376 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 377 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNREACHABLE_CODE = YES; 390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 391 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 394 | ENABLE_NS_ASSERTIONS = NO; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu99; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | SDKROOT = iphoneos; 407 | SUPPORTED_PLATFORMS = iphoneos; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 410 | TARGETED_DEVICE_FAMILY = "1,2"; 411 | VALIDATE_PRODUCT = YES; 412 | }; 413 | name = Release; 414 | }; 415 | 97C147061CF9000F007C117D /* Debug */ = { 416 | isa = XCBuildConfiguration; 417 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 418 | buildSettings = { 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | CLANG_ENABLE_MODULES = YES; 421 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 422 | DEVELOPMENT_TEAM = 434X5NLQGF; 423 | ENABLE_BITCODE = NO; 424 | INFOPLIST_FILE = Runner/Info.plist; 425 | LD_RUNPATH_SEARCH_PATHS = ( 426 | "$(inherited)", 427 | "@executable_path/Frameworks", 428 | ); 429 | PRODUCT_BUNDLE_IDENTIFIER = com.iamageo.fluttermvvmtemplate.flutterMvvmTemplate; 430 | PRODUCT_NAME = "$(TARGET_NAME)"; 431 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 432 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 433 | SWIFT_VERSION = 5.0; 434 | VERSIONING_SYSTEM = "apple-generic"; 435 | }; 436 | name = Debug; 437 | }; 438 | 97C147071CF9000F007C117D /* Release */ = { 439 | isa = XCBuildConfiguration; 440 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | CLANG_ENABLE_MODULES = YES; 444 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 445 | DEVELOPMENT_TEAM = 434X5NLQGF; 446 | ENABLE_BITCODE = NO; 447 | INFOPLIST_FILE = Runner/Info.plist; 448 | LD_RUNPATH_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "@executable_path/Frameworks", 451 | ); 452 | PRODUCT_BUNDLE_IDENTIFIER = com.iamageo.fluttermvvmtemplate.flutterMvvmTemplate; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 455 | SWIFT_VERSION = 5.0; 456 | VERSIONING_SYSTEM = "apple-generic"; 457 | }; 458 | name = Release; 459 | }; 460 | /* End XCBuildConfiguration section */ 461 | 462 | /* Begin XCConfigurationList section */ 463 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 464 | isa = XCConfigurationList; 465 | buildConfigurations = ( 466 | 97C147031CF9000F007C117D /* Debug */, 467 | 97C147041CF9000F007C117D /* Release */, 468 | 249021D3217E4FDB00AE95B9 /* Profile */, 469 | ); 470 | defaultConfigurationIsVisible = 0; 471 | defaultConfigurationName = Release; 472 | }; 473 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | 97C147061CF9000F007C117D /* Debug */, 477 | 97C147071CF9000F007C117D /* Release */, 478 | 249021D4217E4FDB00AE95B9 /* Profile */, 479 | ); 480 | defaultConfigurationIsVisible = 0; 481 | defaultConfigurationName = Release; 482 | }; 483 | /* End XCConfigurationList section */ 484 | }; 485 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 486 | } 487 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamageo/flutter_mvvm_template/940fe965c0945a16df732f6b8db6c57f93e4c443/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Mvvm Template 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_mvvm_template 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mvvm_template/router/screen_controller.dart'; 3 | import 'package:flutter_mvvm_template/router/screen_name.dart'; 4 | import 'package:flutter_mvvm_template/ui/base_screen.dart'; 5 | import 'package:get/get.dart'; 6 | 7 | void main() { 8 | runApp(const MyApp()); 9 | } 10 | 11 | class MyApp extends StatelessWidget { 12 | const MyApp({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return GetMaterialApp( 17 | debugShowCheckedModeBanner: false, 18 | title: 'Flutter MVVM Template', 19 | theme: ThemeData( 20 | primarySwatch: Colors.blue, 21 | ), 22 | initialRoute: ScreensNames.home, 23 | getPages: AppScreens.screens, 24 | home: const BaseScreen(), 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/remote/api/api_endpoints.dart: -------------------------------------------------------------------------------- 1 | class ApiEndPoints { 2 | 3 | final String getData = "https://jsonplaceholder.typicode.com/posts"; 4 | final String postData = "https://jsonplaceholder.typicode.com/posts"; 5 | 6 | } -------------------------------------------------------------------------------- /lib/remote/api/http_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | abstract class HttpMethods { 4 | static const String post = "POST"; 5 | static const String get = "GET"; 6 | static const String put = "PUT"; 7 | static const String patch = "PATCH"; 8 | static const String delete = "DELETE"; 9 | } 10 | 11 | class HttpManager { 12 | Future restRequest( 13 | {required String url, 14 | required String method, 15 | Map? headers, 16 | Map? body}) async { 17 | final headersDefault = headers?.cast() ?? {} 18 | ..addAll({ 19 | 'content-type': 'application/json', 20 | 'accept': 'application/json' 21 | }); 22 | 23 | Dio dio = Dio(); 24 | 25 | try { 26 | Response response = await dio.request(url, 27 | options: Options(method: method, headers: headersDefault), 28 | data: body); 29 | return response.data; 30 | } on DioException catch (e) { 31 | return e.response?.data ?? {'error' : e}; 32 | } catch (e) { 33 | return {'error': e}; 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /lib/remote/model/base_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'base_model.g.dart'; 4 | 5 | @JsonSerializable() 6 | class BaseModel { 7 | int? id; 8 | String? title; 9 | String? body; 10 | int? userId; 11 | 12 | BaseModel({this.id, this.title, this.body, this.userId}); 13 | 14 | factory BaseModel.fromJson(Map json) => _$BaseModelFromJson(json); 15 | 16 | Map toJson() => _$BaseModelToJson(this); 17 | 18 | } -------------------------------------------------------------------------------- /lib/remote/model/base_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'base_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | BaseModel _$BaseModelFromJson(Map json) => BaseModel( 10 | id: (json['id'] as num?)?.toInt(), 11 | title: json['title'] as String?, 12 | body: json['body'] as String?, 13 | userId: (json['userId'] as num?)?.toInt(), 14 | ); 15 | 16 | Map _$BaseModelToJson(BaseModel instance) => { 17 | 'id': instance.id, 18 | 'title': instance.title, 19 | 'body': instance.body, 20 | 'userId': instance.userId, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/remote/repository/app_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_mvvm_template/remote/model/base_model.dart'; 2 | import 'package:flutter_mvvm_template/remote/response/api_response.dart'; 3 | 4 | abstract class AppRepository { 5 | Future>> getDataExample(); 6 | Future> postDataExample(BaseModel baseModel); 7 | } -------------------------------------------------------------------------------- /lib/remote/repository/app_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_mvvm_template/remote/api/http_manager.dart'; 3 | import 'package:flutter_mvvm_template/remote/response/api_response.dart'; 4 | 5 | import '../api/api_endpoints.dart'; 6 | import '../model/base_model.dart'; 7 | import 'app_repository.dart'; 8 | 9 | class AppRepositoryImpl implements AppRepository { 10 | final _httpManager = HttpManager(); 11 | 12 | @override 13 | Future>> getDataExample() async { 14 | try { 15 | dynamic response = await _httpManager.restRequest( 16 | url: ApiEndPoints().getData, method: HttpMethods.get); 17 | 18 | if (response != null) { 19 | List jsonData = List.from(response.map((item) => BaseModel.fromJson(item))); 20 | return ApiResponse.success(data: jsonData); 21 | } else { 22 | return ApiResponse.error(message: "Ocorreu um erro"); 23 | } 24 | } catch (e) { 25 | return ApiResponse.error(message: e.toString()); 26 | } 27 | } 28 | 29 | @override 30 | Future> postDataExample(BaseModel model) async { 31 | try { 32 | dynamic response = await _httpManager.restRequest( 33 | url: ApiEndPoints().postData, 34 | body: model.toJson(), 35 | method: HttpMethods.post); 36 | 37 | if (response != null) { 38 | BaseModel jsonData = BaseModel.fromJson(response); 39 | return ApiResponse.success(data: jsonData); 40 | } else { 41 | return ApiResponse.error(message: "Ocorreu um erro"); 42 | } 43 | } catch (e) { 44 | print(e); 45 | return ApiResponse.error(message: e.toString()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/remote/response/api_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_response.freezed.dart'; 4 | 5 | @freezed 6 | class ApiResponse with _$ApiResponse { 7 | const factory ApiResponse.loading() = Loading; 8 | const factory ApiResponse.success({ required T data }) = Success; 9 | const factory ApiResponse.error({ required String message }) = Error; 10 | } 11 | 12 | extension ApiResponseWhen on ApiResponse { 13 | R when({ 14 | required R Function() loading, 15 | required R Function(T data) success, 16 | required R Function(String message) error, 17 | }) { 18 | if (this is Loading) { 19 | return loading(); 20 | } 21 | if (this is Success) { 22 | return success((this as Success).data); 23 | } 24 | if (this is Error) { 25 | return error((this as Error).message); 26 | } 27 | throw StateError('Tipo de ApiResponse inesperado'); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/remote/response/api_response.freezed.dart: -------------------------------------------------------------------------------- 1 | // dart format width=80 2 | // coverage:ignore-file 3 | // GENERATED CODE - DO NOT MODIFY BY HAND 4 | // ignore_for_file: type=lint 5 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 6 | 7 | part of 'api_response.dart'; 8 | 9 | // ************************************************************************** 10 | // FreezedGenerator 11 | // ************************************************************************** 12 | 13 | // dart format off 14 | T _$identity(T value) => value; 15 | /// @nodoc 16 | mixin _$ApiResponse { 17 | 18 | 19 | 20 | 21 | 22 | @override 23 | bool operator ==(Object other) { 24 | return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiResponse); 25 | } 26 | 27 | 28 | @override 29 | int get hashCode => runtimeType.hashCode; 30 | 31 | @override 32 | String toString() { 33 | return 'ApiResponse<$T>()'; 34 | } 35 | 36 | 37 | } 38 | 39 | /// @nodoc 40 | class $ApiResponseCopyWith { 41 | $ApiResponseCopyWith(ApiResponse _, $Res Function(ApiResponse) __); 42 | } 43 | 44 | 45 | /// @nodoc 46 | 47 | 48 | class Loading implements ApiResponse { 49 | const Loading(); 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | @override 58 | bool operator ==(Object other) { 59 | return identical(this, other) || (other.runtimeType == runtimeType&&other is Loading); 60 | } 61 | 62 | 63 | @override 64 | int get hashCode => runtimeType.hashCode; 65 | 66 | @override 67 | String toString() { 68 | return 'ApiResponse<$T>.loading()'; 69 | } 70 | 71 | 72 | } 73 | 74 | 75 | 76 | 77 | /// @nodoc 78 | 79 | 80 | class Success implements ApiResponse { 81 | const Success({required this.data}); 82 | 83 | 84 | final T data; 85 | 86 | /// Create a copy of ApiResponse 87 | /// with the given fields replaced by the non-null parameter values. 88 | @JsonKey(includeFromJson: false, includeToJson: false) 89 | @pragma('vm:prefer-inline') 90 | $SuccessCopyWith> get copyWith => _$SuccessCopyWithImpl>(this, _$identity); 91 | 92 | 93 | 94 | @override 95 | bool operator ==(Object other) { 96 | return identical(this, other) || (other.runtimeType == runtimeType&&other is Success&&const DeepCollectionEquality().equals(other.data, data)); 97 | } 98 | 99 | 100 | @override 101 | int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data)); 102 | 103 | @override 104 | String toString() { 105 | return 'ApiResponse<$T>.success(data: $data)'; 106 | } 107 | 108 | 109 | } 110 | 111 | /// @nodoc 112 | abstract mixin class $SuccessCopyWith implements $ApiResponseCopyWith { 113 | factory $SuccessCopyWith(Success value, $Res Function(Success) _then) = _$SuccessCopyWithImpl; 114 | @useResult 115 | $Res call({ 116 | T data 117 | }); 118 | 119 | 120 | 121 | 122 | } 123 | /// @nodoc 124 | class _$SuccessCopyWithImpl 125 | implements $SuccessCopyWith { 126 | _$SuccessCopyWithImpl(this._self, this._then); 127 | 128 | final Success _self; 129 | final $Res Function(Success) _then; 130 | 131 | /// Create a copy of ApiResponse 132 | /// with the given fields replaced by the non-null parameter values. 133 | @pragma('vm:prefer-inline') $Res call({Object? data = freezed,}) { 134 | return _then(Success( 135 | data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable 136 | as T, 137 | )); 138 | } 139 | 140 | 141 | } 142 | 143 | /// @nodoc 144 | 145 | 146 | class Error implements ApiResponse { 147 | const Error({required this.message}); 148 | 149 | 150 | final String message; 151 | 152 | /// Create a copy of ApiResponse 153 | /// with the given fields replaced by the non-null parameter values. 154 | @JsonKey(includeFromJson: false, includeToJson: false) 155 | @pragma('vm:prefer-inline') 156 | $ErrorCopyWith> get copyWith => _$ErrorCopyWithImpl>(this, _$identity); 157 | 158 | 159 | 160 | @override 161 | bool operator ==(Object other) { 162 | return identical(this, other) || (other.runtimeType == runtimeType&&other is Error&&(identical(other.message, message) || other.message == message)); 163 | } 164 | 165 | 166 | @override 167 | int get hashCode => Object.hash(runtimeType,message); 168 | 169 | @override 170 | String toString() { 171 | return 'ApiResponse<$T>.error(message: $message)'; 172 | } 173 | 174 | 175 | } 176 | 177 | /// @nodoc 178 | abstract mixin class $ErrorCopyWith implements $ApiResponseCopyWith { 179 | factory $ErrorCopyWith(Error value, $Res Function(Error) _then) = _$ErrorCopyWithImpl; 180 | @useResult 181 | $Res call({ 182 | String message 183 | }); 184 | 185 | 186 | 187 | 188 | } 189 | /// @nodoc 190 | class _$ErrorCopyWithImpl 191 | implements $ErrorCopyWith { 192 | _$ErrorCopyWithImpl(this._self, this._then); 193 | 194 | final Error _self; 195 | final $Res Function(Error) _then; 196 | 197 | /// Create a copy of ApiResponse 198 | /// with the given fields replaced by the non-null parameter values. 199 | @pragma('vm:prefer-inline') $Res call({Object? message = null,}) { 200 | return _then(Error( 201 | message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable 202 | as String, 203 | )); 204 | } 205 | 206 | 207 | } 208 | 209 | // dart format on 210 | -------------------------------------------------------------------------------- /lib/router/screen_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_mvvm_template/router/screen_name.dart'; 2 | import 'package:flutter_mvvm_template/ui/base_screen.dart'; 3 | import 'package:flutter_mvvm_template/ui/get/binding/get_example_binding.dart'; 4 | import 'package:flutter_mvvm_template/ui/get/get_example_screen.dart'; 5 | import 'package:flutter_mvvm_template/ui/post/binding/post_example_binding.dart'; 6 | import 'package:flutter_mvvm_template/ui/post/post_example_screen.dart'; 7 | import 'package:get/get_navigation/src/routes/get_route.dart'; 8 | 9 | abstract class AppScreens { 10 | 11 | static final screens = [ 12 | GetPage(name: ScreensNames.home, page: () => const BaseScreen()), 13 | GetPage(name: ScreensNames.getExample, page: () => const GetExampleScreen(), bindings: [GetExampleBinding()]), 14 | GetPage(name: ScreensNames.postExample, page: () => const PostExampleScreen(), bindings: [PostExampleBinding()]), 15 | ]; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/router/screen_name.dart: -------------------------------------------------------------------------------- 1 | abstract class ScreensNames { 2 | 3 | static const String home = "/home"; 4 | static const String getExample = "/getExample"; 5 | static const String postExample = "/postExample"; 6 | 7 | } -------------------------------------------------------------------------------- /lib/ui/base_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mvvm_template/router/screen_name.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | class BaseScreen extends StatefulWidget { 6 | const BaseScreen({super.key}); 7 | 8 | @override 9 | State createState() => _BaseScreenState(); 10 | } 11 | 12 | class _BaseScreenState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text("MVVM Flutter Template"), 18 | ), 19 | body: Padding( 20 | padding: const EdgeInsets.all(8.0), 21 | child: Column( 22 | mainAxisAlignment: MainAxisAlignment.center, 23 | crossAxisAlignment: CrossAxisAlignment.stretch, 24 | children: [ 25 | Expanded( 26 | child: Column( 27 | mainAxisAlignment: MainAxisAlignment.center, 28 | crossAxisAlignment: CrossAxisAlignment.stretch, 29 | children: [ 30 | ElevatedButton( 31 | onPressed: () { 32 | Get.toNamed(ScreensNames.getExample); 33 | }, 34 | child: const Text("GET"), 35 | ), 36 | ElevatedButton( 37 | onPressed: () { 38 | Get.toNamed(ScreensNames.postExample); 39 | }, 40 | child: const Text("POST"), 41 | ), 42 | ], 43 | ), 44 | ), 45 | const Padding( 46 | padding: EdgeInsets.all(8.0), 47 | child: Column( 48 | crossAxisAlignment: CrossAxisAlignment.center, 49 | children: [ 50 | Text("v - 1.0.0"), 51 | Text("Made with ❤️ by iamageo"), 52 | ], 53 | ), 54 | ) 55 | ], 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /lib/ui/controller/example_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_mvvm_template/remote/repository/app_repository.dart'; 2 | import 'package:flutter_mvvm_template/remote/repository/app_repository_impl.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import '../../remote/model/base_model.dart'; 6 | import '../../remote/response/api_response.dart'; 7 | 8 | class ExampleViewModel extends GetxController { 9 | 10 | final AppRepository _api; 11 | 12 | ExampleViewModel({required AppRepository api}) : _api = api; 13 | 14 | Rx>> getResponse = const ApiResponse>.loading().obs; 15 | Rx> postResponse = ApiResponse.loading().obs; 16 | 17 | Future getPosts() async { 18 | 19 | ApiResponse> result = await _api.getDataExample(); 20 | 21 | result.when( 22 | loading: () { 23 | getResponse.value = const ApiResponse.loading(); 24 | }, 25 | success: (list) { 26 | getResponse.value = ApiResponse.success(data: list); 27 | update(); 28 | }, 29 | error: (message) { 30 | getResponse.value = ApiResponse.error(message: message); 31 | }, 32 | ); 33 | } 34 | 35 | Future sendPost({required BaseModel model}) async { 36 | postResponse.value = ApiResponse.loading(); 37 | 38 | ApiResponse result = await _api.postDataExample(model); 39 | 40 | result.when( 41 | loading: () { 42 | postResponse.value = const ApiResponse.loading(); 43 | }, 44 | success: (list) { 45 | postResponse.value = ApiResponse.success(data: list); 46 | update(); 47 | }, 48 | error: (message) { 49 | postResponse.value = ApiResponse.error(message: message); 50 | }, 51 | ); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /lib/ui/get/binding/get_example_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import '../../../remote/repository/app_repository.dart'; 4 | import '../../../remote/repository/app_repository_impl.dart'; 5 | import '../../controller/example_view_model.dart'; 6 | 7 | class GetExampleBinding extends Bindings { 8 | @override 9 | void dependencies() { 10 | Get.put(AppRepositoryImpl()); 11 | Get.put(ExampleViewModel(api: Get.find())); 12 | } 13 | } -------------------------------------------------------------------------------- /lib/ui/get/get_example_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mvvm_template/remote/model/base_model.dart'; 3 | import 'package:flutter_mvvm_template/remote/response/api_response.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:shimmer/shimmer.dart'; 6 | 7 | import '../controller/example_view_model.dart'; 8 | 9 | 10 | class GetExampleScreen extends StatefulWidget { 11 | const GetExampleScreen({super.key}); 12 | 13 | @override 14 | State createState() => _GetExampleScreenState(); 15 | } 16 | 17 | class _GetExampleScreenState extends State { 18 | ExampleViewModel viewModel = Get.find(); 19 | 20 | @override 21 | void initState() { 22 | viewModel.getPosts(); 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | title: const Text("get example"), 31 | ), 32 | body: Column( 33 | mainAxisAlignment: MainAxisAlignment.center, 34 | children: [ 35 | Obx(() { 36 | return viewModel.getResponse.value.when( 37 | loading: () => buildShimmerEffect(), 38 | success: (list) => buildUserList(list), 39 | error: (message) => buildUserError(message), 40 | ); 41 | }) 42 | ], 43 | ), 44 | ); 45 | } 46 | 47 | Padding buildUserError(String message) { 48 | return Padding( 49 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 50 | child: Column( 51 | crossAxisAlignment: CrossAxisAlignment.stretch, 52 | children: [ 53 | Align( 54 | alignment: Alignment.center, 55 | child: Text('Ops, ocorreu um erro, tente novamente. ${message}')), 56 | ElevatedButton( 57 | onPressed: () { 58 | viewModel.getPosts(); 59 | }, 60 | child: const Text("Tentar novamente")) 61 | ], 62 | ), 63 | ); 64 | } 65 | 66 | Expanded buildUserList(List list) { 67 | return Expanded( 68 | child: ListView.builder( 69 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), 70 | itemCount: list.length, 71 | itemBuilder: (BuildContext context, int index) { 72 | return ExpansionTile( 73 | leading: const FlutterLogo(), 74 | title: Text(list[index].title!), children: [ 75 | Text(list[index].body!, style: const TextStyle(),) 76 | ],); 77 | }, 78 | ), 79 | ); 80 | } 81 | 82 | Expanded buildShimmerEffect() { 83 | return Expanded( 84 | child: ListView.builder( 85 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), 86 | itemCount: 20, 87 | itemBuilder: (BuildContext context, int index) { 88 | return ListTile( 89 | title: SizedBox( 90 | height: 30, 91 | child: Shimmer.fromColors( 92 | baseColor: Colors.grey[300]!, 93 | highlightColor: Colors.grey[100]!, 94 | child: Container( 95 | color: Colors.grey[300], 96 | width: double.infinity, 97 | child: const Text( 98 | '', 99 | textAlign: TextAlign.center, 100 | style: TextStyle( 101 | fontSize: 40.0, 102 | fontWeight: FontWeight.bold, 103 | ), 104 | ), 105 | ), 106 | ), 107 | ), 108 | ); 109 | }, 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/ui/post/binding/post_example_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import '../../../remote/repository/app_repository.dart'; 4 | import '../../../remote/repository/app_repository_impl.dart'; 5 | import '../../controller/example_view_model.dart'; 6 | 7 | class PostExampleBinding extends Bindings { 8 | @override 9 | void dependencies() { 10 | Get.put(AppRepositoryImpl()); 11 | Get.put(ExampleViewModel(api: Get.find())); 12 | } 13 | } -------------------------------------------------------------------------------- /lib/ui/post/post_example_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mvvm_template/remote/response/api_response.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import '../../remote/model/base_model.dart'; 6 | import '../controller/example_view_model.dart'; 7 | 8 | class PostExampleScreen extends StatefulWidget { 9 | const PostExampleScreen({super.key}); 10 | 11 | @override 12 | State createState() => _PostExampleScreenState(); 13 | } 14 | 15 | class _PostExampleScreenState extends State { 16 | 17 | ExampleViewModel viewModel = Get.find(); 18 | 19 | TextEditingController userIdController = TextEditingController(); 20 | TextEditingController idController = TextEditingController(); 21 | TextEditingController titleController = TextEditingController(); 22 | TextEditingController bodyController = TextEditingController(); 23 | 24 | final formKey = GlobalKey(); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | title: const Text("post example"), 31 | ), 32 | body: Padding( 33 | padding: const EdgeInsets.all(8.0), 34 | child: Form( 35 | key: formKey, 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.stretch, 38 | children: [ 39 | TextFormField( 40 | validator: (s) { 41 | if (s!.isEmpty) { 42 | return "Preencha o campo"; 43 | } 44 | return null; 45 | }, 46 | keyboardType: TextInputType.number, 47 | controller: userIdController, 48 | decoration: const InputDecoration( 49 | hintText: "userId", 50 | ), 51 | ), 52 | TextFormField( 53 | validator: (s) { 54 | if (s!.isEmpty) { 55 | return "Preencha o campo"; 56 | } 57 | return null; 58 | }, 59 | controller: titleController, 60 | decoration: const InputDecoration( 61 | hintText: "title", 62 | ), 63 | ), 64 | TextFormField( 65 | validator: (s) { 66 | if (s!.isEmpty) { 67 | return "Preencha o campo"; 68 | } 69 | return null; 70 | }, 71 | controller: bodyController, 72 | decoration: const InputDecoration( 73 | hintText: "body", 74 | ), 75 | ), 76 | Padding( 77 | padding: const EdgeInsets.all(8.0), 78 | child: ElevatedButton( 79 | onPressed: () { 80 | if (formKey.currentState!.validate()) { 81 | viewModel.sendPost( 82 | model: BaseModel( 83 | userId: int.parse(userIdController.text), 84 | title: titleController.text.trim(), 85 | body: bodyController.text.trim())); 86 | } 87 | }, 88 | child: const Text("ENVIAR")), 89 | ), 90 | Obx(() { 91 | return viewModel.postResponse.value.when( 92 | loading: () => Container(), 93 | success: (data) => buildPostSuccess(data), 94 | error: (message) => const Text( 95 | "Ocorreu algum erro ao realizar o post, tente novamente."), 96 | ); 97 | }) 98 | ], 99 | ), 100 | ), 101 | ), 102 | ); 103 | } 104 | 105 | Widget buildPostSuccess(BaseModel baseModel) { 106 | return Padding( 107 | padding: const EdgeInsets.all(8.0), 108 | child: Column( 109 | mainAxisAlignment: MainAxisAlignment.start, 110 | crossAxisAlignment: CrossAxisAlignment.start, 111 | children: [ 112 | const Text("Resposta do post: ", style: TextStyle(fontWeight: FontWeight.bold),), 113 | const SizedBox(height: 4,), 114 | Column( 115 | crossAxisAlignment: CrossAxisAlignment.start, 116 | children: [ 117 | Text("userID: ${baseModel.userId}"), 118 | Text("ID do post: ${baseModel.id}"), 119 | Text("Título: ${baseModel.title}"), 120 | Text("Body: ${baseModel.body}"), 121 | ], 122 | ), 123 | ], 124 | ), 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "82.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "7.4.5" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.7.0" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.12.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.1.2" 44 | build: 45 | dependency: transitive 46 | description: 47 | name: build 48 | sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.4.2" 52 | build_config: 53 | dependency: transitive 54 | description: 55 | name: build_config 56 | sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.1.2" 60 | build_daemon: 61 | dependency: transitive 62 | description: 63 | name: build_daemon 64 | sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "4.0.4" 68 | build_resolvers: 69 | dependency: transitive 70 | description: 71 | name: build_resolvers 72 | sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "2.4.4" 76 | build_runner: 77 | dependency: "direct dev" 78 | description: 79 | name: build_runner 80 | sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.4.15" 84 | build_runner_core: 85 | dependency: transitive 86 | description: 87 | name: build_runner_core 88 | sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "8.0.0" 92 | built_collection: 93 | dependency: transitive 94 | description: 95 | name: built_collection 96 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "5.1.1" 100 | built_value: 101 | dependency: transitive 102 | description: 103 | name: built_value 104 | sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "8.10.1" 108 | characters: 109 | dependency: transitive 110 | description: 111 | name: characters 112 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.4.0" 116 | checked_yaml: 117 | dependency: transitive 118 | description: 119 | name: checked_yaml 120 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.0.3" 124 | clock: 125 | dependency: transitive 126 | description: 127 | name: clock 128 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.1.2" 132 | code_builder: 133 | dependency: transitive 134 | description: 135 | name: code_builder 136 | sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "4.10.1" 140 | collection: 141 | dependency: transitive 142 | description: 143 | name: collection 144 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.19.1" 148 | convert: 149 | dependency: transitive 150 | description: 151 | name: convert 152 | sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "3.1.2" 156 | crypto: 157 | dependency: transitive 158 | description: 159 | name: crypto 160 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "3.0.6" 164 | cupertino_icons: 165 | dependency: "direct main" 166 | description: 167 | name: cupertino_icons 168 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.0.8" 172 | dart_style: 173 | dependency: transitive 174 | description: 175 | name: dart_style 176 | sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "3.1.0" 180 | dio: 181 | dependency: "direct main" 182 | description: 183 | name: dio 184 | sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "5.8.0+1" 188 | dio_web_adapter: 189 | dependency: transitive 190 | description: 191 | name: dio_web_adapter 192 | sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.1.1" 196 | fake_async: 197 | dependency: transitive 198 | description: 199 | name: fake_async 200 | sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "1.3.2" 204 | file: 205 | dependency: transitive 206 | description: 207 | name: file 208 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "7.0.1" 212 | fixnum: 213 | dependency: transitive 214 | description: 215 | name: fixnum 216 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.1.1" 220 | flutter: 221 | dependency: "direct main" 222 | description: flutter 223 | source: sdk 224 | version: "0.0.0" 225 | flutter_lints: 226 | dependency: "direct dev" 227 | description: 228 | name: flutter_lints 229 | sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 230 | url: "https://pub.dev" 231 | source: hosted 232 | version: "2.0.3" 233 | flutter_test: 234 | dependency: "direct dev" 235 | description: flutter 236 | source: sdk 237 | version: "0.0.0" 238 | freezed: 239 | dependency: "direct dev" 240 | description: 241 | name: freezed 242 | sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c" 243 | url: "https://pub.dev" 244 | source: hosted 245 | version: "3.0.6" 246 | freezed_annotation: 247 | dependency: "direct main" 248 | description: 249 | name: freezed_annotation 250 | sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b 251 | url: "https://pub.dev" 252 | source: hosted 253 | version: "3.0.0" 254 | frontend_server_client: 255 | dependency: transitive 256 | description: 257 | name: frontend_server_client 258 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 259 | url: "https://pub.dev" 260 | source: hosted 261 | version: "4.0.0" 262 | get: 263 | dependency: "direct main" 264 | description: 265 | name: get 266 | sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 267 | url: "https://pub.dev" 268 | source: hosted 269 | version: "4.7.2" 270 | glob: 271 | dependency: transitive 272 | description: 273 | name: glob 274 | sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de 275 | url: "https://pub.dev" 276 | source: hosted 277 | version: "2.1.3" 278 | graphs: 279 | dependency: transitive 280 | description: 281 | name: graphs 282 | sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" 283 | url: "https://pub.dev" 284 | source: hosted 285 | version: "2.3.2" 286 | http: 287 | dependency: transitive 288 | description: 289 | name: http 290 | sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" 291 | url: "https://pub.dev" 292 | source: hosted 293 | version: "1.4.0" 294 | http_multi_server: 295 | dependency: transitive 296 | description: 297 | name: http_multi_server 298 | sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 299 | url: "https://pub.dev" 300 | source: hosted 301 | version: "3.2.2" 302 | http_parser: 303 | dependency: transitive 304 | description: 305 | name: http_parser 306 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 307 | url: "https://pub.dev" 308 | source: hosted 309 | version: "4.1.2" 310 | io: 311 | dependency: transitive 312 | description: 313 | name: io 314 | sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b 315 | url: "https://pub.dev" 316 | source: hosted 317 | version: "1.0.5" 318 | js: 319 | dependency: transitive 320 | description: 321 | name: js 322 | sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" 323 | url: "https://pub.dev" 324 | source: hosted 325 | version: "0.7.2" 326 | json_annotation: 327 | dependency: "direct main" 328 | description: 329 | name: json_annotation 330 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 331 | url: "https://pub.dev" 332 | source: hosted 333 | version: "4.9.0" 334 | json_serializable: 335 | dependency: "direct dev" 336 | description: 337 | name: json_serializable 338 | sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c 339 | url: "https://pub.dev" 340 | source: hosted 341 | version: "6.9.5" 342 | leak_tracker: 343 | dependency: transitive 344 | description: 345 | name: leak_tracker 346 | sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec 347 | url: "https://pub.dev" 348 | source: hosted 349 | version: "10.0.8" 350 | leak_tracker_flutter_testing: 351 | dependency: transitive 352 | description: 353 | name: leak_tracker_flutter_testing 354 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 355 | url: "https://pub.dev" 356 | source: hosted 357 | version: "3.0.9" 358 | leak_tracker_testing: 359 | dependency: transitive 360 | description: 361 | name: leak_tracker_testing 362 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 363 | url: "https://pub.dev" 364 | source: hosted 365 | version: "3.0.1" 366 | lints: 367 | dependency: transitive 368 | description: 369 | name: lints 370 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 371 | url: "https://pub.dev" 372 | source: hosted 373 | version: "2.1.1" 374 | logging: 375 | dependency: transitive 376 | description: 377 | name: logging 378 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 379 | url: "https://pub.dev" 380 | source: hosted 381 | version: "1.3.0" 382 | matcher: 383 | dependency: transitive 384 | description: 385 | name: matcher 386 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 387 | url: "https://pub.dev" 388 | source: hosted 389 | version: "0.12.17" 390 | material_color_utilities: 391 | dependency: transitive 392 | description: 393 | name: material_color_utilities 394 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 395 | url: "https://pub.dev" 396 | source: hosted 397 | version: "0.11.1" 398 | meta: 399 | dependency: transitive 400 | description: 401 | name: meta 402 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 403 | url: "https://pub.dev" 404 | source: hosted 405 | version: "1.16.0" 406 | mime: 407 | dependency: transitive 408 | description: 409 | name: mime 410 | sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" 411 | url: "https://pub.dev" 412 | source: hosted 413 | version: "2.0.0" 414 | mockito: 415 | dependency: "direct dev" 416 | description: 417 | name: mockito 418 | sha256: "4546eac99e8967ea91bae633d2ca7698181d008e95fa4627330cf903d573277a" 419 | url: "https://pub.dev" 420 | source: hosted 421 | version: "5.4.6" 422 | package_config: 423 | dependency: transitive 424 | description: 425 | name: package_config 426 | sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc 427 | url: "https://pub.dev" 428 | source: hosted 429 | version: "2.2.0" 430 | path: 431 | dependency: transitive 432 | description: 433 | name: path 434 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 435 | url: "https://pub.dev" 436 | source: hosted 437 | version: "1.9.1" 438 | pool: 439 | dependency: transitive 440 | description: 441 | name: pool 442 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 443 | url: "https://pub.dev" 444 | source: hosted 445 | version: "1.5.1" 446 | pub_semver: 447 | dependency: transitive 448 | description: 449 | name: pub_semver 450 | sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" 451 | url: "https://pub.dev" 452 | source: hosted 453 | version: "2.2.0" 454 | pubspec_parse: 455 | dependency: transitive 456 | description: 457 | name: pubspec_parse 458 | sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" 459 | url: "https://pub.dev" 460 | source: hosted 461 | version: "1.5.0" 462 | shelf: 463 | dependency: transitive 464 | description: 465 | name: shelf 466 | sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 467 | url: "https://pub.dev" 468 | source: hosted 469 | version: "1.4.2" 470 | shelf_web_socket: 471 | dependency: transitive 472 | description: 473 | name: shelf_web_socket 474 | sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" 475 | url: "https://pub.dev" 476 | source: hosted 477 | version: "3.0.0" 478 | shimmer: 479 | dependency: "direct main" 480 | description: 481 | name: shimmer 482 | sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" 483 | url: "https://pub.dev" 484 | source: hosted 485 | version: "3.0.0" 486 | sky_engine: 487 | dependency: transitive 488 | description: flutter 489 | source: sdk 490 | version: "0.0.0" 491 | source_gen: 492 | dependency: transitive 493 | description: 494 | name: source_gen 495 | sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" 496 | url: "https://pub.dev" 497 | source: hosted 498 | version: "2.0.0" 499 | source_helper: 500 | dependency: transitive 501 | description: 502 | name: source_helper 503 | sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" 504 | url: "https://pub.dev" 505 | source: hosted 506 | version: "1.3.5" 507 | source_span: 508 | dependency: transitive 509 | description: 510 | name: source_span 511 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 512 | url: "https://pub.dev" 513 | source: hosted 514 | version: "1.10.1" 515 | stack_trace: 516 | dependency: transitive 517 | description: 518 | name: stack_trace 519 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 520 | url: "https://pub.dev" 521 | source: hosted 522 | version: "1.12.1" 523 | stream_channel: 524 | dependency: transitive 525 | description: 526 | name: stream_channel 527 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 528 | url: "https://pub.dev" 529 | source: hosted 530 | version: "2.1.4" 531 | stream_transform: 532 | dependency: transitive 533 | description: 534 | name: stream_transform 535 | sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 536 | url: "https://pub.dev" 537 | source: hosted 538 | version: "2.1.1" 539 | string_scanner: 540 | dependency: transitive 541 | description: 542 | name: string_scanner 543 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 544 | url: "https://pub.dev" 545 | source: hosted 546 | version: "1.4.1" 547 | term_glyph: 548 | dependency: transitive 549 | description: 550 | name: term_glyph 551 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 552 | url: "https://pub.dev" 553 | source: hosted 554 | version: "1.2.2" 555 | test_api: 556 | dependency: transitive 557 | description: 558 | name: test_api 559 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 560 | url: "https://pub.dev" 561 | source: hosted 562 | version: "0.7.4" 563 | timing: 564 | dependency: transitive 565 | description: 566 | name: timing 567 | sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" 568 | url: "https://pub.dev" 569 | source: hosted 570 | version: "1.0.2" 571 | typed_data: 572 | dependency: transitive 573 | description: 574 | name: typed_data 575 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 576 | url: "https://pub.dev" 577 | source: hosted 578 | version: "1.4.0" 579 | vector_math: 580 | dependency: transitive 581 | description: 582 | name: vector_math 583 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 584 | url: "https://pub.dev" 585 | source: hosted 586 | version: "2.1.4" 587 | vm_service: 588 | dependency: transitive 589 | description: 590 | name: vm_service 591 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" 592 | url: "https://pub.dev" 593 | source: hosted 594 | version: "14.3.1" 595 | watcher: 596 | dependency: transitive 597 | description: 598 | name: watcher 599 | sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" 600 | url: "https://pub.dev" 601 | source: hosted 602 | version: "1.1.1" 603 | web: 604 | dependency: transitive 605 | description: 606 | name: web 607 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 608 | url: "https://pub.dev" 609 | source: hosted 610 | version: "1.1.1" 611 | web_socket: 612 | dependency: transitive 613 | description: 614 | name: web_socket 615 | sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" 616 | url: "https://pub.dev" 617 | source: hosted 618 | version: "1.0.1" 619 | web_socket_channel: 620 | dependency: transitive 621 | description: 622 | name: web_socket_channel 623 | sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 624 | url: "https://pub.dev" 625 | source: hosted 626 | version: "3.0.3" 627 | yaml: 628 | dependency: transitive 629 | description: 630 | name: yaml 631 | sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce 632 | url: "https://pub.dev" 633 | source: hosted 634 | version: "3.1.3" 635 | sdks: 636 | dart: ">=3.7.2 <4.0.0" 637 | flutter: ">=3.18.0-18.0.pre.54" 638 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_mvvm_template 2 | description: A new Flutter project. 3 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ^3.7.2 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | cupertino_icons: ^1.0.2 15 | get: ^4.7.2 16 | dio: ^5.8.0+1 17 | shimmer: ^3.0.0 18 | freezed_annotation: ^3.0.0 19 | json_annotation: ^4.9.0 20 | 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | mockito: ^5.4.6 26 | build_runner: ^2.4.15 27 | json_serializable: ^6.9.5 28 | freezed: ^3.0.6 29 | 30 | flutter_lints: ^2.0.0 31 | 32 | flutter: 33 | uses-material-design: true 34 | 35 | # To add assets to your application, add an assets section, like this: 36 | # assets: 37 | # - images/a_dot_burr.jpeg 38 | # - images/a_dot_ham.jpeg 39 | 40 | # An image asset can refer to one or more resolution-specific "variants", see 41 | # https://flutter.dev/assets-and-images/#resolution-aware 42 | 43 | # For details regarding adding assets from package dependencies, see 44 | # https://flutter.dev/assets-and-images/#from-packages 45 | 46 | # To add custom fonts to your application, add a fonts section here, 47 | # in this "flutter" section. Each entry in this list should have a 48 | # "family" key with the font family name, and a "fonts" key with a 49 | # list giving the asset and other descriptors for the font. For 50 | # example: 51 | # fonts: 52 | # - family: Schyler 53 | # fonts: 54 | # - asset: fonts/Schyler-Regular.ttf 55 | # - asset: fonts/Schyler-Italic.ttf 56 | # style: italic 57 | # - family: Trajan Pro 58 | # fonts: 59 | # - asset: fonts/TrajanPro.ttf 60 | # - asset: fonts/TrajanPro_Bold.ttf 61 | # weight: 700 62 | # 63 | # For details regarding fonts from package dependencies, 64 | # see https://flutter.dev/custom-fonts/#from-packages 65 | -------------------------------------------------------------------------------- /test/example_view_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:flutter_mvvm_template/remote/model/base_model.dart'; 4 | import 'package:flutter_mvvm_template/remote/response/api_response.dart'; 5 | import 'package:flutter_mvvm_template/ui/controller/example_view_model.dart'; 6 | import 'mocks.mocks.dart'; 7 | 8 | void main() { 9 | late MockAppRepository mockRepository; 10 | late ExampleViewModel viewModel; 11 | 12 | setUp(() { 13 | mockRepository = MockAppRepository(); 14 | viewModel = ExampleViewModel(api: mockRepository); 15 | viewModel.getResponse.value = const ApiResponse.loading(); 16 | }); 17 | 18 | test( 19 | 'Quando getDataExample retorna sucesso, getResponse.value deve ser Success com a lista', 20 | () async { 21 | 22 | final listaFake = [ 23 | BaseModel(id: 1, title: 'Título 1', body: 'Corpo 1'), 24 | BaseModel(id: 2, title: 'Título 2', body: 'Corpo 2'), 25 | ]; 26 | 27 | when(mockRepository.getDataExample()) 28 | .thenAnswer((_) async => ApiResponse.success(data: listaFake)); 29 | 30 | await viewModel.getPosts(); 31 | 32 | final estado = viewModel.getResponse.value; 33 | expect(estado, isA>>()); 34 | 35 | if (estado is Success>) { 36 | expect(estado.data, listaFake); 37 | expect(estado.data.length, 2); 38 | expect(estado.data[0].title, 'Título 1'); 39 | } 40 | }, 41 | ); 42 | 43 | test( 44 | 'Quando getDataExample retorna erro, getResponse.value deve ser Error com mensagem', 45 | () async { 46 | 47 | const msgErro = 'Falha de rede'; 48 | 49 | when(mockRepository.getDataExample()) 50 | .thenAnswer((_) async => const ApiResponse.error(message: msgErro)); 51 | 52 | await viewModel.getPosts(); 53 | 54 | final estado = viewModel.getResponse.value; 55 | expect(estado, isA>>()); 56 | 57 | if (estado is Error>) { 58 | expect(estado.message, msgErro); 59 | } 60 | }, 61 | ); 62 | 63 | } 64 | -------------------------------------------------------------------------------- /test/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/annotations.dart'; 2 | import 'package:flutter_mvvm_template/remote/repository/app_repository.dart'; 3 | 4 | @GenerateMocks([ 5 | AppRepository, 6 | ]) 7 | void main() {} --------------------------------------------------------------------------------