├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── ouday │ │ │ │ └── nyt │ │ │ │ └── nyt_flutter │ │ │ │ └── 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 ├── bitrise.yml ├── integration_test ├── article_detail │ └── presentation │ │ └── screen │ │ └── article_detail_screen_should.dart └── articles_list │ └── presentation │ └── screen │ └── article_list_screen_should.dart ├── 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 ├── article_detail │ └── presentation │ │ └── screen │ │ └── article_detail.dart ├── articles_list │ ├── data │ │ ├── model │ │ │ ├── article.dart │ │ │ ├── article.g.dart │ │ │ ├── most_popular_response.dart │ │ │ └── most_popular_response.g.dart │ │ ├── remote │ │ │ ├── service │ │ │ │ ├── article_service.dart │ │ │ │ └── article_service.g.dart │ │ │ └── source │ │ │ │ ├── article_remote_data_source.dart │ │ │ │ └── article_remote_data_source_impl.dart │ │ └── repository │ │ │ └── article_repo_impl.dart │ ├── domain │ │ ├── repository │ │ │ └── article_repo.dart │ │ └── usecase │ │ │ ├── article_usecase.dart │ │ │ └── article_usecase_impl.dart │ └── presentation │ │ ├── bloc │ │ ├── article_list_bloc.dart │ │ ├── article_list_bloc.freezed.dart │ │ ├── article_list_event.dart │ │ └── article_list_state.dart │ │ ├── screen │ │ └── articles_list_screen.dart │ │ └── widget │ │ └── article_list_item.dart ├── common │ └── constant.dart ├── core │ ├── error.dart │ └── error.freezed.dart ├── di │ ├── app_module.dart │ ├── di_setup.config.dart │ └── di_setup.dart └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── readme_res ├── bitrise-workflows.png ├── bitrise.png ├── code_coverage.png ├── flutter_test.png ├── integration_test_article_detail_screen.png ├── integration_test_article_list_screen.png ├── lint.png └── nyt-flutter-emulator.gif ├── test └── unit-tests │ └── articles_list │ ├── data │ ├── remote │ │ ├── article_service_impl_test.dart │ │ └── article_service_impl_test.mocks.dart │ └── repository │ │ ├── article_repo_impl_test.dart │ │ └── article_repo_impl_test.mocks.dart │ └── domain │ ├── article_usecase_impl_test.dart │ └── article_usecase_impl_test.mocks.dart └── test_with_coverage.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: 18116933e77adc82f80866c928266a5b4f1ed645 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table of Content 2 | - [News Reader](#news-reader) 3 | * [Requirements](#requirements) 4 | * [API Key](#api-key) 5 | - [App Architecture](#app-architecture) 6 | * [Model/Entities](#model-entities) 7 | * [Data layer](#data-layer) 8 | * [Remote data source](#remote-data-source) 9 | * [Local data source](#local-data-source) 10 | * [Repository](#repository) 11 | * [Usecases/Interactors](#usecases-interactors) 12 | * [BLoC](#bloc) 13 | * [Bloc](#bloc) 14 | * [BlocBuilder](#blocbuilder) 15 | * [BlocProvider](#blocprovider) 16 | - [Getting Started](#getting-started) 17 | * [Checkout the Code](#checkout-the-code) 18 | * [Major Libraries / Tools](#major-libraries---tools) 19 | - [Setting up Prerequisites](#setting-up-prerequisites) 20 | * [Install LCOV](#install-lcov) 21 | * [Install scrcpy](#install-scrcpy) 22 | * [Generate files](#generate-files) 23 | - [Running Quality Gates and Deployment Commands](#running-quality-gates-and-deployment-commands) 24 | * [Linting](#linting) 25 | * [Testing](#testing) 26 | * [Tests](#tests) 27 | * [Integration tests](#integration-tests) 28 | + [Running the Unit Tests](#running-the-unit-tests) 29 | * [Test Coverage](#test-coverage) 30 | - [CI-CD - Build via Bitrise (yml file)](#ci-cd---build-via-bitrise--yml-file-) 31 | * [Building the application using Bitrise](#building-the-application-using-bitrise) 32 | - [License](#license) 33 | 34 | # News Reader 35 | News Reader is simple flutter app to hit the NY Times Most Popular Articles API and show a list of articles, that shows details when items on the list are tapped (a typical master/detail app). 36 | 37 | We'll be using the most viewed section of this API. 38 | 39 | [http://api.nytimes.com/svc/mostpopular/v2/mostviewed/{section}/{period}.json?api-key=sample-key](http://api.nytimes.com/svc/mostpopular/v2/mostviewed/{section}/{period}.json?api-key=sample-key) 40 | To test this API, you can use all-sections for the section path component in the URL above and 7 for period (available period values are 1, 7 and 30, which represents how far back, in days, the API returns results for). 41 | 42 | [http://api.nytimes.com/svc/mostpopular/v2/mostviewed/all-sections/7.json?api-key=sample-key](http://api.nytimes.com/svc/mostpopular/v2/mostviewed/all-sections/7.json?api-key=sample-key) 43 | 44 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/nyt-flutter-emulator.gif?raw=true) 45 | ## Requirements 46 | - Android Studio 47 | - Flutter SDK 2.8.1 48 | 49 | ## API Key 50 | 51 | An API key is necessary to successfully connect to the [API](https://developer.nytimes.com/signup) that the app uses. Once an API key has been aquired, change the `API_KEY` property in `/nyt_flutter/lib/common/contant.dart` and run the app. 52 | 53 | # App Architecture 54 | 55 | This sample follows BLoC pattern + Clean Architecture. 56 | 57 | ## Model/Entities 58 | The model is the domain object. It represents the actual data and/or information we are dealing with. An example of a model might be a contact (containing name, phone number, address, etc) or the characteristics of a live streaming publishing point. 59 | 60 | The key to remember with the model is that it holds the information, but not behaviors or services that manipulate the information. It is not responsible for formatting text to look pretty on the screen, or fetching a list of items from a remote server (in fact, in that list, each item would most likely be a model of its own). Business logic is typically kept separate from the model, and encapsulated in other classes that act on the model. 61 | 62 | ## Data layer 63 | Provides all required data to the repository in form of models/entities. 64 | ##### Remote data source 65 | Manage all server/external API calls. 66 | ##### Local data source 67 | Manage all local data storage: example SQLite implementation, Room, Realm... 68 | ##### Repository 69 | The decision maker class when it comes to manage data CRUD operations. Operations can be done in this layer is caching mechanism, manage consecutive api calls etc... 70 | 71 | ## Usecases/Interactors 72 | Represents concepts of the business, information about the current situation and business rules. 73 | 74 | ## BLoC 75 | There are three primary gadgets in the BLoC library: 76 | - Bloc 77 | - BlocBuilder 78 | - BlocProvider 79 | You’ll require them to set up BLoCs, construct those BLoCs as indicated by the progressions in the app’s state, and set up conditions. How about we perceive how to execute every gadget and use it in your app’s business rationale. 80 | ##### Bloc 81 | The Bloc gadget is the fundamental segment you’ll have to execute all business rationale. To utilize it, expand the Bloc class and supersede the mapEventToState and initialState techniques. 82 | ##### BlocBuilder 83 | BlocBuilder is a gadget that reacts to new states by building BLoCs. This gadget can be called on numerous occasions and acts like a capacity that reacts to changes in the state by making gadgets that appear new UI components. 84 | ##### BlocProvider 85 | This gadget fills in as a reliance infusion, which means it can give BLoCs to a few gadgets all at once that have a place with the equivalent subtree. BlocProvider is utilized to construct blocs that will be accessible for all gadgets in the subtree. 86 | 87 | # Getting Started 88 | 89 | This repository implements the following quality gates: 90 | 91 | ![Build Pipeline](/readme_res/bitrise.png "Build Pipeline") 92 | 93 | - Static code checks: running [lint](https://pub.dev/packages/lint) to check the code for any issues. 94 | - Unit testing: running the [unit tests](https://docs.flutter.dev/cookbook/testing/unit/introduction) 95 | - Code coverage: generating code coverage reports using the [LCOV](https://github.com/linux-test-project/lcov) 96 | - Integration testing: running the functional tests using [Flutter Integration Testing](https://docs.flutter.dev/cookbook/testing/integration/introduction) 97 | 98 | These steps can be run manually or using a Continous Integration tool such as [Bitrise](https://app.bitrise.io/). 99 | 100 | ## Checkout the Code 101 | 102 | Checkout and run the code 103 | ```bash 104 | git clone https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test.git 105 | ``` 106 | 107 | ## Major Libraries / Tools 108 | 109 | | Category | Library/Tool | Link | 110 | |--------------------------------- |---------------- |------------------------------------------------------------ | 111 | | Development | Flutter - Dart | https://flutter.dev/ | 112 | | IDE | Android Studio | https://developer.android.com/studio | 113 | | Unit Testing | Flutter Unit Test | https://docs.flutter.dev/cookbook/testing/unit/introduction | 114 | | Code Coverage | LCOV | https://github.com/linux-test-project/lcov| 115 | | Static Code Check | Lint for Dart/Flutter | https://pub.dev/packages/lint | 116 | | Integration Testing | Flutter Integration Testing | https://docs.flutter.dev/cookbook/testing/integration/introduction | 117 | | CI/CD | Bitrise | https://app.bitrise.io/ | 118 | | Dependency Injection | injectable | https://pub.dev/packages/injectable | 119 | | Service Locator | get_it | https://pub.dev/packages/get_it | 120 | | Presentation Layer Mangement | flutter_bloc | https://pub.dev/packages/flutter_bloc | 121 | | Network Layer Mangement | retrofit | https://pub.dev/packages/retrofit | 122 | | Code Generator | Freezed | https://pub.dev/packages/freezed | 123 | | HTTP Client | Dio | https://pub.dev/packages/dio| 124 | | Image Caching | cached_network_image | https://pub.dev/packages/cached_network_image| 125 | | Mock Library | Mockito | https://pub.dev/packages/mockito| 126 | 127 | # Setting up Prerequisites 128 | ## Install LCOV 129 | Run the following command in terminal `sudo apt-get install lcov` 130 | ## Install scrcpy 131 | Run the following command in terminal `sudo apt install scrcpy` 132 | ## Generate files 133 | Run the following command in terminal `flutter pub run build_runner watch --delete-conflicting-outputs` 134 | # Running Quality Gates and Deployment Commands 135 | ## Linting 136 | 137 | Run the following command in terminal `flutter analyze` 138 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/lint.png?raw=true) 139 | ## Testing 140 | Tests in Flutter are separated into 2 types: 141 | 142 | ##### Tests 143 | 144 | Located at `/test` - These are tests that run on your machine. Use these tests to minimize execution time when your tests have no flutter framework dependencies or when you can mock the flutter framework dependencies. 145 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/flutter_test.png?raw=true) 146 | ##### Integration tests 147 | 148 | Located at `/integration_test` - These are tests that run on a hardware device or emulator. These tests have access to all flutter APIs, give you access to information such as the Context of the app you are testing, and let you control the app under test from your test code. Use these tests when writing integration and functional UI tests to automate user interaction, or when your tests have flutter dependencies that mock objects cannot satisfy. 149 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/integration_test_article_list_screen.png?raw=true) 150 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/integration_test_article_detail_screen.png?raw=true) 151 | ### Running the Unit Tests 152 | 153 | Unit testing for Flutter applications is fully explained in the [Flutter documentation](https://docs.flutter.dev/cookbook/testing/unit/introduction). In this repository, 154 | From Android Studio 155 | 156 | * Right Clicking on the Class and select "Run 157 | * To see the coverage we have t the select "Run with Coverage" 158 | 159 | ## Test Coverage 160 | 161 | The test coverage uses the [LCOV](https://github.com/linux-test-project/lcov) library 162 | In order to run both `test` and `integration_test` and generate a code coverage report, create a script file to do the job. 163 | ``` 164 | 165 | red=$(tput setaf 1) 166 | none=$(tput sgr0) 167 | filename= 168 | open_browser= 169 | 170 | show_help() { 171 | printf " 172 | Script for running all unit and widget tests with code coverage. 173 | (run it from your root Flutter's project) 174 | *Important: requires lcov 175 | Usage: 176 | $0 [--help] [--open] [--filename ]where: 177 | -o, --open Open the coverage in your browser, Default is google-chrome you can change this in the function open_cov(). -h, --help print this message -f , --filename Run a particular test file. For example: -f test/a_particular_test.dart 178 | Or you can run all tests in a directory 179 | -f test/some_directory/" 180 | } 181 | 182 | run_tests() { 183 | if [[ -f "pubspec.yaml" ]]; then 184 | rm -f coverage/lcov.info 185 | rm -f coverage/lcov-final.info 186 | flutter test --coverage "$filename" 187 | ch_dir 188 | else 189 | printf "\n${red}Error: this is not a Flutter project${none}\n" 190 | exit 1 191 | fi 192 | } 193 | 194 | run_report() { 195 | if [[ -f "coverage/lcov.info" ]]; then 196 | lcov -r coverage/lcov.info lib/resources/l10n/\* lib/\*/fake_\*.dart \ 197 | -o coverage/lcov-final.info 198 | genhtml -o coverage coverage/lcov-final.info 199 | else 200 | printf "\n${red}Error: no coverage info was generated${none}\n" 201 | exit 1 202 | fi 203 | } 204 | 205 | ch_dir(){ 206 | dir=$(pwd) 207 | input="$dir/coverage/lcov.info" 208 | output="$dir/coverage/lcov_new.info" 209 | echo "$input" 210 | while read line 211 | do 212 | secondString="SF:$dir/" 213 | echo "${line/SF:/$secondString}" >> $output 214 | done < "$input" 215 | 216 | mv $output $input 217 | } 218 | 219 | open_cov(){ 220 | # This depends on your system 221 | # Google Chrome: 222 | # google-chrome coverage/index-sort-l.html # Mozilla: firefox coverage/index-sort-l.html 223 | } 224 | 225 | while [ "$1" != "" ]; do 226 | case $1 in 227 | -h|--help) 228 | show_help 229 | exit ;; 230 | -o|--open) 231 | open_browser=1 232 | ;; 233 | -f|--filename) 234 | shift 235 | filename=$1 236 | ;; 237 | *) 238 | show_help 239 | exit ;; 240 | esac shift 241 | done 242 | 243 | run_tests 244 | remove_from_coverage -f coverage/lcov.info -r '.g.dart$' 245 | remove_from_coverage -f coverage/lcov.info -r '.freezed.dart$' 246 | remove_from_coverage -f coverage/lcov.info -r '.config.dart$' 247 | run_report 248 | if [ "$open_browser" = "1" ]; then 249 | open_cov 250 | fi 251 | ``` 252 | Below lines are added to ignore the generated files when generating the code coverage report: 253 | ``` 254 | remove_from_coverage -f coverage/lcov.info -r '.g.dart$' 255 | remove_from_coverage -f coverage/lcov.info -r '.freezed.dart$' 256 | remove_from_coverage -f coverage/lcov.info -r '.config.dart$' 257 | ``` 258 | From the commandline 259 | 260 | `sh test_with_coverage.sh` 261 | 262 | Test coverage results are available at 263 | 264 | `/coverage/index.html` 265 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/code_coverage.png?raw=true) 266 | # CI-CD - Build via Bitrise (yml file) 267 | 268 | This repo contains a [bitrise](./bitrise.yml), which is used to define a Bitrise **declarative pipeline** for CI-CD to build the code, run the quality gates, code coverage, static analysis and deploy to Bitrise. 269 | 270 | Here is the structure of the bitrise declarative pipeline: 271 | 272 | ``` 273 | --- 274 | format_version: '11' 275 | default_step_lib_source: 'https://github.com/bitrise-io/bitrise-steplib.git' 276 | project_type: flutter 277 | trigger_map: 278 | - push_branch: '*' 279 | workflow: primary 280 | - pull_request_source_branch: '*' 281 | workflow: primary 282 | workflows: 283 | deploy: 284 | steps: 285 | - activate-ssh-key@4: 286 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 287 | - git-clone@6: {} 288 | - script@1: 289 | title: Do anything with Script step 290 | - certificate-and-profile-installer@1: {} 291 | - flutter-installer@0: 292 | inputs: 293 | - is_update: 'false' 294 | - cache-pull@2: {} 295 | - flutter-analyze@0: 296 | inputs: 297 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 298 | - flutter-test@1: 299 | inputs: 300 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 301 | - flutter-build@0: 302 | inputs: 303 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 304 | - platform: both 305 | - xcode-archive@4: 306 | inputs: 307 | - project_path: $BITRISE_PROJECT_PATH 308 | - scheme: $BITRISE_SCHEME 309 | - distribution_method: $BITRISE_DISTRIBUTION_METHOD 310 | - configuration: Release 311 | - deploy-to-bitrise-io@2: {} 312 | - cache-push@2: {} 313 | primary: 314 | steps: 315 | - activate-ssh-key@4: 316 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 317 | - git-clone@6: {} 318 | - script@1: 319 | title: Do anything with Script step 320 | - flutter-installer@0: 321 | inputs: 322 | - is_update: 'false' 323 | - cache-pull@2: {} 324 | - flutter-analyze@0.3: 325 | inputs: 326 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 327 | - flutter-test@1: 328 | inputs: 329 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 330 | - deploy-to-bitrise-io@2: {} 331 | - cache-push@2: {} 332 | meta: 333 | bitrise.io: 334 | stack: linux-docker-android-20.04 335 | machine_type_id: elite 336 | app: 337 | envs: 338 | - opts: 339 | is_expand: false 340 | BITRISE_FLUTTER_PROJECT_LOCATION: . 341 | - opts: 342 | is_expand: false 343 | BITRISE_PROJECT_PATH: . 344 | - opts: 345 | is_expand: false 346 | BITRISE_SCHEME: . 347 | - opts: 348 | is_expand: false 349 | BITRISE_DISTRIBUTION_METHOD: development 350 | ``` 351 | 352 | Below is an illustration of the pipeline that Bitrise will execute 353 | 354 | ![alt text](https://github.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/blob/master/readme_res/bitrise-workflows.png?raw=true) 355 | ## Building the application using Bitrise 356 | 357 | These steps should be followed to automated the app build using Bitrise: 358 | 359 | - Create an account on Bitrise. 360 | - Follow the wizard for creating a Flutter project on Bitrise. 361 | - In `workflows` tab, and select `<>bitrise.yaml` tab. 362 | - Choose `Store in app repository` to read the repository yaml file. 363 | 364 | #### This repository already attached to a [public bitrise project](https://app.bitrise.io/app/1ba1887b850ddd8a#). 365 | 366 | # License 367 | 368 | Apache License, Version 2.0 369 | 370 | http://www.apache.org/licenses/LICENSE-2.0 371 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/main/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 15 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/packages/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | strong-mode: 23 | implicit-casts: false 24 | implicit-dynamic: false 25 | errors: 26 | # treat missing required parameters as a warning (not a hint) 27 | missing_required_param: warning 28 | # treat missing returns as a warning (not a hint) 29 | missing_return: warning 30 | # allow having TODO comments in the code 31 | todo: ignore 32 | # allow self-reference to deprecated members (we do this because otherwise we have 33 | # to annotate every member in every test, assert, etc, when we deprecate something) 34 | deprecated_member_use_from_same_package: ignore 35 | # TODO(ianh): https://github.com/flutter/flutter/issues/74381 36 | # Clean up existing unnecessary imports, and remove line to ignore. 37 | unnecessary_import: ignore 38 | # Turned off until null-safe rollout is complete. 39 | unnecessary_null_comparison: ignore 40 | exclude: 41 | - "bin/cache/**" 42 | # Ignore protoc generated files 43 | - "dev/conductor/lib/proto/*" 44 | # Ignore generate files 45 | - "**/*.g.dart" 46 | - "**/*.config.dart" 47 | - "**/*.freezed.dart" 48 | - "**/*.mocks.dart" 49 | 50 | linter: 51 | rules: 52 | # these rules are documented on and in the same order as 53 | # the Dart Lint rules page to make maintenance easier 54 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 55 | - always_declare_return_types 56 | - always_put_control_body_on_new_line 57 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 58 | - always_require_non_null_named_parameters 59 | - always_specify_types 60 | # - always_use_package_imports # we do this commonly 61 | - annotate_overrides 62 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 63 | - avoid_bool_literals_in_conditional_expressions 64 | # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 65 | # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 66 | - avoid_classes_with_only_static_members 67 | - avoid_double_and_int_checks 68 | - avoid_dynamic_calls 69 | - avoid_empty_else 70 | - avoid_equals_and_hash_code_on_mutable_classes 71 | - avoid_escaping_inner_quotes 72 | - avoid_field_initializers_in_const_classes 73 | - avoid_function_literals_in_foreach_calls 74 | - avoid_implementing_value_types 75 | - avoid_init_to_null 76 | - avoid_js_rounded_ints 77 | # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to 78 | - avoid_null_checks_in_equality_operators 79 | # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it 80 | - avoid_print 81 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 82 | - avoid_redundant_argument_values 83 | - avoid_relative_lib_imports 84 | - avoid_renaming_method_parameters 85 | - avoid_return_types_on_setters 86 | # - avoid_returning_null # still violated by some pre-nnbd code that we haven't yet migrated 87 | - avoid_returning_null_for_future 88 | - avoid_returning_null_for_void 89 | # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives 90 | - avoid_setters_without_getters 91 | - avoid_shadowing_type_parameters 92 | - avoid_single_cascade_in_expression_statements 93 | - avoid_slow_async_io 94 | - avoid_type_to_string 95 | - avoid_types_as_parameter_names 96 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 97 | - avoid_unnecessary_containers 98 | - avoid_unused_constructor_parameters 99 | - avoid_void_async 100 | # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere 101 | - await_only_futures 102 | - camel_case_extensions 103 | - camel_case_types 104 | - cancel_subscriptions 105 | # - cascade_invocations # doesn't match the typical style of this repo 106 | - cast_nullable_to_non_nullable 107 | # - close_sinks # not reliable enough 108 | # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 109 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 110 | - control_flow_in_finally 111 | # - curly_braces_in_flow_control_structures # not required by flutter style 112 | - depend_on_referenced_packages 113 | - deprecated_consistency 114 | # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) 115 | - directives_ordering 116 | # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic 117 | - empty_catches 118 | - empty_constructor_bodies 119 | - empty_statements 120 | - eol_at_end_of_file 121 | - exhaustive_cases 122 | - file_names 123 | - flutter_style_todos 124 | - hash_and_equals 125 | - implementation_imports 126 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 127 | - iterable_contains_unrelated_type 128 | # - join_return_with_assignment # not required by flutter style 129 | - leading_newlines_in_multiline_strings 130 | - library_names 131 | - library_prefixes 132 | - library_private_types_in_public_api 133 | # - lines_longer_than_80_chars # not required by flutter style 134 | - list_remove_unrelated_type 135 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 136 | - missing_whitespace_between_adjacent_strings 137 | - no_adjacent_strings_in_list 138 | - no_default_cases 139 | - no_duplicate_case_values 140 | - no_logic_in_create_state 141 | # - no_runtimeType_toString # ok in tests; we enable this only in packages/ 142 | - non_constant_identifier_names 143 | - noop_primitive_operations 144 | - null_check_on_nullable_type_parameter 145 | - null_closures 146 | # - omit_local_variable_types # opposite of always_specify_types 147 | # - one_member_abstracts # too many false positives 148 | - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al 149 | - overridden_fields 150 | - package_api_docs 151 | - package_names 152 | - package_prefixed_library_names 153 | # - parameter_assignments # we do this commonly 154 | - prefer_adjacent_string_concatenation 155 | - prefer_asserts_in_initializer_lists 156 | # - prefer_asserts_with_message # not required by flutter style 157 | - prefer_collection_literals 158 | - prefer_conditional_assignment 159 | - prefer_const_constructors 160 | - prefer_const_constructors_in_immutables 161 | - prefer_const_declarations 162 | - prefer_const_literals_to_create_immutables 163 | # - prefer_constructors_over_static_methods # far too many false positives 164 | - prefer_contains 165 | # - prefer_double_quotes # opposite of prefer_single_quotes 166 | - prefer_equal_for_default_values 167 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 168 | - prefer_final_fields 169 | - prefer_final_in_for_each 170 | - prefer_final_locals 171 | # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments 172 | - prefer_for_elements_to_map_fromIterable 173 | - prefer_foreach 174 | - prefer_function_declarations_over_variables 175 | - prefer_generic_function_type_aliases 176 | - prefer_if_elements_to_conditional_expressions 177 | - prefer_if_null_operators 178 | - prefer_initializing_formals 179 | - prefer_inlined_adds 180 | # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants 181 | - prefer_interpolation_to_compose_strings 182 | - prefer_is_empty 183 | - prefer_is_not_empty 184 | - prefer_is_not_operator 185 | - prefer_iterable_whereType 186 | # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 187 | - prefer_null_aware_operators 188 | # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere 189 | - prefer_relative_imports 190 | - prefer_single_quotes 191 | - prefer_spread_collections 192 | - prefer_typing_uninitialized_variables 193 | - prefer_void_to_null 194 | - provide_deprecation_message 195 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 196 | - recursive_getters 197 | # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 198 | - sized_box_for_whitespace 199 | - slash_for_doc_comments 200 | - sort_child_properties_last 201 | - sort_constructors_first 202 | # - sort_pub_dependencies # prevents separating pinned transitive dependencies 203 | - sort_unnamed_constructors_first 204 | - test_types_in_equals 205 | - throw_in_finally 206 | - tighten_type_of_initializing_formals 207 | # - type_annotate_public_apis # subset of always_specify_types 208 | - type_init_formals 209 | # - unawaited_futures # too many false positives, especially with the way AnimationController works 210 | - unnecessary_await_in_return 211 | - unnecessary_brace_in_string_interps 212 | - unnecessary_const 213 | # - unnecessary_final # conflicts with prefer_final_locals 214 | - unnecessary_getters_setters 215 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 216 | - unnecessary_new 217 | - unnecessary_null_aware_assignments 218 | - unnecessary_null_checks 219 | - unnecessary_null_in_if_null_operators 220 | - unnecessary_nullable_for_final_variable_declarations 221 | - unnecessary_overrides 222 | - unnecessary_parenthesis 223 | # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint 224 | - unnecessary_statements 225 | - unnecessary_string_escapes 226 | - unnecessary_string_interpolations 227 | - unnecessary_this 228 | - unrelated_type_equality_checks 229 | - unsafe_html 230 | - use_build_context_synchronously 231 | - use_full_hex_values_for_flutter_colors 232 | - use_function_type_syntax_for_parameters 233 | # - use_if_null_to_convert_nulls_to_bools # blocked on https://github.com/dart-lang/sdk/issues/47436 234 | - use_is_even_rather_than_modulo 235 | - use_key_in_widget_constructors 236 | - use_late_for_private_fields_and_variables 237 | - use_named_constants 238 | - use_raw_strings 239 | - use_rethrow_when_possible 240 | - use_setters_to_change_properties 241 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 242 | - use_test_throws_matchers 243 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 244 | - valid_regexps 245 | - void_checks -------------------------------------------------------------------------------- /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 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 31 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.ouday.nyt.nyt_flutter" 47 | minSdkVersion 16 48 | targetSdkVersion 30 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/ouday/nyt/nyt_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ouday.nyt.nyt_flutter 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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.0' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '11' 3 | default_step_lib_source: 'https://github.com/bitrise-io/bitrise-steplib.git' 4 | project_type: flutter 5 | trigger_map: 6 | - push_branch: '*' 7 | workflow: primary 8 | - pull_request_source_branch: '*' 9 | workflow: primary 10 | workflows: 11 | deploy: 12 | steps: 13 | - activate-ssh-key@4: 14 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 15 | - git-clone@6: {} 16 | - script@1: 17 | title: Do anything with Script step 18 | - certificate-and-profile-installer@1: {} 19 | - flutter-installer@0: 20 | inputs: 21 | - is_update: 'false' 22 | - cache-pull@2: {} 23 | - flutter-analyze@0: 24 | inputs: 25 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 26 | - flutter-test@1: 27 | inputs: 28 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 29 | - flutter-build@0: 30 | inputs: 31 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 32 | - platform: both 33 | - xcode-archive@4: 34 | inputs: 35 | - project_path: $BITRISE_PROJECT_PATH 36 | - scheme: $BITRISE_SCHEME 37 | - distribution_method: $BITRISE_DISTRIBUTION_METHOD 38 | - configuration: Release 39 | - deploy-to-bitrise-io@2: {} 40 | - cache-push@2: {} 41 | primary: 42 | steps: 43 | - activate-ssh-key@4: 44 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 45 | - git-clone@6: {} 46 | - script@1: 47 | title: Do anything with Script step 48 | - flutter-installer@0: 49 | inputs: 50 | - is_update: 'false' 51 | - cache-pull@2: {} 52 | - flutter-analyze@0.3: 53 | inputs: 54 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 55 | - flutter-test@1: 56 | inputs: 57 | - project_location: $BITRISE_FLUTTER_PROJECT_LOCATION 58 | - deploy-to-bitrise-io@2: {} 59 | - cache-push@2: {} 60 | meta: 61 | bitrise.io: 62 | stack: linux-docker-android-20.04 63 | machine_type_id: elite 64 | app: 65 | envs: 66 | - opts: 67 | is_expand: false 68 | BITRISE_FLUTTER_PROJECT_LOCATION: . 69 | - opts: 70 | is_expand: false 71 | BITRISE_PROJECT_PATH: . 72 | - opts: 73 | is_expand: false 74 | BITRISE_SCHEME: . 75 | - opts: 76 | is_expand: false 77 | BITRISE_DISTRIBUTION_METHOD: development 78 | -------------------------------------------------------------------------------- /integration_test/article_detail/presentation/screen/article_detail_screen_should.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:integration_test/integration_test.dart'; 5 | import 'package:nyt_flutter/article_detail/presentation/screen/article_detail.dart'; 6 | import 'package:nyt_flutter/articles_list/data/model/article.dart'; 7 | import 'package:nyt_flutter/common/constant.dart'; 8 | 9 | void main() { 10 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 11 | 12 | testWidgets('show title', (WidgetTester tester) async { 13 | await tester.pumpWidget(getArticleDetailWidget(mockArticle)); 14 | await tester.pumpAndSettle(); 15 | expect(find.text('Article title'), findsOneWidget); 16 | }); 17 | 18 | testWidgets('show description', (WidgetTester tester) async { 19 | await tester.pumpWidget(getArticleDetailWidget(mockArticle)); 20 | await tester.pumpAndSettle(); 21 | expect(find.text('abstract'), findsOneWidget); 22 | }); 23 | 24 | testWidgets('show image', (WidgetTester tester) async { 25 | await tester.pumpWidget(getArticleDetailWidget(mockArticle)); 26 | await tester.pumpAndSettle(); 27 | final CachedNetworkImage articleImageWidget = tester 28 | .widget(find.byKey(const Key('articleImage'))); 29 | expect(articleImageWidget.imageUrl, mockImageUrl); 30 | }); 31 | 32 | testWidgets( 33 | 'show default image when 3rd image in model object does not exist', 34 | (WidgetTester tester) async { 35 | await tester 36 | .pumpWidget(getArticleDetailWidget(mockArticleWithMissingImage)); 37 | await tester.pumpAndSettle(); 38 | final CachedNetworkImage articleImageWidget = tester 39 | .widget(find.byKey(const Key('articleImage'))); 40 | expect(articleImageWidget.imageUrl, defaultImage); 41 | }); 42 | 43 | testWidgets('show default image when no metadata', 44 | (WidgetTester tester) async { 45 | await tester.pumpWidget(getArticleDetailWidget(mockArticleWithMetaData)); 46 | await tester.pumpAndSettle(); 47 | final CachedNetworkImage articleImageWidget = tester 48 | .widget(find.byKey(const Key('articleImage'))); 49 | expect(articleImageWidget.imageUrl, defaultImage); 50 | }); 51 | } 52 | 53 | const String mockImageUrl = 54 | 'https://media.istockphoto.com/photos/eagle-hunter-in-traditional-costume-riding-horse-with-golden-eagle-in-picture-id1343808526'; 55 | 56 | Article mockArticle = 57 | Article('Article title', 'abstract', 123, 'url', 'publishedData', [ 58 | Media('caption', [ 59 | MediaMetaData('url', 'format'), 60 | MediaMetaData('url', 'format'), 61 | MediaMetaData(mockImageUrl, 'format'), 62 | ]) 63 | ]); 64 | 65 | Article mockArticleWithMissingImage = 66 | Article('Article title', 'abstract', 123, 'url', 'publishedData', [ 67 | Media('caption', [ 68 | MediaMetaData('url', 'format'), 69 | MediaMetaData(mockImageUrl, 'format'), 70 | ]) 71 | ]); 72 | 73 | Article mockArticleWithMetaData = Article('Article title', 'abstract', 123, 74 | 'url', 'publishedData', [Media('caption', [])]); 75 | 76 | MaterialApp getArticleDetailWidget(Article article) => MaterialApp( 77 | theme: ThemeData(), 78 | home: ArticleDetail(article), 79 | ); 80 | -------------------------------------------------------------------------------- /integration_test/articles_list/presentation/screen/article_list_screen_should.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | import 'package:integration_test/integration_test.dart'; 6 | import 'package:nyt_flutter/article_detail/presentation/screen/article_detail.dart'; 7 | import 'package:nyt_flutter/articles_list/data/model/article.dart'; 8 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart'; 9 | import 'package:nyt_flutter/articles_list/domain/usecase/article_usecase.dart'; 10 | import 'package:nyt_flutter/articles_list/presentation/screen/articles_list_screen.dart'; 11 | import 'package:nyt_flutter/core/error.dart'; 12 | import 'package:nyt_flutter/di/di_setup.dart'; 13 | 14 | const int numberOfArticles = 20; 15 | 16 | void setupDiForSuccess() { 17 | GetIt.instance.allowReassignment = true; 18 | configureDependencies(); 19 | GetIt.instance 20 | .registerSingleton(MockArticleListUseCaseSuccess()); 21 | } 22 | 23 | void main() { 24 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 25 | 26 | testWidgets('show when widget start should show all articles', 27 | (WidgetTester tester) async { 28 | setupDiForSuccess(); 29 | await tester.pumpWidget(getArticleListWidget()); 30 | await tester.pumpAndSettle(); 31 | final Finder articlesListFinder = find.byKey(const Key('ArticlesList')); 32 | expect(find.byKey(const Key('ArticlesList')), findsOneWidget); 33 | final ListView listWidget = tester.widget(articlesListFinder); 34 | expect(listWidget.childrenDelegate.estimatedChildCount, numberOfArticles); 35 | }); 36 | 37 | testWidgets('should show scroll vertically', (WidgetTester tester) async { 38 | setupDiForSuccess(); 39 | await tester.pumpWidget(getArticleListWidget()); 40 | await tester.pumpAndSettle(); 41 | final Finder articlesListFinder = find.byType(Scrollable); 42 | final Finder targetItem = 43 | find.byKey(const Key('ArticlesList_Item_${numberOfArticles - 1}')); 44 | await tester.scrollUntilVisible(targetItem, 100, 45 | scrollable: articlesListFinder); 46 | await tester.pumpAndSettle(); 47 | expect(find.text('title${numberOfArticles - 1}'), findsOneWidget); 48 | }); 49 | 50 | testWidgets('should open article details screen when item tapped', 51 | (WidgetTester tester) async { 52 | setupDiForSuccess(); 53 | await tester.pumpWidget(getArticleListWidget()); 54 | await tester.pumpAndSettle(); 55 | final Finder targetItem = find.byKey(const Key('ArticlesList_Item_1')); 56 | await tester.tap(targetItem); 57 | await tester.pumpAndSettle(); 58 | expect(find.byType(MockScreen), findsOneWidget); 59 | }); 60 | } 61 | 62 | List
getMockArticles() { 63 | return List
.generate( 64 | numberOfArticles, 65 | (int index) => Article('title$index', 'abstract$index', index, 'url$index', 66 | 'publishedData', [ 67 | Media('caption$index', [MediaMetaData('url$index', 'format$index')]) 68 | ])); 69 | } 70 | 71 | class MockArticleListUseCaseSuccess implements ArticleUseCase { 72 | @override 73 | Future> requestNews() async { 74 | return right(MostPopularResponse('', '', getMockArticles())); 75 | } 76 | } 77 | 78 | class MockArticleListUseCaseFailure implements ArticleUseCase { 79 | @override 80 | Future> requestNews() async { 81 | return left(const Error.httpInternalServerError('')); 82 | } 83 | } 84 | 85 | class MockScreen extends StatelessWidget { 86 | const MockScreen({Key? key}) : super(key: key); 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | return Container(); 91 | } 92 | } 93 | 94 | MaterialApp getArticleListWidget() => MaterialApp( 95 | theme: ThemeData(), 96 | home: const ArticlesListScreen(title: 'Test Title'), 97 | routes: { 98 | ArticleDetail.routeKey: (BuildContext context) => const MockScreen(), 99 | }, 100 | ); 101 | -------------------------------------------------------------------------------- /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 | 9.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 = 46; 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 = 1020; 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 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | INFOPLIST_FILE = Runner/Info.plist; 293 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 294 | PRODUCT_BUNDLE_IDENTIFIER = com.ouday.nyt.nytFlutter; 295 | PRODUCT_NAME = "$(TARGET_NAME)"; 296 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 297 | SWIFT_VERSION = 5.0; 298 | VERSIONING_SYSTEM = "apple-generic"; 299 | }; 300 | name = Profile; 301 | }; 302 | 97C147031CF9000F007C117D /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = dwarf; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | ENABLE_TESTABILITY = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_DYNAMIC_NO_PIC = NO; 337 | GCC_NO_COMMON_BLOCKS = YES; 338 | GCC_OPTIMIZATION_LEVEL = 0; 339 | GCC_PREPROCESSOR_DEFINITIONS = ( 340 | "DEBUG=1", 341 | "$(inherited)", 342 | ); 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 350 | MTL_ENABLE_DEBUG_INFO = YES; 351 | ONLY_ACTIVE_ARCH = YES; 352 | SDKROOT = iphoneos; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Debug; 356 | }; 357 | 97C147041CF9000F007C117D /* Release */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_NONNULL = YES; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 386 | COPY_PHASE_STRIP = NO; 387 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 388 | ENABLE_NS_ASSERTIONS = NO; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu99; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 399 | MTL_ENABLE_DEBUG_INFO = NO; 400 | SDKROOT = iphoneos; 401 | SUPPORTED_PLATFORMS = iphoneos; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 403 | TARGETED_DEVICE_FAMILY = "1,2"; 404 | VALIDATE_PRODUCT = YES; 405 | }; 406 | name = Release; 407 | }; 408 | 97C147061CF9000F007C117D /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 411 | buildSettings = { 412 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 413 | CLANG_ENABLE_MODULES = YES; 414 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 415 | ENABLE_BITCODE = NO; 416 | INFOPLIST_FILE = Runner/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 418 | PRODUCT_BUNDLE_IDENTIFIER = com.ouday.nyt.nytFlutter; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 422 | SWIFT_VERSION = 5.0; 423 | VERSIONING_SYSTEM = "apple-generic"; 424 | }; 425 | name = Debug; 426 | }; 427 | 97C147071CF9000F007C117D /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | CLANG_ENABLE_MODULES = YES; 433 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 434 | ENABLE_BITCODE = NO; 435 | INFOPLIST_FILE = Runner/Info.plist; 436 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 437 | PRODUCT_BUNDLE_IDENTIFIER = com.ouday.nyt.nytFlutter; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 440 | SWIFT_VERSION = 5.0; 441 | VERSIONING_SYSTEM = "apple-generic"; 442 | }; 443 | name = Release; 444 | }; 445 | /* End XCBuildConfiguration section */ 446 | 447 | /* Begin XCConfigurationList section */ 448 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 449 | isa = XCConfigurationList; 450 | buildConfigurations = ( 451 | 97C147031CF9000F007C117D /* Debug */, 452 | 97C147041CF9000F007C117D /* Release */, 453 | 249021D3217E4FDB00AE95B9 /* Profile */, 454 | ); 455 | defaultConfigurationIsVisible = 0; 456 | defaultConfigurationName = Release; 457 | }; 458 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | 97C147061CF9000F007C117D /* Debug */, 462 | 97C147071CF9000F007C117D /* Release */, 463 | 249021D4217E4FDB00AE95B9 /* Profile */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | /* End XCConfigurationList section */ 469 | }; 470 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 471 | } 472 | -------------------------------------------------------------------------------- /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 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/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 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | nyt_flutter 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/article_detail/presentation/screen/article_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import '../../../articles_list/data/model/article.dart'; 4 | import '../../../common/constant.dart'; 5 | 6 | class ArticleDetail extends StatefulWidget { 7 | const ArticleDetail(this._article, {Key? key}) : super(key: key); 8 | static const String routeKey = '/ArticleDetail'; 9 | final Article _article; 10 | 11 | @override 12 | ArticleDetailState createState() => ArticleDetailState(); 13 | } 14 | 15 | class ArticleDetailState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | String imageUrl = defaultImage; 19 | if (widget._article.media.isNotEmpty && 20 | widget._article.media.first.metaData.isNotEmpty && 21 | widget._article.media.first.metaData.length > 2) { 22 | imageUrl = widget._article.media.first.metaData[2].url; 23 | } 24 | return Scaffold( 25 | appBar: AppBar( 26 | title: Text(widget._article.title), 27 | ), 28 | body: ListView( 29 | children: [ 30 | CachedNetworkImage( 31 | key: const Key('articleImage'), 32 | imageUrl: imageUrl, 33 | width: double.infinity, 34 | height: MediaQuery.of(context).size.height * 0.3, 35 | fit: BoxFit.fitWidth, 36 | imageBuilder: (BuildContext context, ImageProvider imageProvider) { 37 | return Material( 38 | elevation: 4, 39 | child: Container( 40 | decoration: BoxDecoration( 41 | image: DecorationImage( 42 | image: imageProvider, 43 | fit: BoxFit.fitWidth, 44 | ), 45 | ), 46 | ), 47 | ); 48 | }, 49 | ), 50 | Padding( 51 | padding: const EdgeInsets.all(8.0), 52 | child: Text( 53 | widget._article.abstract, 54 | style: Theme.of(context).textTheme.headline5, 55 | ), 56 | ), 57 | ], 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/articles_list/data/model/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'article.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Article { 7 | Article(this.title, this.abstract, this.id, this.url, this.publishedData, 8 | this.media); 9 | 10 | factory Article.fromJson(Map json) => 11 | _$ArticleFromJson(json); 12 | 13 | Map toJson() => _$ArticleToJson(this); 14 | 15 | final int id; 16 | final String title, abstract, url; 17 | @JsonKey(name: 'published_date') 18 | final String? publishedData; 19 | final List media; 20 | } 21 | 22 | @JsonSerializable() 23 | class Media { 24 | Media(this.caption, this.metaData); 25 | 26 | factory Media.fromJson(Map json) => _$MediaFromJson(json); 27 | 28 | @JsonKey(name: 'caption', defaultValue: '') 29 | final String caption; 30 | 31 | @JsonKey(name: 'media-metadata', defaultValue: []) 32 | final List metaData; 33 | 34 | Map toJson() => _$MediaToJson(this); 35 | } 36 | 37 | @JsonSerializable() 38 | class MediaMetaData { 39 | MediaMetaData(this.url, this.format); 40 | 41 | factory MediaMetaData.fromJson(Map json) => 42 | _$MediaMetaDataFromJson(json); 43 | 44 | @JsonKey(defaultValue: '') 45 | final String url; 46 | 47 | @JsonKey(defaultValue: '') 48 | final String format; 49 | 50 | Map toJson() => _$MediaMetaDataToJson(this); 51 | } 52 | -------------------------------------------------------------------------------- /lib/articles_list/data/model/article.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'article.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Article _$ArticleFromJson(Map json) => Article( 10 | json['title'] as String, 11 | json['abstract'] as String, 12 | json['id'] as int, 13 | json['url'] as String, 14 | json['published_date'] as String?, 15 | (json['media'] as List) 16 | .map((dynamic media) => Media.fromJson(media as Map)) 17 | .toList(), 18 | ); 19 | 20 | Map _$ArticleToJson(Article instance) => { 21 | 'id': instance.id, 22 | 'title': instance.title, 23 | 'abstract': instance.abstract, 24 | 'url': instance.url, 25 | 'published_date': instance.publishedData, 26 | 'media': instance.media, 27 | }; 28 | 29 | Media _$MediaFromJson(Map json) => Media( 30 | json['caption'] as String? ?? '', 31 | (json['media-metadata'] as List?) 32 | ?.map((dynamic media) => MediaMetaData.fromJson(media as Map)) 33 | .toList() ?? [], 34 | ); 35 | 36 | Map _$MediaToJson(Media instance) => { 37 | 'caption': instance.caption, 38 | 'media-metadata': instance.metaData, 39 | }; 40 | 41 | MediaMetaData _$MediaMetaDataFromJson(Map json) => 42 | MediaMetaData( 43 | json['url'] as String? ?? '', 44 | json['format'] as String? ?? '', 45 | ); 46 | 47 | Map _$MediaMetaDataToJson(MediaMetaData instance) => 48 | { 49 | 'url': instance.url, 50 | 'format': instance.format, 51 | }; 52 | -------------------------------------------------------------------------------- /lib/articles_list/data/model/most_popular_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'article.dart'; 3 | 4 | part 'most_popular_response.g.dart'; 5 | 6 | @JsonSerializable() 7 | class MostPopularResponse { 8 | MostPopularResponse(this.status, this.copyright, this.articles); 9 | 10 | factory MostPopularResponse.fromJson(Map json) => 11 | _$MostPopularResponseFromJson(json); 12 | final String status, copyright; 13 | @JsonKey(name: 'results') 14 | final List
articles; 15 | 16 | Map toJson() => _$MostPopularResponseToJson(this); 17 | } 18 | -------------------------------------------------------------------------------- /lib/articles_list/data/model/most_popular_response.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'most_popular_response.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | MostPopularResponse _$MostPopularResponseFromJson(Map json) => 10 | MostPopularResponse( 11 | json['status'] as String, 12 | json['copyright'] as String, 13 | (json['results'] as List?) 14 | ?.map((dynamic e) => Article.fromJson(e as Map)) 15 | .toList() ?? 16 |
[], 17 | ); 18 | 19 | Map _$MostPopularResponseToJson( 20 | MostPopularResponse instance) => 21 | { 22 | 'status': instance.status, 23 | 'copyright': instance.copyright, 24 | 'results': instance.articles, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/articles_list/data/remote/service/article_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/http.dart'; 4 | 5 | import '../../model/most_popular_response.dart'; 6 | 7 | part 'article_service.g.dart'; 8 | 9 | @RestApi() 10 | @injectable 11 | abstract class ArticleService { 12 | @factoryMethod 13 | factory ArticleService(Dio dio) = _ArticleService; 14 | 15 | @GET('mostpopular/v2/emailed/30.json') 16 | Future getTasks(@Query('api-key') String apiKey); 17 | } 18 | -------------------------------------------------------------------------------- /lib/articles_list/data/remote/service/article_service.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'article_service.dart'; 4 | 5 | // ************************************************************************** 6 | // RetrofitGenerator 7 | // ************************************************************************** 8 | 9 | class _ArticleService implements ArticleService { 10 | _ArticleService(this._dio, {this.baseUrl}); 11 | 12 | final Dio _dio; 13 | 14 | String? baseUrl; 15 | 16 | @override 17 | Future getTasks(apiKey) async { 18 | const _extra = {}; 19 | final queryParameters = {r'api-key': apiKey}; 20 | final _headers = {}; 21 | final _data = {}; 22 | final _result = await _dio.fetch>( 23 | _setStreamType( 24 | Options(method: 'GET', headers: _headers, extra: _extra) 25 | .compose(_dio.options, 'mostpopular/v2/emailed/30.json', 26 | queryParameters: queryParameters, data: _data) 27 | .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); 28 | final value = MostPopularResponse.fromJson(_result.data!); 29 | return value; 30 | } 31 | 32 | RequestOptions _setStreamType(RequestOptions requestOptions) { 33 | if (T != dynamic && 34 | !(requestOptions.responseType == ResponseType.bytes || 35 | requestOptions.responseType == ResponseType.stream)) { 36 | if (T == String) { 37 | requestOptions.responseType = ResponseType.plain; 38 | } else { 39 | requestOptions.responseType = ResponseType.json; 40 | } 41 | } 42 | return requestOptions; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/articles_list/data/remote/source/article_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import '../../model/most_popular_response.dart'; 2 | 3 | abstract class ArticleRemoteDataSource { 4 | Future getTasks(String apiKey); 5 | } 6 | -------------------------------------------------------------------------------- /lib/articles_list/data/remote/source/article_remote_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import '../../model/most_popular_response.dart'; 3 | import '../service/article_service.dart'; 4 | 5 | import 'article_remote_data_source.dart'; 6 | 7 | @Injectable(as: ArticleRemoteDataSource) 8 | class ArticleRemoteDataSourceImpl implements ArticleRemoteDataSource { 9 | ArticleRemoteDataSourceImpl(this._service); 10 | 11 | final ArticleService _service; 12 | 13 | @override 14 | Future getTasks(String apiKey) => 15 | _service.getTasks(apiKey); 16 | } 17 | -------------------------------------------------------------------------------- /lib/articles_list/data/repository/article_repo_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | import '../../../common/constant.dart'; 5 | 6 | import '../../../core/error.dart'; 7 | import '../../domain/repository/article_repo.dart'; 8 | import '../model/most_popular_response.dart'; 9 | import '../remote/source/article_remote_data_source.dart'; 10 | 11 | @Injectable(as: ArticleRepo) 12 | class ArticleRepoImpl implements ArticleRepo { 13 | ArticleRepoImpl(this._remoteDataSource); 14 | 15 | final ArticleRemoteDataSource _remoteDataSource; 16 | 17 | @override 18 | Future> requestNews() async { 19 | try { 20 | final MostPopularResponse result = 21 | await _remoteDataSource.getTasks(apiKey); 22 | return right(result); 23 | } on DioError catch (exception) { 24 | if (exception.type == DioErrorType.response) { 25 | final int statusCode = exception.response!.statusCode ?? 503; 26 | if (statusCode == 401) { 27 | return left(const Error.httpUnAuthorizedError()); 28 | } else { 29 | return left(HttpInternalServerError(exception.message)); 30 | } 31 | } 32 | return left(HttpUnknownError(exception.message)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/articles_list/domain/repository/article_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../../../core/error.dart'; 4 | import '../../data/model/most_popular_response.dart'; 5 | 6 | abstract class ArticleRepo { 7 | Future> requestNews(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/articles_list/domain/usecase/article_usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../../../core/error.dart'; 4 | import '../../data/model/most_popular_response.dart'; 5 | 6 | abstract class ArticleUseCase { 7 | Future> requestNews(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/articles_list/domain/usecase/article_usecase_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../../core/error.dart'; 5 | import '../../data/model/most_popular_response.dart'; 6 | import '../repository/article_repo.dart'; 7 | import 'article_usecase.dart'; 8 | 9 | @Injectable(as: ArticleUseCase) 10 | class ArticleUseCaseImpl implements ArticleUseCase { 11 | ArticleUseCaseImpl(this._articleRepo); 12 | 13 | final ArticleRepo _articleRepo; 14 | 15 | @override 16 | Future> requestNews() => 17 | _articleRepo.requestNews(); 18 | } 19 | -------------------------------------------------------------------------------- /lib/articles_list/presentation/bloc/article_list_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:freezed_annotation/freezed_annotation.dart'; 7 | import 'package:injectable/injectable.dart'; 8 | 9 | import '../../../core/error.dart'; 10 | import '../../data/model/article.dart'; 11 | import '../../data/model/most_popular_response.dart'; 12 | import '../../domain/usecase/article_usecase.dart'; 13 | 14 | part 'article_list_bloc.freezed.dart'; 15 | 16 | part 'article_list_event.dart'; 17 | 18 | part 'article_list_state.dart'; 19 | 20 | @injectable 21 | class ArticleListBloc extends Bloc { 22 | ArticleListBloc(this._articleUseCase) : super(ArticleListState.initial()) { 23 | on( 24 | (ArticleListEvent event, Emitter emit) async { 25 | await event.when( 26 | loadArticles: () => loadArticles(emit), 27 | markAsFavorite: (Article article) => markAsFavorite(emit, article), 28 | unMarkAsFavorite: (Article article) => unMarkAsFavorite(emit, article), 29 | ); 30 | }); 31 | } 32 | 33 | final ArticleUseCase _articleUseCase; 34 | 35 | Future loadArticles(Emitter emit) async { 36 | emit(state.copyWith(isLoading: true, articles: none())); 37 | final Either result = 38 | await _articleUseCase.requestNews(); 39 | emit(result.fold( 40 | (Error error) => state.copyWith(isLoading: false, articles: none()), 41 | (MostPopularResponse response) => state.copyWith( 42 | isLoading: false, articles: optionOf(response.articles)))); 43 | } 44 | 45 | Future markAsFavorite( 46 | Emitter emit, Article article) async {} 47 | 48 | Future unMarkAsFavorite( 49 | Emitter emit, Article article) async {} 50 | } 51 | -------------------------------------------------------------------------------- /lib/articles_list/presentation/bloc/article_list_bloc.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // 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 4 | 5 | part of 'article_list_bloc.dart'; 6 | 7 | // ************************************************************************** 8 | // FreezedGenerator 9 | // ************************************************************************** 10 | 11 | T _$identity(T value) => value; 12 | 13 | final _privateConstructorUsedError = UnsupportedError( 14 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 15 | 16 | /// @nodoc 17 | class _$ArticleListEventTearOff { 18 | const _$ArticleListEventTearOff(); 19 | 20 | LoadArticles loadArticles() { 21 | return const LoadArticles(); 22 | } 23 | 24 | MarkAsFavorite markAsFavorite(Article article) { 25 | return MarkAsFavorite( 26 | article, 27 | ); 28 | } 29 | 30 | UnMarkAsFavorite unMarkAsFavorite(Article article) { 31 | return UnMarkAsFavorite( 32 | article, 33 | ); 34 | } 35 | } 36 | 37 | /// @nodoc 38 | const $ArticleListEvent = _$ArticleListEventTearOff(); 39 | 40 | /// @nodoc 41 | mixin _$ArticleListEvent { 42 | @optionalTypeArgs 43 | TResult when({ 44 | required TResult Function() loadArticles, 45 | required TResult Function(Article article) markAsFavorite, 46 | required TResult Function(Article article) unMarkAsFavorite, 47 | }) => 48 | throw _privateConstructorUsedError; 49 | @optionalTypeArgs 50 | TResult? whenOrNull({ 51 | TResult Function()? loadArticles, 52 | TResult Function(Article article)? markAsFavorite, 53 | TResult Function(Article article)? unMarkAsFavorite, 54 | }) => 55 | throw _privateConstructorUsedError; 56 | @optionalTypeArgs 57 | TResult maybeWhen({ 58 | TResult Function()? loadArticles, 59 | TResult Function(Article article)? markAsFavorite, 60 | TResult Function(Article article)? unMarkAsFavorite, 61 | required TResult orElse(), 62 | }) => 63 | throw _privateConstructorUsedError; 64 | @optionalTypeArgs 65 | TResult map({ 66 | required TResult Function(LoadArticles value) loadArticles, 67 | required TResult Function(MarkAsFavorite value) markAsFavorite, 68 | required TResult Function(UnMarkAsFavorite value) unMarkAsFavorite, 69 | }) => 70 | throw _privateConstructorUsedError; 71 | @optionalTypeArgs 72 | TResult? mapOrNull({ 73 | TResult Function(LoadArticles value)? loadArticles, 74 | TResult Function(MarkAsFavorite value)? markAsFavorite, 75 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 76 | }) => 77 | throw _privateConstructorUsedError; 78 | @optionalTypeArgs 79 | TResult maybeMap({ 80 | TResult Function(LoadArticles value)? loadArticles, 81 | TResult Function(MarkAsFavorite value)? markAsFavorite, 82 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 83 | required TResult orElse(), 84 | }) => 85 | throw _privateConstructorUsedError; 86 | } 87 | 88 | /// @nodoc 89 | abstract class $ArticleListEventCopyWith<$Res> { 90 | factory $ArticleListEventCopyWith( 91 | ArticleListEvent value, $Res Function(ArticleListEvent) then) = 92 | _$ArticleListEventCopyWithImpl<$Res>; 93 | } 94 | 95 | /// @nodoc 96 | class _$ArticleListEventCopyWithImpl<$Res> 97 | implements $ArticleListEventCopyWith<$Res> { 98 | _$ArticleListEventCopyWithImpl(this._value, this._then); 99 | 100 | final ArticleListEvent _value; 101 | // ignore: unused_field 102 | final $Res Function(ArticleListEvent) _then; 103 | } 104 | 105 | /// @nodoc 106 | abstract class $LoadArticlesCopyWith<$Res> { 107 | factory $LoadArticlesCopyWith( 108 | LoadArticles value, $Res Function(LoadArticles) then) = 109 | _$LoadArticlesCopyWithImpl<$Res>; 110 | } 111 | 112 | /// @nodoc 113 | class _$LoadArticlesCopyWithImpl<$Res> 114 | extends _$ArticleListEventCopyWithImpl<$Res> 115 | implements $LoadArticlesCopyWith<$Res> { 116 | _$LoadArticlesCopyWithImpl( 117 | LoadArticles _value, $Res Function(LoadArticles) _then) 118 | : super(_value, (v) => _then(v as LoadArticles)); 119 | 120 | @override 121 | LoadArticles get _value => super._value as LoadArticles; 122 | } 123 | 124 | /// @nodoc 125 | 126 | class _$LoadArticles implements LoadArticles { 127 | const _$LoadArticles(); 128 | 129 | @override 130 | String toString() { 131 | return 'ArticleListEvent.loadArticles()'; 132 | } 133 | 134 | @override 135 | bool operator ==(dynamic other) { 136 | return identical(this, other) || 137 | (other.runtimeType == runtimeType && other is LoadArticles); 138 | } 139 | 140 | @override 141 | int get hashCode => runtimeType.hashCode; 142 | 143 | @override 144 | @optionalTypeArgs 145 | TResult when({ 146 | required TResult Function() loadArticles, 147 | required TResult Function(Article article) markAsFavorite, 148 | required TResult Function(Article article) unMarkAsFavorite, 149 | }) { 150 | return loadArticles(); 151 | } 152 | 153 | @override 154 | @optionalTypeArgs 155 | TResult? whenOrNull({ 156 | TResult Function()? loadArticles, 157 | TResult Function(Article article)? markAsFavorite, 158 | TResult Function(Article article)? unMarkAsFavorite, 159 | }) { 160 | return loadArticles?.call(); 161 | } 162 | 163 | @override 164 | @optionalTypeArgs 165 | TResult maybeWhen({ 166 | TResult Function()? loadArticles, 167 | TResult Function(Article article)? markAsFavorite, 168 | TResult Function(Article article)? unMarkAsFavorite, 169 | required TResult orElse(), 170 | }) { 171 | if (loadArticles != null) { 172 | return loadArticles(); 173 | } 174 | return orElse(); 175 | } 176 | 177 | @override 178 | @optionalTypeArgs 179 | TResult map({ 180 | required TResult Function(LoadArticles value) loadArticles, 181 | required TResult Function(MarkAsFavorite value) markAsFavorite, 182 | required TResult Function(UnMarkAsFavorite value) unMarkAsFavorite, 183 | }) { 184 | return loadArticles(this); 185 | } 186 | 187 | @override 188 | @optionalTypeArgs 189 | TResult? mapOrNull({ 190 | TResult Function(LoadArticles value)? loadArticles, 191 | TResult Function(MarkAsFavorite value)? markAsFavorite, 192 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 193 | }) { 194 | return loadArticles?.call(this); 195 | } 196 | 197 | @override 198 | @optionalTypeArgs 199 | TResult maybeMap({ 200 | TResult Function(LoadArticles value)? loadArticles, 201 | TResult Function(MarkAsFavorite value)? markAsFavorite, 202 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 203 | required TResult orElse(), 204 | }) { 205 | if (loadArticles != null) { 206 | return loadArticles(this); 207 | } 208 | return orElse(); 209 | } 210 | } 211 | 212 | abstract class LoadArticles implements ArticleListEvent { 213 | const factory LoadArticles() = _$LoadArticles; 214 | } 215 | 216 | /// @nodoc 217 | abstract class $MarkAsFavoriteCopyWith<$Res> { 218 | factory $MarkAsFavoriteCopyWith( 219 | MarkAsFavorite value, $Res Function(MarkAsFavorite) then) = 220 | _$MarkAsFavoriteCopyWithImpl<$Res>; 221 | $Res call({Article article}); 222 | } 223 | 224 | /// @nodoc 225 | class _$MarkAsFavoriteCopyWithImpl<$Res> 226 | extends _$ArticleListEventCopyWithImpl<$Res> 227 | implements $MarkAsFavoriteCopyWith<$Res> { 228 | _$MarkAsFavoriteCopyWithImpl( 229 | MarkAsFavorite _value, $Res Function(MarkAsFavorite) _then) 230 | : super(_value, (v) => _then(v as MarkAsFavorite)); 231 | 232 | @override 233 | MarkAsFavorite get _value => super._value as MarkAsFavorite; 234 | 235 | @override 236 | $Res call({ 237 | Object? article = freezed, 238 | }) { 239 | return _then(MarkAsFavorite( 240 | article == freezed 241 | ? _value.article 242 | : article // ignore: cast_nullable_to_non_nullable 243 | as Article, 244 | )); 245 | } 246 | } 247 | 248 | /// @nodoc 249 | 250 | class _$MarkAsFavorite implements MarkAsFavorite { 251 | const _$MarkAsFavorite(this.article); 252 | 253 | @override 254 | final Article article; 255 | 256 | @override 257 | String toString() { 258 | return 'ArticleListEvent.markAsFavorite(article: $article)'; 259 | } 260 | 261 | @override 262 | bool operator ==(dynamic other) { 263 | return identical(this, other) || 264 | (other.runtimeType == runtimeType && 265 | other is MarkAsFavorite && 266 | (identical(other.article, article) || other.article == article)); 267 | } 268 | 269 | @override 270 | int get hashCode => Object.hash(runtimeType, article); 271 | 272 | @JsonKey(ignore: true) 273 | @override 274 | $MarkAsFavoriteCopyWith get copyWith => 275 | _$MarkAsFavoriteCopyWithImpl(this, _$identity); 276 | 277 | @override 278 | @optionalTypeArgs 279 | TResult when({ 280 | required TResult Function() loadArticles, 281 | required TResult Function(Article article) markAsFavorite, 282 | required TResult Function(Article article) unMarkAsFavorite, 283 | }) { 284 | return markAsFavorite(article); 285 | } 286 | 287 | @override 288 | @optionalTypeArgs 289 | TResult? whenOrNull({ 290 | TResult Function()? loadArticles, 291 | TResult Function(Article article)? markAsFavorite, 292 | TResult Function(Article article)? unMarkAsFavorite, 293 | }) { 294 | return markAsFavorite?.call(article); 295 | } 296 | 297 | @override 298 | @optionalTypeArgs 299 | TResult maybeWhen({ 300 | TResult Function()? loadArticles, 301 | TResult Function(Article article)? markAsFavorite, 302 | TResult Function(Article article)? unMarkAsFavorite, 303 | required TResult orElse(), 304 | }) { 305 | if (markAsFavorite != null) { 306 | return markAsFavorite(article); 307 | } 308 | return orElse(); 309 | } 310 | 311 | @override 312 | @optionalTypeArgs 313 | TResult map({ 314 | required TResult Function(LoadArticles value) loadArticles, 315 | required TResult Function(MarkAsFavorite value) markAsFavorite, 316 | required TResult Function(UnMarkAsFavorite value) unMarkAsFavorite, 317 | }) { 318 | return markAsFavorite(this); 319 | } 320 | 321 | @override 322 | @optionalTypeArgs 323 | TResult? mapOrNull({ 324 | TResult Function(LoadArticles value)? loadArticles, 325 | TResult Function(MarkAsFavorite value)? markAsFavorite, 326 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 327 | }) { 328 | return markAsFavorite?.call(this); 329 | } 330 | 331 | @override 332 | @optionalTypeArgs 333 | TResult maybeMap({ 334 | TResult Function(LoadArticles value)? loadArticles, 335 | TResult Function(MarkAsFavorite value)? markAsFavorite, 336 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 337 | required TResult orElse(), 338 | }) { 339 | if (markAsFavorite != null) { 340 | return markAsFavorite(this); 341 | } 342 | return orElse(); 343 | } 344 | } 345 | 346 | abstract class MarkAsFavorite implements ArticleListEvent { 347 | const factory MarkAsFavorite(Article article) = _$MarkAsFavorite; 348 | 349 | Article get article; 350 | @JsonKey(ignore: true) 351 | $MarkAsFavoriteCopyWith get copyWith => 352 | throw _privateConstructorUsedError; 353 | } 354 | 355 | /// @nodoc 356 | abstract class $UnMarkAsFavoriteCopyWith<$Res> { 357 | factory $UnMarkAsFavoriteCopyWith( 358 | UnMarkAsFavorite value, $Res Function(UnMarkAsFavorite) then) = 359 | _$UnMarkAsFavoriteCopyWithImpl<$Res>; 360 | $Res call({Article article}); 361 | } 362 | 363 | /// @nodoc 364 | class _$UnMarkAsFavoriteCopyWithImpl<$Res> 365 | extends _$ArticleListEventCopyWithImpl<$Res> 366 | implements $UnMarkAsFavoriteCopyWith<$Res> { 367 | _$UnMarkAsFavoriteCopyWithImpl( 368 | UnMarkAsFavorite _value, $Res Function(UnMarkAsFavorite) _then) 369 | : super(_value, (v) => _then(v as UnMarkAsFavorite)); 370 | 371 | @override 372 | UnMarkAsFavorite get _value => super._value as UnMarkAsFavorite; 373 | 374 | @override 375 | $Res call({ 376 | Object? article = freezed, 377 | }) { 378 | return _then(UnMarkAsFavorite( 379 | article == freezed 380 | ? _value.article 381 | : article // ignore: cast_nullable_to_non_nullable 382 | as Article, 383 | )); 384 | } 385 | } 386 | 387 | /// @nodoc 388 | 389 | class _$UnMarkAsFavorite implements UnMarkAsFavorite { 390 | const _$UnMarkAsFavorite(this.article); 391 | 392 | @override 393 | final Article article; 394 | 395 | @override 396 | String toString() { 397 | return 'ArticleListEvent.unMarkAsFavorite(article: $article)'; 398 | } 399 | 400 | @override 401 | bool operator ==(dynamic other) { 402 | return identical(this, other) || 403 | (other.runtimeType == runtimeType && 404 | other is UnMarkAsFavorite && 405 | (identical(other.article, article) || other.article == article)); 406 | } 407 | 408 | @override 409 | int get hashCode => Object.hash(runtimeType, article); 410 | 411 | @JsonKey(ignore: true) 412 | @override 413 | $UnMarkAsFavoriteCopyWith get copyWith => 414 | _$UnMarkAsFavoriteCopyWithImpl(this, _$identity); 415 | 416 | @override 417 | @optionalTypeArgs 418 | TResult when({ 419 | required TResult Function() loadArticles, 420 | required TResult Function(Article article) markAsFavorite, 421 | required TResult Function(Article article) unMarkAsFavorite, 422 | }) { 423 | return unMarkAsFavorite(article); 424 | } 425 | 426 | @override 427 | @optionalTypeArgs 428 | TResult? whenOrNull({ 429 | TResult Function()? loadArticles, 430 | TResult Function(Article article)? markAsFavorite, 431 | TResult Function(Article article)? unMarkAsFavorite, 432 | }) { 433 | return unMarkAsFavorite?.call(article); 434 | } 435 | 436 | @override 437 | @optionalTypeArgs 438 | TResult maybeWhen({ 439 | TResult Function()? loadArticles, 440 | TResult Function(Article article)? markAsFavorite, 441 | TResult Function(Article article)? unMarkAsFavorite, 442 | required TResult orElse(), 443 | }) { 444 | if (unMarkAsFavorite != null) { 445 | return unMarkAsFavorite(article); 446 | } 447 | return orElse(); 448 | } 449 | 450 | @override 451 | @optionalTypeArgs 452 | TResult map({ 453 | required TResult Function(LoadArticles value) loadArticles, 454 | required TResult Function(MarkAsFavorite value) markAsFavorite, 455 | required TResult Function(UnMarkAsFavorite value) unMarkAsFavorite, 456 | }) { 457 | return unMarkAsFavorite(this); 458 | } 459 | 460 | @override 461 | @optionalTypeArgs 462 | TResult? mapOrNull({ 463 | TResult Function(LoadArticles value)? loadArticles, 464 | TResult Function(MarkAsFavorite value)? markAsFavorite, 465 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 466 | }) { 467 | return unMarkAsFavorite?.call(this); 468 | } 469 | 470 | @override 471 | @optionalTypeArgs 472 | TResult maybeMap({ 473 | TResult Function(LoadArticles value)? loadArticles, 474 | TResult Function(MarkAsFavorite value)? markAsFavorite, 475 | TResult Function(UnMarkAsFavorite value)? unMarkAsFavorite, 476 | required TResult orElse(), 477 | }) { 478 | if (unMarkAsFavorite != null) { 479 | return unMarkAsFavorite(this); 480 | } 481 | return orElse(); 482 | } 483 | } 484 | 485 | abstract class UnMarkAsFavorite implements ArticleListEvent { 486 | const factory UnMarkAsFavorite(Article article) = _$UnMarkAsFavorite; 487 | 488 | Article get article; 489 | @JsonKey(ignore: true) 490 | $UnMarkAsFavoriteCopyWith get copyWith => 491 | throw _privateConstructorUsedError; 492 | } 493 | 494 | /// @nodoc 495 | class _$ArticleListStateTearOff { 496 | const _$ArticleListStateTearOff(); 497 | 498 | _ArticleListState call( 499 | {bool isLoading = false, required Option> articles}) { 500 | return _ArticleListState( 501 | isLoading: isLoading, 502 | articles: articles, 503 | ); 504 | } 505 | } 506 | 507 | /// @nodoc 508 | const $ArticleListState = _$ArticleListStateTearOff(); 509 | 510 | /// @nodoc 511 | mixin _$ArticleListState { 512 | bool get isLoading => throw _privateConstructorUsedError; 513 | Option> get articles => throw _privateConstructorUsedError; 514 | 515 | @JsonKey(ignore: true) 516 | $ArticleListStateCopyWith get copyWith => 517 | throw _privateConstructorUsedError; 518 | } 519 | 520 | /// @nodoc 521 | abstract class $ArticleListStateCopyWith<$Res> { 522 | factory $ArticleListStateCopyWith( 523 | ArticleListState value, $Res Function(ArticleListState) then) = 524 | _$ArticleListStateCopyWithImpl<$Res>; 525 | $Res call({bool isLoading, Option> articles}); 526 | } 527 | 528 | /// @nodoc 529 | class _$ArticleListStateCopyWithImpl<$Res> 530 | implements $ArticleListStateCopyWith<$Res> { 531 | _$ArticleListStateCopyWithImpl(this._value, this._then); 532 | 533 | final ArticleListState _value; 534 | // ignore: unused_field 535 | final $Res Function(ArticleListState) _then; 536 | 537 | @override 538 | $Res call({ 539 | Object? isLoading = freezed, 540 | Object? articles = freezed, 541 | }) { 542 | return _then(_value.copyWith( 543 | isLoading: isLoading == freezed 544 | ? _value.isLoading 545 | : isLoading // ignore: cast_nullable_to_non_nullable 546 | as bool, 547 | articles: articles == freezed 548 | ? _value.articles 549 | : articles // ignore: cast_nullable_to_non_nullable 550 | as Option>, 551 | )); 552 | } 553 | } 554 | 555 | /// @nodoc 556 | abstract class _$ArticleListStateCopyWith<$Res> 557 | implements $ArticleListStateCopyWith<$Res> { 558 | factory _$ArticleListStateCopyWith( 559 | _ArticleListState value, $Res Function(_ArticleListState) then) = 560 | __$ArticleListStateCopyWithImpl<$Res>; 561 | @override 562 | $Res call({bool isLoading, Option> articles}); 563 | } 564 | 565 | /// @nodoc 566 | class __$ArticleListStateCopyWithImpl<$Res> 567 | extends _$ArticleListStateCopyWithImpl<$Res> 568 | implements _$ArticleListStateCopyWith<$Res> { 569 | __$ArticleListStateCopyWithImpl( 570 | _ArticleListState _value, $Res Function(_ArticleListState) _then) 571 | : super(_value, (v) => _then(v as _ArticleListState)); 572 | 573 | @override 574 | _ArticleListState get _value => super._value as _ArticleListState; 575 | 576 | @override 577 | $Res call({ 578 | Object? isLoading = freezed, 579 | Object? articles = freezed, 580 | }) { 581 | return _then(_ArticleListState( 582 | isLoading: isLoading == freezed 583 | ? _value.isLoading 584 | : isLoading // ignore: cast_nullable_to_non_nullable 585 | as bool, 586 | articles: articles == freezed 587 | ? _value.articles 588 | : articles // ignore: cast_nullable_to_non_nullable 589 | as Option>, 590 | )); 591 | } 592 | } 593 | 594 | /// @nodoc 595 | 596 | class _$_ArticleListState implements _ArticleListState { 597 | const _$_ArticleListState({this.isLoading = false, required this.articles}); 598 | 599 | @JsonKey(defaultValue: false) 600 | @override 601 | final bool isLoading; 602 | @override 603 | final Option> articles; 604 | 605 | @override 606 | String toString() { 607 | return 'ArticleListState(isLoading: $isLoading, articles: $articles)'; 608 | } 609 | 610 | @override 611 | bool operator ==(dynamic other) { 612 | return identical(this, other) || 613 | (other.runtimeType == runtimeType && 614 | other is _ArticleListState && 615 | (identical(other.isLoading, isLoading) || 616 | other.isLoading == isLoading) && 617 | (identical(other.articles, articles) || 618 | other.articles == articles)); 619 | } 620 | 621 | @override 622 | int get hashCode => Object.hash(runtimeType, isLoading, articles); 623 | 624 | @JsonKey(ignore: true) 625 | @override 626 | _$ArticleListStateCopyWith<_ArticleListState> get copyWith => 627 | __$ArticleListStateCopyWithImpl<_ArticleListState>(this, _$identity); 628 | } 629 | 630 | abstract class _ArticleListState implements ArticleListState { 631 | const factory _ArticleListState( 632 | {bool isLoading, 633 | required Option> articles}) = _$_ArticleListState; 634 | 635 | @override 636 | bool get isLoading; 637 | @override 638 | Option> get articles; 639 | @override 640 | @JsonKey(ignore: true) 641 | _$ArticleListStateCopyWith<_ArticleListState> get copyWith => 642 | throw _privateConstructorUsedError; 643 | } 644 | -------------------------------------------------------------------------------- /lib/articles_list/presentation/bloc/article_list_event.dart: -------------------------------------------------------------------------------- 1 | part of 'article_list_bloc.dart'; 2 | 3 | @freezed 4 | class ArticleListEvent with _$ArticleListEvent { 5 | const factory ArticleListEvent.loadArticles() = LoadArticles; 6 | } 7 | -------------------------------------------------------------------------------- /lib/articles_list/presentation/bloc/article_list_state.dart: -------------------------------------------------------------------------------- 1 | part of 'article_list_bloc.dart'; 2 | 3 | @freezed 4 | class ArticleListState with _$ArticleListState { 5 | const factory ArticleListState({ 6 | @Default(false) bool isLoading, 7 | required Option> articles, 8 | }) = _ArticleListState; 9 | 10 | factory ArticleListState.initial() => ArticleListState(articles: none()); 11 | } 12 | -------------------------------------------------------------------------------- /lib/articles_list/presentation/screen/articles_list_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../../di/di_setup.dart'; 5 | import '../../data/model/article.dart'; 6 | import '../bloc/article_list_bloc.dart'; 7 | import '../widget/article_list_item.dart'; 8 | 9 | class ArticlesListScreen extends StatefulWidget { 10 | const ArticlesListScreen({Key? key, required this.title}) : super(key: key); 11 | final String title; 12 | 13 | @override 14 | State createState() => _ArticlesListScreenState(); 15 | } 16 | 17 | class _ArticlesListScreenState extends State { 18 | late final ArticleListBloc _articleListBloc; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _articleListBloc = getIt(); 24 | _articleListBloc.add(const ArticleListEvent.loadArticles()); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: const Text('Articles list'), 32 | ), 33 | body: BlocProvider( 34 | create: (_) => _articleListBloc, 35 | child: BlocBuilder( 36 | builder: (BuildContext context, ArticleListState state) { 37 | return getArticleList(context); 38 | }, 39 | )), 40 | ); 41 | } 42 | 43 | Widget getArticleList(BuildContext context) { 44 | return BlocBuilder( 45 | builder: (BuildContext context, ArticleListState state) { 46 | if (state.isLoading) { 47 | return const Center( 48 | child: Text('Loading'), 49 | ); 50 | } else { 51 | return state.articles.fold( 52 | () => Center( 53 | child: TextButton( 54 | onPressed: () { 55 | _articleListBloc 56 | .add(const ArticleListEvent.loadArticles()); 57 | }, 58 | child: const Text('Retry'), 59 | ), 60 | ), 61 | drawArticles); 62 | } 63 | }); 64 | } 65 | 66 | Widget drawArticles(List
articles) { 67 | return ListView.builder( 68 | key: const Key('ArticlesList'), 69 | itemBuilder: (BuildContext context, int index) { 70 | return ArticleListItem( 71 | key: Key('ArticlesList_Item_$index'), 72 | article: articles[index], 73 | ); 74 | }, 75 | itemCount: articles.length, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/articles_list/presentation/widget/article_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../../../article_detail/presentation/screen/article_detail.dart'; 5 | import '../../../common/constant.dart'; 6 | import '../../data/model/article.dart'; 7 | 8 | class ArticleListItem extends StatelessWidget { 9 | const ArticleListItem({ 10 | Key? key, 11 | required this.article, 12 | }) : super(key: key); 13 | 14 | final Article article; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | String imageUrl = defaultImage; 19 | if (article.media.isNotEmpty && 20 | article.media.first.metaData.isNotEmpty && 21 | article.media.first.metaData.first.url.contains('http')) { 22 | imageUrl = article.media.first.metaData.first.url; 23 | } 24 | 25 | return InkWell( 26 | child: Padding( 27 | padding: const EdgeInsets.all(8.0), 28 | child: Container( 29 | padding: const EdgeInsets.all(8.0), 30 | decoration: BoxDecoration( 31 | borderRadius: const BorderRadius.all(Radius.circular(10)), 32 | border: Border.all()), 33 | child: Row( 34 | children: [ 35 | CachedNetworkImage( 36 | imageUrl: imageUrl, 37 | width: 64, 38 | height: 64, 39 | errorWidget: 40 | (BuildContext context, String url, dynamic error) => 41 | const CircleAvatar(), 42 | imageBuilder: (BuildContext context, 43 | ImageProvider imageProvider) { 44 | return Material( 45 | elevation: 4, 46 | shape: const CircleBorder(), 47 | child: Container( 48 | decoration: BoxDecoration( 49 | image: DecorationImage(image: imageProvider), 50 | shape: BoxShape.circle), 51 | ), 52 | ); 53 | }, 54 | ), 55 | Padding( 56 | padding: const EdgeInsets.all(8.0), 57 | child: Column( 58 | crossAxisAlignment: CrossAxisAlignment.start, 59 | children: [ 60 | Text( 61 | article.title.substring(0, min(10, article.title.length)), 62 | style: Theme.of(context).textTheme.headline6, 63 | ), 64 | const SizedBox( 65 | height: 8, 66 | ), 67 | Text( 68 | article.abstract 69 | .substring(0, min(40, article.abstract.length)), 70 | style: Theme.of(context).textTheme.subtitle2, 71 | ), 72 | ], 73 | ), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ), 79 | onTap: () => onItemClicked(context), 80 | ); 81 | } 82 | 83 | void onItemClicked(BuildContext context) { 84 | Navigator.pushNamed(context, ArticleDetail.routeKey, arguments: article); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/common/constant.dart: -------------------------------------------------------------------------------- 1 | const String defaultImage = 2 | 'https://cdn.pixabay.com/photo/2019/12/14/07/21/mountain-4694346_960_720.png'; 3 | const String apiKey = 'IKbXn4bCOGQFKxbxLwE6C2mPuNzkV3po'; 4 | const String baseUrl = 'https://api.nytimes.com/svc/'; 5 | -------------------------------------------------------------------------------- /lib/core/error.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'error.freezed.dart'; 4 | 5 | @freezed 6 | class Error with _$Error { 7 | const factory Error.httpInternalServerError(String errorBody) = 8 | HttpInternalServerError; 9 | 10 | const factory Error.httpUnAuthorizedError() = HttpUnAuthorizedError; 11 | 12 | const factory Error.httpUnknownError(String message) = HttpUnknownError; 13 | } 14 | -------------------------------------------------------------------------------- /lib/core/error.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // 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 4 | 5 | part of 'error.dart'; 6 | 7 | // ************************************************************************** 8 | // FreezedGenerator 9 | // ************************************************************************** 10 | 11 | T _$identity(T value) => value; 12 | 13 | final _privateConstructorUsedError = UnsupportedError( 14 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 15 | 16 | /// @nodoc 17 | class _$ErrorTearOff { 18 | const _$ErrorTearOff(); 19 | 20 | HttpInternalServerError httpInternalServerError(String errorBody) { 21 | return HttpInternalServerError( 22 | errorBody, 23 | ); 24 | } 25 | 26 | HttpUnAuthorizedError httpUnAuthorizedError() { 27 | return const HttpUnAuthorizedError(); 28 | } 29 | 30 | HttpUnknownError httpUnknownError(String message) { 31 | return HttpUnknownError( 32 | message, 33 | ); 34 | } 35 | } 36 | 37 | /// @nodoc 38 | const $Error = _$ErrorTearOff(); 39 | 40 | /// @nodoc 41 | mixin _$Error { 42 | @optionalTypeArgs 43 | TResult when({ 44 | required TResult Function(String errorBody) httpInternalServerError, 45 | required TResult Function() httpUnAuthorizedError, 46 | required TResult Function(String message) httpUnknownError, 47 | }) => 48 | throw _privateConstructorUsedError; 49 | @optionalTypeArgs 50 | TResult? whenOrNull({ 51 | TResult Function(String errorBody)? httpInternalServerError, 52 | TResult Function()? httpUnAuthorizedError, 53 | TResult Function(String message)? httpUnknownError, 54 | }) => 55 | throw _privateConstructorUsedError; 56 | @optionalTypeArgs 57 | TResult maybeWhen({ 58 | TResult Function(String errorBody)? httpInternalServerError, 59 | TResult Function()? httpUnAuthorizedError, 60 | TResult Function(String message)? httpUnknownError, 61 | required TResult orElse(), 62 | }) => 63 | throw _privateConstructorUsedError; 64 | @optionalTypeArgs 65 | TResult map({ 66 | required TResult Function(HttpInternalServerError value) 67 | httpInternalServerError, 68 | required TResult Function(HttpUnAuthorizedError value) 69 | httpUnAuthorizedError, 70 | required TResult Function(HttpUnknownError value) httpUnknownError, 71 | }) => 72 | throw _privateConstructorUsedError; 73 | @optionalTypeArgs 74 | TResult? mapOrNull({ 75 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 76 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 77 | TResult Function(HttpUnknownError value)? httpUnknownError, 78 | }) => 79 | throw _privateConstructorUsedError; 80 | @optionalTypeArgs 81 | TResult maybeMap({ 82 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 83 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 84 | TResult Function(HttpUnknownError value)? httpUnknownError, 85 | required TResult orElse(), 86 | }) => 87 | throw _privateConstructorUsedError; 88 | } 89 | 90 | /// @nodoc 91 | abstract class $ErrorCopyWith<$Res> { 92 | factory $ErrorCopyWith(Error value, $Res Function(Error) then) = 93 | _$ErrorCopyWithImpl<$Res>; 94 | } 95 | 96 | /// @nodoc 97 | class _$ErrorCopyWithImpl<$Res> implements $ErrorCopyWith<$Res> { 98 | _$ErrorCopyWithImpl(this._value, this._then); 99 | 100 | final Error _value; 101 | // ignore: unused_field 102 | final $Res Function(Error) _then; 103 | } 104 | 105 | /// @nodoc 106 | abstract class $HttpInternalServerErrorCopyWith<$Res> { 107 | factory $HttpInternalServerErrorCopyWith(HttpInternalServerError value, 108 | $Res Function(HttpInternalServerError) then) = 109 | _$HttpInternalServerErrorCopyWithImpl<$Res>; 110 | $Res call({String errorBody}); 111 | } 112 | 113 | /// @nodoc 114 | class _$HttpInternalServerErrorCopyWithImpl<$Res> 115 | extends _$ErrorCopyWithImpl<$Res> 116 | implements $HttpInternalServerErrorCopyWith<$Res> { 117 | _$HttpInternalServerErrorCopyWithImpl(HttpInternalServerError _value, 118 | $Res Function(HttpInternalServerError) _then) 119 | : super(_value, (v) => _then(v as HttpInternalServerError)); 120 | 121 | @override 122 | HttpInternalServerError get _value => super._value as HttpInternalServerError; 123 | 124 | @override 125 | $Res call({ 126 | Object? errorBody = freezed, 127 | }) { 128 | return _then(HttpInternalServerError( 129 | errorBody == freezed 130 | ? _value.errorBody 131 | : errorBody // ignore: cast_nullable_to_non_nullable 132 | as String, 133 | )); 134 | } 135 | } 136 | 137 | /// @nodoc 138 | 139 | class _$HttpInternalServerError implements HttpInternalServerError { 140 | const _$HttpInternalServerError(this.errorBody); 141 | 142 | @override 143 | final String errorBody; 144 | 145 | @override 146 | String toString() { 147 | return 'Error.httpInternalServerError(errorBody: $errorBody)'; 148 | } 149 | 150 | @override 151 | bool operator ==(dynamic other) { 152 | return identical(this, other) || 153 | (other.runtimeType == runtimeType && 154 | other is HttpInternalServerError && 155 | (identical(other.errorBody, errorBody) || 156 | other.errorBody == errorBody)); 157 | } 158 | 159 | @override 160 | int get hashCode => Object.hash(runtimeType, errorBody); 161 | 162 | @JsonKey(ignore: true) 163 | @override 164 | $HttpInternalServerErrorCopyWith get copyWith => 165 | _$HttpInternalServerErrorCopyWithImpl( 166 | this, _$identity); 167 | 168 | @override 169 | @optionalTypeArgs 170 | TResult when({ 171 | required TResult Function(String errorBody) httpInternalServerError, 172 | required TResult Function() httpUnAuthorizedError, 173 | required TResult Function(String message) httpUnknownError, 174 | }) { 175 | return httpInternalServerError(errorBody); 176 | } 177 | 178 | @override 179 | @optionalTypeArgs 180 | TResult? whenOrNull({ 181 | TResult Function(String errorBody)? httpInternalServerError, 182 | TResult Function()? httpUnAuthorizedError, 183 | TResult Function(String message)? httpUnknownError, 184 | }) { 185 | return httpInternalServerError?.call(errorBody); 186 | } 187 | 188 | @override 189 | @optionalTypeArgs 190 | TResult maybeWhen({ 191 | TResult Function(String errorBody)? httpInternalServerError, 192 | TResult Function()? httpUnAuthorizedError, 193 | TResult Function(String message)? httpUnknownError, 194 | required TResult orElse(), 195 | }) { 196 | if (httpInternalServerError != null) { 197 | return httpInternalServerError(errorBody); 198 | } 199 | return orElse(); 200 | } 201 | 202 | @override 203 | @optionalTypeArgs 204 | TResult map({ 205 | required TResult Function(HttpInternalServerError value) 206 | httpInternalServerError, 207 | required TResult Function(HttpUnAuthorizedError value) 208 | httpUnAuthorizedError, 209 | required TResult Function(HttpUnknownError value) httpUnknownError, 210 | }) { 211 | return httpInternalServerError(this); 212 | } 213 | 214 | @override 215 | @optionalTypeArgs 216 | TResult? mapOrNull({ 217 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 218 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 219 | TResult Function(HttpUnknownError value)? httpUnknownError, 220 | }) { 221 | return httpInternalServerError?.call(this); 222 | } 223 | 224 | @override 225 | @optionalTypeArgs 226 | TResult maybeMap({ 227 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 228 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 229 | TResult Function(HttpUnknownError value)? httpUnknownError, 230 | required TResult orElse(), 231 | }) { 232 | if (httpInternalServerError != null) { 233 | return httpInternalServerError(this); 234 | } 235 | return orElse(); 236 | } 237 | } 238 | 239 | abstract class HttpInternalServerError implements Error { 240 | const factory HttpInternalServerError(String errorBody) = 241 | _$HttpInternalServerError; 242 | 243 | String get errorBody; 244 | @JsonKey(ignore: true) 245 | $HttpInternalServerErrorCopyWith get copyWith => 246 | throw _privateConstructorUsedError; 247 | } 248 | 249 | /// @nodoc 250 | abstract class $HttpUnAuthorizedErrorCopyWith<$Res> { 251 | factory $HttpUnAuthorizedErrorCopyWith(HttpUnAuthorizedError value, 252 | $Res Function(HttpUnAuthorizedError) then) = 253 | _$HttpUnAuthorizedErrorCopyWithImpl<$Res>; 254 | } 255 | 256 | /// @nodoc 257 | class _$HttpUnAuthorizedErrorCopyWithImpl<$Res> 258 | extends _$ErrorCopyWithImpl<$Res> 259 | implements $HttpUnAuthorizedErrorCopyWith<$Res> { 260 | _$HttpUnAuthorizedErrorCopyWithImpl( 261 | HttpUnAuthorizedError _value, $Res Function(HttpUnAuthorizedError) _then) 262 | : super(_value, (v) => _then(v as HttpUnAuthorizedError)); 263 | 264 | @override 265 | HttpUnAuthorizedError get _value => super._value as HttpUnAuthorizedError; 266 | } 267 | 268 | /// @nodoc 269 | 270 | class _$HttpUnAuthorizedError implements HttpUnAuthorizedError { 271 | const _$HttpUnAuthorizedError(); 272 | 273 | @override 274 | String toString() { 275 | return 'Error.httpUnAuthorizedError()'; 276 | } 277 | 278 | @override 279 | bool operator ==(dynamic other) { 280 | return identical(this, other) || 281 | (other.runtimeType == runtimeType && other is HttpUnAuthorizedError); 282 | } 283 | 284 | @override 285 | int get hashCode => runtimeType.hashCode; 286 | 287 | @override 288 | @optionalTypeArgs 289 | TResult when({ 290 | required TResult Function(String errorBody) httpInternalServerError, 291 | required TResult Function() httpUnAuthorizedError, 292 | required TResult Function(String message) httpUnknownError, 293 | }) { 294 | return httpUnAuthorizedError(); 295 | } 296 | 297 | @override 298 | @optionalTypeArgs 299 | TResult? whenOrNull({ 300 | TResult Function(String errorBody)? httpInternalServerError, 301 | TResult Function()? httpUnAuthorizedError, 302 | TResult Function(String message)? httpUnknownError, 303 | }) { 304 | return httpUnAuthorizedError?.call(); 305 | } 306 | 307 | @override 308 | @optionalTypeArgs 309 | TResult maybeWhen({ 310 | TResult Function(String errorBody)? httpInternalServerError, 311 | TResult Function()? httpUnAuthorizedError, 312 | TResult Function(String message)? httpUnknownError, 313 | required TResult orElse(), 314 | }) { 315 | if (httpUnAuthorizedError != null) { 316 | return httpUnAuthorizedError(); 317 | } 318 | return orElse(); 319 | } 320 | 321 | @override 322 | @optionalTypeArgs 323 | TResult map({ 324 | required TResult Function(HttpInternalServerError value) 325 | httpInternalServerError, 326 | required TResult Function(HttpUnAuthorizedError value) 327 | httpUnAuthorizedError, 328 | required TResult Function(HttpUnknownError value) httpUnknownError, 329 | }) { 330 | return httpUnAuthorizedError(this); 331 | } 332 | 333 | @override 334 | @optionalTypeArgs 335 | TResult? mapOrNull({ 336 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 337 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 338 | TResult Function(HttpUnknownError value)? httpUnknownError, 339 | }) { 340 | return httpUnAuthorizedError?.call(this); 341 | } 342 | 343 | @override 344 | @optionalTypeArgs 345 | TResult maybeMap({ 346 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 347 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 348 | TResult Function(HttpUnknownError value)? httpUnknownError, 349 | required TResult orElse(), 350 | }) { 351 | if (httpUnAuthorizedError != null) { 352 | return httpUnAuthorizedError(this); 353 | } 354 | return orElse(); 355 | } 356 | } 357 | 358 | abstract class HttpUnAuthorizedError implements Error { 359 | const factory HttpUnAuthorizedError() = _$HttpUnAuthorizedError; 360 | } 361 | 362 | /// @nodoc 363 | abstract class $HttpUnknownErrorCopyWith<$Res> { 364 | factory $HttpUnknownErrorCopyWith( 365 | HttpUnknownError value, $Res Function(HttpUnknownError) then) = 366 | _$HttpUnknownErrorCopyWithImpl<$Res>; 367 | $Res call({String message}); 368 | } 369 | 370 | /// @nodoc 371 | class _$HttpUnknownErrorCopyWithImpl<$Res> extends _$ErrorCopyWithImpl<$Res> 372 | implements $HttpUnknownErrorCopyWith<$Res> { 373 | _$HttpUnknownErrorCopyWithImpl( 374 | HttpUnknownError _value, $Res Function(HttpUnknownError) _then) 375 | : super(_value, (v) => _then(v as HttpUnknownError)); 376 | 377 | @override 378 | HttpUnknownError get _value => super._value as HttpUnknownError; 379 | 380 | @override 381 | $Res call({ 382 | Object? message = freezed, 383 | }) { 384 | return _then(HttpUnknownError( 385 | message == freezed 386 | ? _value.message 387 | : message // ignore: cast_nullable_to_non_nullable 388 | as String, 389 | )); 390 | } 391 | } 392 | 393 | /// @nodoc 394 | 395 | class _$HttpUnknownError implements HttpUnknownError { 396 | const _$HttpUnknownError(this.message); 397 | 398 | @override 399 | final String message; 400 | 401 | @override 402 | String toString() { 403 | return 'Error.httpUnknownError(message: $message)'; 404 | } 405 | 406 | @override 407 | bool operator ==(dynamic other) { 408 | return identical(this, other) || 409 | (other.runtimeType == runtimeType && 410 | other is HttpUnknownError && 411 | (identical(other.message, message) || other.message == message)); 412 | } 413 | 414 | @override 415 | int get hashCode => Object.hash(runtimeType, message); 416 | 417 | @JsonKey(ignore: true) 418 | @override 419 | $HttpUnknownErrorCopyWith get copyWith => 420 | _$HttpUnknownErrorCopyWithImpl(this, _$identity); 421 | 422 | @override 423 | @optionalTypeArgs 424 | TResult when({ 425 | required TResult Function(String errorBody) httpInternalServerError, 426 | required TResult Function() httpUnAuthorizedError, 427 | required TResult Function(String message) httpUnknownError, 428 | }) { 429 | return httpUnknownError(message); 430 | } 431 | 432 | @override 433 | @optionalTypeArgs 434 | TResult? whenOrNull({ 435 | TResult Function(String errorBody)? httpInternalServerError, 436 | TResult Function()? httpUnAuthorizedError, 437 | TResult Function(String message)? httpUnknownError, 438 | }) { 439 | return httpUnknownError?.call(message); 440 | } 441 | 442 | @override 443 | @optionalTypeArgs 444 | TResult maybeWhen({ 445 | TResult Function(String errorBody)? httpInternalServerError, 446 | TResult Function()? httpUnAuthorizedError, 447 | TResult Function(String message)? httpUnknownError, 448 | required TResult orElse(), 449 | }) { 450 | if (httpUnknownError != null) { 451 | return httpUnknownError(message); 452 | } 453 | return orElse(); 454 | } 455 | 456 | @override 457 | @optionalTypeArgs 458 | TResult map({ 459 | required TResult Function(HttpInternalServerError value) 460 | httpInternalServerError, 461 | required TResult Function(HttpUnAuthorizedError value) 462 | httpUnAuthorizedError, 463 | required TResult Function(HttpUnknownError value) httpUnknownError, 464 | }) { 465 | return httpUnknownError(this); 466 | } 467 | 468 | @override 469 | @optionalTypeArgs 470 | TResult? mapOrNull({ 471 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 472 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 473 | TResult Function(HttpUnknownError value)? httpUnknownError, 474 | }) { 475 | return httpUnknownError?.call(this); 476 | } 477 | 478 | @override 479 | @optionalTypeArgs 480 | TResult maybeMap({ 481 | TResult Function(HttpInternalServerError value)? httpInternalServerError, 482 | TResult Function(HttpUnAuthorizedError value)? httpUnAuthorizedError, 483 | TResult Function(HttpUnknownError value)? httpUnknownError, 484 | required TResult orElse(), 485 | }) { 486 | if (httpUnknownError != null) { 487 | return httpUnknownError(this); 488 | } 489 | return orElse(); 490 | } 491 | } 492 | 493 | abstract class HttpUnknownError implements Error { 494 | const factory HttpUnknownError(String message) = _$HttpUnknownError; 495 | 496 | String get message; 497 | @JsonKey(ignore: true) 498 | $HttpUnknownErrorCopyWith get copyWith => 499 | throw _privateConstructorUsedError; 500 | } 501 | -------------------------------------------------------------------------------- /lib/di/app_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import '../common/constant.dart'; 4 | 5 | @module 6 | abstract class AppModule { 7 | @singleton 8 | Dio get dio => Dio(BaseOptions(baseUrl: baseUrl)); 9 | } 10 | -------------------------------------------------------------------------------- /lib/di/di_setup.config.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // InjectableConfigGenerator 5 | // ************************************************************************** 6 | 7 | import 'package:dio/dio.dart' as _i3; 8 | import 'package:get_it/get_it.dart' as _i1; 9 | import 'package:injectable/injectable.dart' as _i2; 10 | 11 | import '../articles_list/data/remote/service/article_service.dart' as _i4; 12 | import '../articles_list/data/remote/source/article_remote_data_source.dart' 13 | as _i5; 14 | import '../articles_list/data/remote/source/article_remote_data_source_impl.dart' 15 | as _i6; 16 | import '../articles_list/data/repository/article_repo_impl.dart' as _i8; 17 | import '../articles_list/domain/repository/article_repo.dart' as _i7; 18 | import '../articles_list/domain/usecase/article_usecase.dart' as _i9; 19 | import '../articles_list/domain/usecase/article_usecase_impl.dart' as _i10; 20 | import '../articles_list/presentation/bloc/article_list_bloc.dart' as _i11; 21 | import 'app_module.dart' as _i12; // ignore_for_file: unnecessary_lambdas 22 | 23 | // ignore_for_file: lines_longer_than_80_chars 24 | /// initializes the registration of provided dependencies inside of [GetIt] 25 | _i1.GetIt $initGetIt(_i1.GetIt get, 26 | {String? environment, _i2.EnvironmentFilter? environmentFilter}) { 27 | final gh = _i2.GetItHelper(get, environment, environmentFilter); 28 | final appModule = _$AppModule(); 29 | gh.singleton<_i3.Dio>(appModule.dio); 30 | gh.factory<_i4.ArticleService>(() => _i4.ArticleService(get<_i3.Dio>())); 31 | gh.factory<_i5.ArticleRemoteDataSource>( 32 | () => _i6.ArticleRemoteDataSourceImpl(get<_i4.ArticleService>())); 33 | gh.factory<_i7.ArticleRepo>( 34 | () => _i8.ArticleRepoImpl(get<_i5.ArticleRemoteDataSource>())); 35 | gh.factory<_i9.ArticleUseCase>( 36 | () => _i10.ArticleUseCaseImpl(get<_i7.ArticleRepo>())); 37 | gh.factory<_i11.ArticleListBloc>( 38 | () => _i11.ArticleListBloc(get<_i9.ArticleUseCase>())); 39 | return get; 40 | } 41 | 42 | class _$AppModule extends _i12.AppModule {} 43 | -------------------------------------------------------------------------------- /lib/di/di_setup.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:get_it/get_it.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | import 'di_setup.config.dart'; 6 | 7 | final GetIt getIt = GetIt.instance; 8 | 9 | @InjectableInit( 10 | initializerName: r'$initGetIt', // default 11 | preferRelativeImports: true, // default 12 | asExtension: false, // default 13 | ) 14 | void configureDependencies({String env = Environment.dev}) => $initGetIt(getIt, environment: env); 15 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'article_detail/presentation/screen/article_detail.dart'; 4 | import 'articles_list/data/model/article.dart'; 5 | import 'articles_list/presentation/screen/articles_list_screen.dart'; 6 | import 'di/di_setup.dart'; 7 | 8 | Future main() async { 9 | configureDependencies(); 10 | runApp(const MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | const MyApp({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return MaterialApp( 19 | title: 'Flutter Demo', 20 | theme: ThemeData( 21 | primarySwatch: Colors.blue, 22 | ), 23 | home: const ArticlesListScreen(title: 'Flutter Demo Home Page'), 24 | routes: { 25 | ArticleDetail.routeKey: (BuildContext context) => ArticleDetail( 26 | ModalRoute.of(context)!.settings.arguments! as Article), 27 | }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "31.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.8.0" 18 | archive: 19 | dependency: transitive 20 | description: 21 | name: archive 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "3.3.0" 25 | args: 26 | dependency: transitive 27 | description: 28 | name: args 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.3.1" 32 | async: 33 | dependency: transitive 34 | description: 35 | name: async 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.9.0" 39 | bloc: 40 | dependency: transitive 41 | description: 42 | name: bloc 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "8.0.3" 46 | boolean_selector: 47 | dependency: transitive 48 | description: 49 | name: boolean_selector 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.0" 53 | build: 54 | dependency: transitive 55 | description: 56 | name: build 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.3.0" 60 | build_config: 61 | dependency: transitive 62 | description: 63 | name: build_config 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.1.0" 67 | build_daemon: 68 | dependency: transitive 69 | description: 70 | name: build_daemon 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "3.1.0" 74 | build_resolvers: 75 | dependency: transitive 76 | description: 77 | name: build_resolvers 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "2.0.6" 81 | build_runner: 82 | dependency: "direct dev" 83 | description: 84 | name: build_runner 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "2.2.0" 88 | build_runner_core: 89 | dependency: transitive 90 | description: 91 | name: build_runner_core 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "7.2.3" 95 | built_collection: 96 | dependency: transitive 97 | description: 98 | name: built_collection 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "5.1.1" 102 | built_value: 103 | dependency: transitive 104 | description: 105 | name: built_value 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "8.4.0" 109 | cached_network_image: 110 | dependency: "direct main" 111 | description: 112 | name: cached_network_image 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "3.2.1" 116 | cached_network_image_platform_interface: 117 | dependency: transitive 118 | description: 119 | name: cached_network_image_platform_interface 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.0.0" 123 | cached_network_image_web: 124 | dependency: transitive 125 | description: 126 | name: cached_network_image_web 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "1.0.1" 130 | characters: 131 | dependency: transitive 132 | description: 133 | name: characters 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.2.1" 137 | checked_yaml: 138 | dependency: transitive 139 | description: 140 | name: checked_yaml 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "2.0.1" 144 | cli_util: 145 | dependency: transitive 146 | description: 147 | name: cli_util 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "0.3.5" 151 | clock: 152 | dependency: transitive 153 | description: 154 | name: clock 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "1.1.1" 158 | code_builder: 159 | dependency: transitive 160 | description: 161 | name: code_builder 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "4.1.0" 165 | collection: 166 | dependency: transitive 167 | description: 168 | name: collection 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "1.16.0" 172 | convert: 173 | dependency: transitive 174 | description: 175 | name: convert 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "3.0.2" 179 | crypto: 180 | dependency: transitive 181 | description: 182 | name: crypto 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "3.0.2" 186 | cupertino_icons: 187 | dependency: "direct main" 188 | description: 189 | name: cupertino_icons 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "1.0.5" 193 | dart_style: 194 | dependency: transitive 195 | description: 196 | name: dart_style 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "2.2.1" 200 | dartz: 201 | dependency: "direct main" 202 | description: 203 | name: dartz 204 | url: "https://pub.dartlang.org" 205 | source: hosted 206 | version: "0.10.1" 207 | dio: 208 | dependency: "direct main" 209 | description: 210 | name: dio 211 | url: "https://pub.dartlang.org" 212 | source: hosted 213 | version: "4.0.6" 214 | fake_async: 215 | dependency: transitive 216 | description: 217 | name: fake_async 218 | url: "https://pub.dartlang.org" 219 | source: hosted 220 | version: "1.3.1" 221 | ffi: 222 | dependency: transitive 223 | description: 224 | name: ffi 225 | url: "https://pub.dartlang.org" 226 | source: hosted 227 | version: "2.0.1" 228 | file: 229 | dependency: transitive 230 | description: 231 | name: file 232 | url: "https://pub.dartlang.org" 233 | source: hosted 234 | version: "6.1.2" 235 | fixnum: 236 | dependency: transitive 237 | description: 238 | name: fixnum 239 | url: "https://pub.dartlang.org" 240 | source: hosted 241 | version: "1.0.1" 242 | flutter: 243 | dependency: "direct main" 244 | description: flutter 245 | source: sdk 246 | version: "0.0.0" 247 | flutter_bloc: 248 | dependency: "direct main" 249 | description: 250 | name: flutter_bloc 251 | url: "https://pub.dartlang.org" 252 | source: hosted 253 | version: "8.0.1" 254 | flutter_blurhash: 255 | dependency: transitive 256 | description: 257 | name: flutter_blurhash 258 | url: "https://pub.dartlang.org" 259 | source: hosted 260 | version: "0.7.0" 261 | flutter_cache_manager: 262 | dependency: transitive 263 | description: 264 | name: flutter_cache_manager 265 | url: "https://pub.dartlang.org" 266 | source: hosted 267 | version: "3.3.0" 268 | flutter_driver: 269 | dependency: transitive 270 | description: flutter 271 | source: sdk 272 | version: "0.0.0" 273 | flutter_test: 274 | dependency: "direct dev" 275 | description: flutter 276 | source: sdk 277 | version: "0.0.0" 278 | flutter_web_plugins: 279 | dependency: transitive 280 | description: flutter 281 | source: sdk 282 | version: "0.0.0" 283 | freezed: 284 | dependency: "direct dev" 285 | description: 286 | name: freezed 287 | url: "https://pub.dartlang.org" 288 | source: hosted 289 | version: "1.1.0" 290 | freezed_annotation: 291 | dependency: "direct main" 292 | description: 293 | name: freezed_annotation 294 | url: "https://pub.dartlang.org" 295 | source: hosted 296 | version: "1.1.0" 297 | frontend_server_client: 298 | dependency: transitive 299 | description: 300 | name: frontend_server_client 301 | url: "https://pub.dartlang.org" 302 | source: hosted 303 | version: "2.1.3" 304 | fuchsia_remote_debug_protocol: 305 | dependency: transitive 306 | description: flutter 307 | source: sdk 308 | version: "0.0.0" 309 | get_it: 310 | dependency: "direct main" 311 | description: 312 | name: get_it 313 | url: "https://pub.dartlang.org" 314 | source: hosted 315 | version: "7.2.0" 316 | glob: 317 | dependency: transitive 318 | description: 319 | name: glob 320 | url: "https://pub.dartlang.org" 321 | source: hosted 322 | version: "2.1.0" 323 | graphs: 324 | dependency: transitive 325 | description: 326 | name: graphs 327 | url: "https://pub.dartlang.org" 328 | source: hosted 329 | version: "2.1.0" 330 | http: 331 | dependency: transitive 332 | description: 333 | name: http 334 | url: "https://pub.dartlang.org" 335 | source: hosted 336 | version: "0.13.4" 337 | http_multi_server: 338 | dependency: transitive 339 | description: 340 | name: http_multi_server 341 | url: "https://pub.dartlang.org" 342 | source: hosted 343 | version: "3.2.1" 344 | http_parser: 345 | dependency: transitive 346 | description: 347 | name: http_parser 348 | url: "https://pub.dartlang.org" 349 | source: hosted 350 | version: "4.0.1" 351 | injectable: 352 | dependency: "direct main" 353 | description: 354 | name: injectable 355 | url: "https://pub.dartlang.org" 356 | source: hosted 357 | version: "1.5.3" 358 | injectable_generator: 359 | dependency: "direct dev" 360 | description: 361 | name: injectable_generator 362 | url: "https://pub.dartlang.org" 363 | source: hosted 364 | version: "1.5.2" 365 | integration_test: 366 | dependency: "direct dev" 367 | description: flutter 368 | source: sdk 369 | version: "0.0.0" 370 | io: 371 | dependency: transitive 372 | description: 373 | name: io 374 | url: "https://pub.dartlang.org" 375 | source: hosted 376 | version: "1.0.3" 377 | js: 378 | dependency: transitive 379 | description: 380 | name: js 381 | url: "https://pub.dartlang.org" 382 | source: hosted 383 | version: "0.6.4" 384 | json_annotation: 385 | dependency: "direct main" 386 | description: 387 | name: json_annotation 388 | url: "https://pub.dartlang.org" 389 | source: hosted 390 | version: "4.6.0" 391 | json_serializable: 392 | dependency: "direct dev" 393 | description: 394 | name: json_serializable 395 | url: "https://pub.dartlang.org" 396 | source: hosted 397 | version: "6.3.1" 398 | lint: 399 | dependency: "direct dev" 400 | description: 401 | name: lint 402 | url: "https://pub.dartlang.org" 403 | source: hosted 404 | version: "1.8.2" 405 | logger: 406 | dependency: "direct main" 407 | description: 408 | name: logger 409 | url: "https://pub.dartlang.org" 410 | source: hosted 411 | version: "1.1.0" 412 | logging: 413 | dependency: transitive 414 | description: 415 | name: logging 416 | url: "https://pub.dartlang.org" 417 | source: hosted 418 | version: "1.0.2" 419 | matcher: 420 | dependency: transitive 421 | description: 422 | name: matcher 423 | url: "https://pub.dartlang.org" 424 | source: hosted 425 | version: "0.12.12" 426 | material_color_utilities: 427 | dependency: transitive 428 | description: 429 | name: material_color_utilities 430 | url: "https://pub.dartlang.org" 431 | source: hosted 432 | version: "0.1.5" 433 | meta: 434 | dependency: transitive 435 | description: 436 | name: meta 437 | url: "https://pub.dartlang.org" 438 | source: hosted 439 | version: "1.8.0" 440 | mime: 441 | dependency: transitive 442 | description: 443 | name: mime 444 | url: "https://pub.dartlang.org" 445 | source: hosted 446 | version: "1.0.2" 447 | mockito: 448 | dependency: "direct dev" 449 | description: 450 | name: mockito 451 | url: "https://pub.dartlang.org" 452 | source: hosted 453 | version: "5.2.0" 454 | nested: 455 | dependency: transitive 456 | description: 457 | name: nested 458 | url: "https://pub.dartlang.org" 459 | source: hosted 460 | version: "1.0.0" 461 | octo_image: 462 | dependency: transitive 463 | description: 464 | name: octo_image 465 | url: "https://pub.dartlang.org" 466 | source: hosted 467 | version: "1.0.2" 468 | package_config: 469 | dependency: transitive 470 | description: 471 | name: package_config 472 | url: "https://pub.dartlang.org" 473 | source: hosted 474 | version: "2.1.0" 475 | path: 476 | dependency: transitive 477 | description: 478 | name: path 479 | url: "https://pub.dartlang.org" 480 | source: hosted 481 | version: "1.8.2" 482 | path_provider: 483 | dependency: transitive 484 | description: 485 | name: path_provider 486 | url: "https://pub.dartlang.org" 487 | source: hosted 488 | version: "2.0.11" 489 | path_provider_android: 490 | dependency: transitive 491 | description: 492 | name: path_provider_android 493 | url: "https://pub.dartlang.org" 494 | source: hosted 495 | version: "2.0.16" 496 | path_provider_ios: 497 | dependency: transitive 498 | description: 499 | name: path_provider_ios 500 | url: "https://pub.dartlang.org" 501 | source: hosted 502 | version: "2.0.10" 503 | path_provider_linux: 504 | dependency: transitive 505 | description: 506 | name: path_provider_linux 507 | url: "https://pub.dartlang.org" 508 | source: hosted 509 | version: "2.1.7" 510 | path_provider_macos: 511 | dependency: transitive 512 | description: 513 | name: path_provider_macos 514 | url: "https://pub.dartlang.org" 515 | source: hosted 516 | version: "2.0.6" 517 | path_provider_platform_interface: 518 | dependency: transitive 519 | description: 520 | name: path_provider_platform_interface 521 | url: "https://pub.dartlang.org" 522 | source: hosted 523 | version: "2.0.4" 524 | path_provider_windows: 525 | dependency: transitive 526 | description: 527 | name: path_provider_windows 528 | url: "https://pub.dartlang.org" 529 | source: hosted 530 | version: "2.1.0" 531 | pedantic: 532 | dependency: transitive 533 | description: 534 | name: pedantic 535 | url: "https://pub.dartlang.org" 536 | source: hosted 537 | version: "1.11.1" 538 | platform: 539 | dependency: transitive 540 | description: 541 | name: platform 542 | url: "https://pub.dartlang.org" 543 | source: hosted 544 | version: "3.1.0" 545 | plugin_platform_interface: 546 | dependency: transitive 547 | description: 548 | name: plugin_platform_interface 549 | url: "https://pub.dartlang.org" 550 | source: hosted 551 | version: "2.1.2" 552 | pool: 553 | dependency: transitive 554 | description: 555 | name: pool 556 | url: "https://pub.dartlang.org" 557 | source: hosted 558 | version: "1.5.1" 559 | process: 560 | dependency: transitive 561 | description: 562 | name: process 563 | url: "https://pub.dartlang.org" 564 | source: hosted 565 | version: "4.2.4" 566 | provider: 567 | dependency: transitive 568 | description: 569 | name: provider 570 | url: "https://pub.dartlang.org" 571 | source: hosted 572 | version: "6.0.3" 573 | pub_semver: 574 | dependency: transitive 575 | description: 576 | name: pub_semver 577 | url: "https://pub.dartlang.org" 578 | source: hosted 579 | version: "2.1.1" 580 | pubspec_parse: 581 | dependency: transitive 582 | description: 583 | name: pubspec_parse 584 | url: "https://pub.dartlang.org" 585 | source: hosted 586 | version: "1.2.0" 587 | quiver: 588 | dependency: transitive 589 | description: 590 | name: quiver 591 | url: "https://pub.dartlang.org" 592 | source: hosted 593 | version: "3.1.0" 594 | retrofit: 595 | dependency: "direct main" 596 | description: 597 | name: retrofit 598 | url: "https://pub.dartlang.org" 599 | source: hosted 600 | version: "3.0.1+1" 601 | retrofit_generator: 602 | dependency: "direct dev" 603 | description: 604 | name: retrofit_generator 605 | url: "https://pub.dartlang.org" 606 | source: hosted 607 | version: "3.0.1+1" 608 | rxdart: 609 | dependency: transitive 610 | description: 611 | name: rxdart 612 | url: "https://pub.dartlang.org" 613 | source: hosted 614 | version: "0.27.5" 615 | shared_preferences: 616 | dependency: "direct main" 617 | description: 618 | name: shared_preferences 619 | url: "https://pub.dartlang.org" 620 | source: hosted 621 | version: "2.0.15" 622 | shared_preferences_android: 623 | dependency: transitive 624 | description: 625 | name: shared_preferences_android 626 | url: "https://pub.dartlang.org" 627 | source: hosted 628 | version: "2.0.12" 629 | shared_preferences_ios: 630 | dependency: transitive 631 | description: 632 | name: shared_preferences_ios 633 | url: "https://pub.dartlang.org" 634 | source: hosted 635 | version: "2.1.1" 636 | shared_preferences_linux: 637 | dependency: transitive 638 | description: 639 | name: shared_preferences_linux 640 | url: "https://pub.dartlang.org" 641 | source: hosted 642 | version: "2.1.1" 643 | shared_preferences_macos: 644 | dependency: transitive 645 | description: 646 | name: shared_preferences_macos 647 | url: "https://pub.dartlang.org" 648 | source: hosted 649 | version: "2.0.4" 650 | shared_preferences_platform_interface: 651 | dependency: transitive 652 | description: 653 | name: shared_preferences_platform_interface 654 | url: "https://pub.dartlang.org" 655 | source: hosted 656 | version: "2.0.0" 657 | shared_preferences_web: 658 | dependency: transitive 659 | description: 660 | name: shared_preferences_web 661 | url: "https://pub.dartlang.org" 662 | source: hosted 663 | version: "2.0.4" 664 | shared_preferences_windows: 665 | dependency: transitive 666 | description: 667 | name: shared_preferences_windows 668 | url: "https://pub.dartlang.org" 669 | source: hosted 670 | version: "2.1.1" 671 | shelf: 672 | dependency: transitive 673 | description: 674 | name: shelf 675 | url: "https://pub.dartlang.org" 676 | source: hosted 677 | version: "1.3.1" 678 | shelf_web_socket: 679 | dependency: transitive 680 | description: 681 | name: shelf_web_socket 682 | url: "https://pub.dartlang.org" 683 | source: hosted 684 | version: "1.0.2" 685 | sky_engine: 686 | dependency: transitive 687 | description: flutter 688 | source: sdk 689 | version: "0.0.99" 690 | source_gen: 691 | dependency: transitive 692 | description: 693 | name: source_gen 694 | url: "https://pub.dartlang.org" 695 | source: hosted 696 | version: "1.2.2" 697 | source_helper: 698 | dependency: transitive 699 | description: 700 | name: source_helper 701 | url: "https://pub.dartlang.org" 702 | source: hosted 703 | version: "1.3.2" 704 | source_span: 705 | dependency: transitive 706 | description: 707 | name: source_span 708 | url: "https://pub.dartlang.org" 709 | source: hosted 710 | version: "1.9.0" 711 | sqflite: 712 | dependency: transitive 713 | description: 714 | name: sqflite 715 | url: "https://pub.dartlang.org" 716 | source: hosted 717 | version: "2.0.3" 718 | sqflite_common: 719 | dependency: transitive 720 | description: 721 | name: sqflite_common 722 | url: "https://pub.dartlang.org" 723 | source: hosted 724 | version: "2.2.1+1" 725 | stack_trace: 726 | dependency: transitive 727 | description: 728 | name: stack_trace 729 | url: "https://pub.dartlang.org" 730 | source: hosted 731 | version: "1.10.0" 732 | stream_channel: 733 | dependency: transitive 734 | description: 735 | name: stream_channel 736 | url: "https://pub.dartlang.org" 737 | source: hosted 738 | version: "2.1.0" 739 | stream_transform: 740 | dependency: transitive 741 | description: 742 | name: stream_transform 743 | url: "https://pub.dartlang.org" 744 | source: hosted 745 | version: "2.0.0" 746 | string_scanner: 747 | dependency: transitive 748 | description: 749 | name: string_scanner 750 | url: "https://pub.dartlang.org" 751 | source: hosted 752 | version: "1.1.1" 753 | sync_http: 754 | dependency: transitive 755 | description: 756 | name: sync_http 757 | url: "https://pub.dartlang.org" 758 | source: hosted 759 | version: "0.3.1" 760 | synchronized: 761 | dependency: transitive 762 | description: 763 | name: synchronized 764 | url: "https://pub.dartlang.org" 765 | source: hosted 766 | version: "3.0.0+2" 767 | term_glyph: 768 | dependency: transitive 769 | description: 770 | name: term_glyph 771 | url: "https://pub.dartlang.org" 772 | source: hosted 773 | version: "1.2.1" 774 | test_api: 775 | dependency: transitive 776 | description: 777 | name: test_api 778 | url: "https://pub.dartlang.org" 779 | source: hosted 780 | version: "0.4.12" 781 | timing: 782 | dependency: transitive 783 | description: 784 | name: timing 785 | url: "https://pub.dartlang.org" 786 | source: hosted 787 | version: "1.0.0" 788 | tuple: 789 | dependency: transitive 790 | description: 791 | name: tuple 792 | url: "https://pub.dartlang.org" 793 | source: hosted 794 | version: "2.0.0" 795 | typed_data: 796 | dependency: transitive 797 | description: 798 | name: typed_data 799 | url: "https://pub.dartlang.org" 800 | source: hosted 801 | version: "1.3.1" 802 | uuid: 803 | dependency: transitive 804 | description: 805 | name: uuid 806 | url: "https://pub.dartlang.org" 807 | source: hosted 808 | version: "3.0.6" 809 | vector_math: 810 | dependency: transitive 811 | description: 812 | name: vector_math 813 | url: "https://pub.dartlang.org" 814 | source: hosted 815 | version: "2.1.2" 816 | vm_service: 817 | dependency: transitive 818 | description: 819 | name: vm_service 820 | url: "https://pub.dartlang.org" 821 | source: hosted 822 | version: "9.0.0" 823 | watcher: 824 | dependency: transitive 825 | description: 826 | name: watcher 827 | url: "https://pub.dartlang.org" 828 | source: hosted 829 | version: "1.0.1" 830 | web_socket_channel: 831 | dependency: transitive 832 | description: 833 | name: web_socket_channel 834 | url: "https://pub.dartlang.org" 835 | source: hosted 836 | version: "2.2.0" 837 | webdriver: 838 | dependency: transitive 839 | description: 840 | name: webdriver 841 | url: "https://pub.dartlang.org" 842 | source: hosted 843 | version: "3.0.0" 844 | win32: 845 | dependency: transitive 846 | description: 847 | name: win32 848 | url: "https://pub.dartlang.org" 849 | source: hosted 850 | version: "2.7.0" 851 | xdg_directories: 852 | dependency: transitive 853 | description: 854 | name: xdg_directories 855 | url: "https://pub.dartlang.org" 856 | source: hosted 857 | version: "0.2.0+1" 858 | yaml: 859 | dependency: transitive 860 | description: 861 | name: yaml 862 | url: "https://pub.dartlang.org" 863 | source: hosted 864 | version: "3.1.1" 865 | sdks: 866 | dart: ">=2.17.0 <3.0.0" 867 | flutter: ">=3.0.0" 868 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nyt_flutter 2 | description: NY Times Flutter 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | injectable: ^1.5.0 33 | get_it: ^7.2.0 34 | flutter_bloc: ^8.0.0 35 | json_annotation: ^4.3.0 36 | dartz: ^0.10.0 37 | retrofit: ^3.0.0 38 | logger: ^1.1.0 39 | freezed_annotation: ^1.0.0 40 | dio: ^4.0.3 41 | shared_preferences: ^2.0.9 42 | cached_network_image: ^3.2.0 43 | 44 | # The following adds the Cupertino Icons font to your application. 45 | # Use with the CupertinoIcons class for iOS style icons. 46 | cupertino_icons: ^1.0.2 47 | 48 | dev_dependencies: 49 | flutter_test: 50 | sdk: flutter 51 | integration_test: 52 | sdk: flutter 53 | injectable_generator: ^1.5.2 54 | json_serializable: ^6.0.1 55 | retrofit_generator: ^3.0.0+2 56 | build_runner: ^2.1.5 57 | freezed: ^1.0.0 58 | mockito: ^5.0.16 59 | lint: ^1.7.0 60 | 61 | # For information on the generic Dart part of this file, see the 62 | # following page: https://dart.dev/tools/pub/pubspec 63 | 64 | # The following section is specific to Flutter. 65 | flutter: 66 | 67 | # The following line ensures that the Material Icons font is 68 | # included with your application, so that you can use the icons in 69 | # the material Icons class. 70 | uses-material-design: true 71 | 72 | # To add assets to your application, add an assets section, like this: 73 | # assets: 74 | # - images/a_dot_burr.jpeg 75 | # - images/a_dot_ham.jpeg 76 | 77 | # An image asset can refer to one or more resolution-specific "variants", see 78 | # https://flutter.dev/assets-and-images/#resolution-aware. 79 | 80 | # For articles_list regarding adding assets from package dependencies, see 81 | # https://flutter.dev/assets-and-images/#from-packages 82 | 83 | # To add custom fonts to your application, add a fonts section here, 84 | # in this "flutter" section. Each entry in this list should have a 85 | # "family" key with the font family name, and a "fonts" key with a 86 | # list giving the asset and other descriptors for the font. For 87 | # example: 88 | # fonts: 89 | # - family: Schyler 90 | # fonts: 91 | # - asset: fonts/Schyler-Regular.ttf 92 | # - asset: fonts/Schyler-Italic.ttf 93 | # style: italic 94 | # - family: Trajan Pro 95 | # fonts: 96 | # - asset: fonts/TrajanPro.ttf 97 | # - asset: fonts/TrajanPro_Bold.ttf 98 | # weight: 700 99 | # 100 | # For articles_list regarding fonts from package dependencies, 101 | # see https://flutter.dev/custom-fonts/#from-packages 102 | -------------------------------------------------------------------------------- /readme_res/bitrise-workflows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/bitrise-workflows.png -------------------------------------------------------------------------------- /readme_res/bitrise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/bitrise.png -------------------------------------------------------------------------------- /readme_res/code_coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/code_coverage.png -------------------------------------------------------------------------------- /readme_res/flutter_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/flutter_test.png -------------------------------------------------------------------------------- /readme_res/integration_test_article_detail_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/integration_test_article_detail_screen.png -------------------------------------------------------------------------------- /readme_res/integration_test_article_list_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/integration_test_article_list_screen.png -------------------------------------------------------------------------------- /readme_res/lint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/lint.png -------------------------------------------------------------------------------- /readme_res/nyt-flutter-emulator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oudaykhaled/nyt-flutter-clean-architecture-unit-test/99f180b2be1f2493832c787855c15127ab98ae9d/readme_res/nyt-flutter-emulator.gif -------------------------------------------------------------------------------- /test/unit-tests/articles_list/data/remote/article_service_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/annotations.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | import 'package:nyt_flutter/articles_list/data/model/article.dart'; 6 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart'; 7 | import 'package:nyt_flutter/articles_list/data/remote/service/article_service.dart'; 8 | import 'package:nyt_flutter/articles_list/data/remote/source/article_remote_data_source_impl.dart'; 9 | 10 | import 'article_service_impl_test.mocks.dart'; 11 | 12 | class MockArticleService extends Mock implements ArticleService {} 13 | 14 | @GenerateMocks([MockArticleService]) 15 | void main() { 16 | late MockMockArticleService mockArticleService; 17 | late final List
articles =
[ 18 | Article('title', 'abstract', 123, 'url', 'publishedData', [ 19 | Media('caption', [MediaMetaData('url', 'format')]) 20 | ]) 21 | ]; 22 | 23 | setUp(() { 24 | mockArticleService = MockMockArticleService(); 25 | }); 26 | 27 | test('requestNews should fetch news', () async { 28 | when(mockArticleService.getTasks(any)) 29 | .thenAnswer((_) async => MostPopularResponse('', '', articles)); 30 | final ArticleRemoteDataSourceImpl articleRemoteDataSource = 31 | ArticleRemoteDataSourceImpl(mockArticleService); 32 | final MostPopularResponse response = await articleRemoteDataSource.getTasks(''); 33 | expect(articles, response.articles); 34 | }); 35 | 36 | test('requestNews should fetch news', () async { 37 | when(mockArticleService.getTasks(any)) 38 | .thenThrow(DioError(requestOptions: RequestOptions(path: ''))); 39 | final ArticleRemoteDataSourceImpl articleRemoteDataSource = 40 | ArticleRemoteDataSourceImpl(mockArticleService); 41 | try { 42 | await articleRemoteDataSource.getTasks(''); 43 | assert(false); 44 | } catch (exception) { 45 | assert(true); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/unit-tests/articles_list/data/remote/article_service_impl_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.16 from annotations 2 | // in nyt_flutter/test/unit-tests/articles_list/data/remote/article_service_impl_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i4; 6 | 7 | import 'package:mockito/mockito.dart' as _i1; 8 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart' 9 | as _i2; 10 | 11 | import 'article_service_impl_test.dart' as _i3; 12 | 13 | // ignore_for_file: avoid_redundant_argument_values 14 | // ignore_for_file: avoid_setters_without_getters 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: implementation_imports 17 | // ignore_for_file: invalid_use_of_visible_for_testing_member 18 | // ignore_for_file: prefer_const_constructors 19 | // ignore_for_file: unnecessary_parenthesis 20 | // ignore_for_file: camel_case_types 21 | 22 | class _FakeMostPopularResponse_0 extends _i1.Fake 23 | implements _i2.MostPopularResponse {} 24 | 25 | /// A class which mocks [MockArticleService]. 26 | /// 27 | /// See the documentation for Mockito's code generation for more information. 28 | class MockMockArticleService extends _i1.Mock 29 | implements _i3.MockArticleService { 30 | MockMockArticleService() { 31 | _i1.throwOnMissingStub(this); 32 | } 33 | 34 | @override 35 | String toString() => super.toString(); 36 | @override 37 | _i4.Future<_i2.MostPopularResponse> getTasks(String? apiKey) => 38 | (super.noSuchMethod(Invocation.method(#getTasks, [apiKey]), 39 | returnValue: Future<_i2.MostPopularResponse>.value( 40 | _FakeMostPopularResponse_0())) 41 | as _i4.Future<_i2.MostPopularResponse>); 42 | } 43 | -------------------------------------------------------------------------------- /test/unit-tests/articles_list/data/repository/article_repo_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | import 'package:nyt_flutter/articles_list/data/model/article.dart'; 8 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart'; 9 | import 'package:nyt_flutter/articles_list/data/remote/source/article_remote_data_source.dart'; 10 | import 'package:nyt_flutter/articles_list/data/repository/article_repo_impl.dart'; 11 | import 'package:nyt_flutter/core/error.dart'; 12 | import 'article_repo_impl_test.mocks.dart'; 13 | 14 | class MockArticleRemoteDataSource extends Mock 15 | implements ArticleRemoteDataSource {} 16 | 17 | @GenerateMocks([MockArticleRemoteDataSource]) 18 | void main() { 19 | late MockMockArticleRemoteDataSource mockDataSource; 20 | late final List
articles =
[ 21 | Article('title', 'abstract', 123, 'url', 'publishedData', [ 22 | Media('caption', [ 23 | MediaMetaData('url', 'format') 24 | ]) 25 | ]) 26 | ]; 27 | 28 | setUp(() { 29 | mockDataSource = MockMockArticleRemoteDataSource(); 30 | }); 31 | 32 | test('requestNews should fetch news', () async { 33 | when(mockDataSource.getTasks(any)) 34 | .thenAnswer((_) async => MostPopularResponse('', '', articles)); 35 | final ArticleRepoImpl articleRepo = ArticleRepoImpl(mockDataSource); 36 | final Either response = await articleRepo.requestNews(); 37 | expect(articles, response.toOption().toNullable()?.articles); 38 | }); 39 | 40 | test('requestNews should fetch news with correct fields', () async { 41 | when(mockDataSource.getTasks(any)) 42 | .thenAnswer((_) async => MostPopularResponse('', '', articles)); 43 | final ArticleRepoImpl articleRepo = ArticleRepoImpl(mockDataSource); 44 | final Either response = await articleRepo.requestNews(); 45 | expect(articles, response.toOption().toNullable()?.articles); 46 | expect(response.toOption().toNullable()?.articles.first.title, 'title'); 47 | expect(response.toOption().toNullable()?.articles.first.abstract, 'abstract'); 48 | expect(response.toOption().toNullable()?.articles.first.url, 'url'); 49 | expect(response.toOption().toNullable()?.articles.first.id, 123); 50 | expect(response.toOption().toNullable()?.articles.first.publishedData, 'publishedData'); 51 | expect(response.toOption().toNullable()?.articles.first.media.first.caption, 'caption'); 52 | expect(response.toOption().toNullable()?.articles.first.media.first.metaData.first.url, 'url'); 53 | expect(response.toOption().toNullable()?.articles.first.media.first.metaData.first.format, 'format'); 54 | }); 55 | 56 | test('Article model serialization/deserialization should work properly', () async { 57 | final Map map = Article('title', 'abstract', 123, 'url', 'publishedData', []).toJson(); 58 | final Article article = Article.fromJson(map); 59 | expect(article.title, 'title'); 60 | expect(article.abstract, 'abstract'); 61 | expect(article.url, 'url'); 62 | expect(article.id, 123); 63 | expect(article.publishedData, 'publishedData'); 64 | expect(article.media, []); 65 | }); 66 | 67 | test('Media model serialization/deserialization should work properly', () async { 68 | final Map map = Media('caption', []).toJson(); 69 | final Media media = Media.fromJson(map); 70 | expect(media.caption, 'caption'); 71 | expect(media.metaData, []); 72 | }); 73 | 74 | test('Media model serialization/deserialization should work properly', () async { 75 | final Map map = MediaMetaData('url', 'format').toJson(); 76 | final MediaMetaData mediaMetaData = MediaMetaData.fromJson(map); 77 | expect(mediaMetaData.url, 'url'); 78 | expect(mediaMetaData.format, 'format'); 79 | }); 80 | 81 | test('requestNews should return error when repo throw an exception', () async { 82 | when(mockDataSource.getTasks(any)) 83 | .thenThrow(DioError(requestOptions: RequestOptions(path: ''))); 84 | final ArticleRepoImpl articleRepo = ArticleRepoImpl(mockDataSource); 85 | final Either response = await articleRepo.requestNews(); 86 | assert(response.isLeft()); 87 | }); 88 | 89 | test('requestNews should return httpUnAuthorizedError error when repo throws an 403 exception', () async { 90 | when(mockDataSource.getTasks(any)) 91 | .thenThrow( 92 | DioError( 93 | type: DioErrorType.response, 94 | requestOptions: RequestOptions(path: ''), 95 | response: Response(requestOptions: RequestOptions(path: ''), statusCode: 401) 96 | )); 97 | final ArticleRepoImpl articleRepo = ArticleRepoImpl(mockDataSource); 98 | final Either response = await articleRepo.requestNews(); 99 | response.fold( 100 | (Error exception) { 101 | expect(exception, const Error.httpUnAuthorizedError()); 102 | }, 103 | (MostPopularResponse data) {assert(false);}); 104 | }); 105 | 106 | test('requestNews should return HttpInternalServerError error when repo throws an 503 exception', () async { 107 | when(mockDataSource.getTasks(any)) 108 | .thenThrow( 109 | DioError( 110 | type: DioErrorType.response, 111 | requestOptions: RequestOptions(path: ''), 112 | response: Response(requestOptions: RequestOptions(path: ''), statusCode: 503) 113 | )); 114 | final ArticleRepoImpl articleRepo = ArticleRepoImpl(mockDataSource); 115 | final Either response = await articleRepo.requestNews(); 116 | response.fold( 117 | (Error exception) { 118 | assert(exception is HttpInternalServerError); 119 | }, 120 | (MostPopularResponse data) {assert(false);}); 121 | }); 122 | 123 | } 124 | -------------------------------------------------------------------------------- /test/unit-tests/articles_list/data/repository/article_repo_impl_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.16 from annotations 2 | // in nyt_flutter/test/unit-tests/articles_list/data/repository/article_repo_impl_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i4; 6 | 7 | import 'package:mockito/mockito.dart' as _i1; 8 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart' 9 | as _i2; 10 | 11 | import 'article_repo_impl_test.dart' as _i3; 12 | 13 | // ignore_for_file: avoid_redundant_argument_values 14 | // ignore_for_file: avoid_setters_without_getters 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: implementation_imports 17 | // ignore_for_file: invalid_use_of_visible_for_testing_member 18 | // ignore_for_file: prefer_const_constructors 19 | // ignore_for_file: unnecessary_parenthesis 20 | // ignore_for_file: camel_case_types 21 | 22 | class _FakeMostPopularResponse_0 extends _i1.Fake 23 | implements _i2.MostPopularResponse {} 24 | 25 | /// A class which mocks [MockArticleRemoteDataSource]. 26 | /// 27 | /// See the documentation for Mockito's code generation for more information. 28 | class MockMockArticleRemoteDataSource extends _i1.Mock 29 | implements _i3.MockArticleRemoteDataSource { 30 | MockMockArticleRemoteDataSource() { 31 | _i1.throwOnMissingStub(this); 32 | } 33 | 34 | @override 35 | String toString() => super.toString(); 36 | @override 37 | _i4.Future<_i2.MostPopularResponse> getTasks(String? apiKey) => 38 | (super.noSuchMethod(Invocation.method(#getTasks, [apiKey]), 39 | returnValue: Future<_i2.MostPopularResponse>.value( 40 | _FakeMostPopularResponse_0())) 41 | as _i4.Future<_i2.MostPopularResponse>); 42 | } 43 | -------------------------------------------------------------------------------- /test/unit-tests/articles_list/domain/article_usecase_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/annotations.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | import 'package:nyt_flutter/articles_list/data/model/article.dart'; 6 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart'; 7 | import 'package:nyt_flutter/articles_list/domain/repository/article_repo.dart'; 8 | import 'package:nyt_flutter/articles_list/domain/usecase/article_usecase_impl.dart'; 9 | import 'package:nyt_flutter/core/error.dart'; 10 | import 'article_usecase_impl_test.mocks.dart'; 11 | 12 | class MockArticleRepo extends Mock implements ArticleRepo {} 13 | 14 | @GenerateMocks([MockArticleRepo]) 15 | void main() { 16 | late MockMockArticleRepo mockRepo; 17 | late final List
articles =
[ 18 | Article('title', 'abstract', 123, 'url', 'publishedData', [ 19 | Media('caption', [MediaMetaData('url', 'format')]) 20 | ]) 21 | ]; 22 | 23 | setUp(() { 24 | mockRepo = MockMockArticleRepo(); 25 | }); 26 | 27 | test('requestNews should fetch news', () async { 28 | when(mockRepo.requestNews()) 29 | .thenAnswer((_) async => right(MostPopularResponse('', '', articles))); 30 | final ArticleUseCaseImpl articleRepo = ArticleUseCaseImpl(mockRepo); 31 | final Either response = await articleRepo.requestNews(); 32 | expect(articles, response.toOption().toNullable()?.articles); 33 | }); 34 | 35 | test('requestNews should return error when repo throw an exception', 36 | () async { 37 | when(mockRepo.requestNews()) 38 | .thenAnswer((_) async => left(const Error.httpUnAuthorizedError())); 39 | final ArticleUseCaseImpl articleRepo = ArticleUseCaseImpl(mockRepo); 40 | final Either response = 41 | await articleRepo.requestNews(); 42 | assert(response.isLeft()); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/unit-tests/articles_list/domain/article_usecase_impl_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.16 from annotations 2 | // in nyt_flutter/test/unit-tests/articles_list/domain/article_usecase_impl_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i4; 6 | 7 | import 'package:dartz/dartz.dart' as _i2; 8 | import 'package:mockito/mockito.dart' as _i1; 9 | import 'package:nyt_flutter/articles_list/data/model/most_popular_response.dart' 10 | as _i6; 11 | import 'package:nyt_flutter/core/error.dart' as _i5; 12 | 13 | import 'article_usecase_impl_test.dart' as _i3; 14 | 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeEither_0 extends _i1.Fake implements _i2.Either {} 25 | 26 | /// A class which mocks [MockArticleRepo]. 27 | /// 28 | /// See the documentation for Mockito's code generation for more information. 29 | class MockMockArticleRepo extends _i1.Mock implements _i3.MockArticleRepo { 30 | MockMockArticleRepo() { 31 | _i1.throwOnMissingStub(this); 32 | } 33 | 34 | @override 35 | String toString() => super.toString(); 36 | @override 37 | _i4.Future<_i2.Either<_i5.Error, _i6.MostPopularResponse>> requestNews() => 38 | (super.noSuchMethod(Invocation.method(#requestNews, []), 39 | returnValue: 40 | Future<_i2.Either<_i5.Error, _i6.MostPopularResponse>>.value( 41 | _FakeEither_0<_i5.Error, _i6.MostPopularResponse>())) 42 | as _i4.Future<_i2.Either<_i5.Error, _i6.MostPopularResponse>>); 43 | } 44 | -------------------------------------------------------------------------------- /test_with_coverage.sh: -------------------------------------------------------------------------------- 1 | 2 | red=$(tput setaf 1) 3 | none=$(tput sgr0) 4 | filename= 5 | open_browser= 6 | 7 | show_help() { 8 | printf " 9 | Script for running all unit and widget tests with code coverage. 10 | (run it from your root Flutter's project) 11 | *Important: requires lcov 12 | 13 | Usage: 14 | $0 [--help] [--open] [--filename ] 15 | where: 16 | -o, --open 17 | Open the coverage in your browser, 18 | Default is google-chrome you can change this in the function open_cov(). 19 | -h, --help 20 | print this message 21 | -f , --filename 22 | Run a particular test file. For example: 23 | 24 | -f test/a_particular_test.dart 25 | 26 | Or you can run all tests in a directory 27 | -f test/some_directory/ 28 | " 29 | } 30 | 31 | run_tests() { 32 | if [[ -f "pubspec.yaml" ]]; then 33 | rm -f coverage/lcov.info 34 | rm -f coverage/lcov-final.info 35 | flutter test --coverage "$filename" 36 | ch_dir 37 | else 38 | printf "\n${red}Error: this is not a Flutter project${none}\n" 39 | exit 1 40 | fi 41 | } 42 | 43 | run_report() { 44 | if [[ -f "coverage/lcov.info" ]]; then 45 | lcov -r coverage/lcov.info lib/resources/l10n/\* lib/\*/fake_\*.dart \ 46 | -o coverage/lcov-final.info 47 | genhtml -o coverage coverage/lcov-final.info 48 | else 49 | printf "\n${red}Error: no coverage info was generated${none}\n" 50 | exit 1 51 | fi 52 | } 53 | 54 | ch_dir(){ 55 | dir=$(pwd) 56 | input="$dir/coverage/lcov.info" 57 | output="$dir/coverage/lcov_new.info" 58 | echo "$input" 59 | while read line 60 | do 61 | secondString="SF:$dir/" 62 | echo "${line/SF:/$secondString}" >> $output 63 | done < "$input" 64 | 65 | mv $output $input 66 | } 67 | 68 | open_cov(){ 69 | # This depends on your system 70 | # Google Chrome: 71 | # google-chrome coverage/index-sort-l.html 72 | # Mozilla: 73 | firefox coverage/index-sort-l.html 74 | } 75 | 76 | while [ "$1" != "" ]; do 77 | case $1 in 78 | -h|--help) 79 | show_help 80 | exit 81 | ;; 82 | -o|--open) 83 | open_browser=1 84 | ;; 85 | -f|--filename) 86 | shift 87 | filename=$1 88 | ;; 89 | *) 90 | show_help 91 | exit 92 | ;; 93 | esac 94 | shift 95 | done 96 | 97 | run_tests 98 | remove_from_coverage -f coverage/lcov.info -r '.g.dart$' 99 | remove_from_coverage -f coverage/lcov.info -r '.freezed.dart$' 100 | remove_from_coverage -f coverage/lcov.info -r '.config.dart$' 101 | run_report 102 | if [ "$open_browser" = "1" ]; then 103 | open_cov 104 | fi --------------------------------------------------------------------------------