├── LICENSE ├── README.md ├── flutter_news ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutter_news │ │ │ │ │ └── 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 ├── assets │ └── images │ │ └── breaking_news.png ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── 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 │ ├── core │ │ ├── clippers │ │ │ └── oval_bottom_clipper.dart │ │ ├── colors.dart │ │ ├── exceptions.dart │ │ ├── failures.dart │ │ └── themes.dart │ ├── features │ │ └── news │ │ │ ├── data │ │ │ ├── datasources │ │ │ │ ├── news_hive_helper.dart │ │ │ │ ├── news_local_data_source.dart │ │ │ │ └── news_remote_data_source.dart │ │ │ ├── models │ │ │ │ ├── news_model.dart │ │ │ │ ├── news_model.g.dart │ │ │ │ ├── source_model.dart │ │ │ │ └── source_model.g.dart │ │ │ └── repositories │ │ │ │ └── news_repository_impl.dart │ │ │ ├── domain │ │ │ ├── entities │ │ │ │ ├── news.dart │ │ │ │ └── source.dart │ │ │ ├── params │ │ │ │ └── news_params.dart │ │ │ ├── repositories │ │ │ │ └── news_repository.dart │ │ │ └── usecases │ │ │ │ └── get_news.dart │ │ │ └── presentation │ │ │ ├── bloc │ │ │ ├── news_bloc.dart │ │ │ ├── news_event.dart │ │ │ └── news_state.dart │ │ │ ├── pages │ │ │ ├── detail_page.dart │ │ │ └── home_page.dart │ │ │ └── widgets │ │ │ ├── category_chip.dart │ │ │ ├── headlines.dart │ │ │ └── news_of_the_day.dart │ ├── injection_container.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test │ └── features │ │ └── news │ │ ├── data │ │ ├── datasources │ │ │ ├── news_local_data_source_test.dart │ │ │ └── news_remote_data_source_test.dart │ │ ├── models │ │ │ └── news_model_test.dart │ │ └── repositories │ │ │ └── news_repository_impl_test.dart │ │ ├── domain │ │ └── usecases │ │ │ └── get_news_test.dart │ │ ├── fixtures │ │ ├── cached_news.json │ │ ├── fixture_reader.dart │ │ ├── news.json │ │ └── news_model.json │ │ └── presentation │ │ └── bloc │ │ └── news_bloc_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json └── images ├── clean-architecture-flutter.png ├── clean-architecture.png ├── logo.png ├── screenshot.png └── screenshot2.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ajvelo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Contributors][contributors-shield]][contributors-url] 4 | [![Forks][forks-shield]][forks-url] 5 | [![Stargazers][stars-shield]][stars-url] 6 | [![Issues][issues-shield]][issues-url] 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | Logo 15 | 16 | 17 |

Flutter News

18 | 19 |

20 | A Flutter project detailing how to build an application using TDD and clean architecture. 21 |
22 | Explore the docs » 23 |
24 |
25 | View Demo 26 | · 27 | Report Bug 28 | · 29 | Request Feature 30 |

31 |
32 | 33 | 34 | 35 | 36 |
37 | Table of Contents 38 |
    39 |
  1. 40 | About The Project 41 | 44 |
  2. 45 |
  3. 46 | Getting Started 47 | 51 |
  4. 52 |
53 |
54 | 55 | 56 | 57 | ## About The Project 58 | 59 |
60 |

61 | 62 | Screenshot 63 | Screenshot2 64 | 65 |

66 |
67 | 68 | ### Before We Start 69 | 70 | This application uses `BLoC`. Providing you are using Clean Architecture in the intended manner, it ultimately doesn't matter what state management solution/framework you choose as you can easily swap one out for the other as you'll find below. 71 | 72 | If you want to view a similar project that uses `Riverpod`, please click here 73 | 74 | There are a number of Flutter tutorials out there that illustrate how to build an application with different state management solutions such as `BLoC`, `GetX`, `Riverpod` etc. However most are incomplete, they do not show how to integrate networking and make API calls or how tests can be written. In short, they do not provide an overall solution for clean architecture implementation. This project aims to give an insight into how you would create a production-level application that is scalable, testable and written with clean code. 75 | 76 | ### Here's what you can expect to learn from this project: 77 | 78 | * How to structure your application so that everything is modularised and discrete. 79 | * How to write tests for every module of your application. 80 | * How to structure your architecture in such a way that you can replace modules with different tools/libraries as you see fit. E.g. Replacing your `BLoC` state management with something like `Riverpod`. 81 | 82 | There are certainly a number of ways that you can implementation your application such that you abide by the right design principles (SOLID, DRY, YAGNI etc.). However, there is no one approach that works better than the others. There is only those that are more modularised. Hence, the approach you should take depends on the project, its requirements and its constraints. For example, if you needed to create an MVP in a short amount of time you certainly wouldn't want to implement an architecture that conformed to TDD and was made of discrete components. You would want to use something that required little boiler-plate code and that should be developed quickly. 83 | 84 | With that said, this approach here is not meant for MVP applications but rather for large-scale applications that require unit testing and components that aren't coupled together. I discuss in detail what the approach entails and how and why modules are organised and created in the fashion they are. You can read about it at the README here. 85 | 86 | ### Built With 87 | 88 | * [Flutter](https://flutter.dev) 89 | 90 | ## Getting Started 91 | 92 | To get a local copy up and running follow these simple example steps. 93 | 94 | ### Prerequisites 95 | 96 | 1. Follow the Flutter guide on [Get Started](https://docs.flutter.dev/get-started/install) to install the Flutter SDK on your machine. NOTE this project is built with Flutter version 2.8.0. 97 | 2. Ensure everything is installed correctly by running the command `flutter doctor --verbose` on your terminal. 98 | 99 | ### Installation 100 | 101 | 1. Get a free API Key at [News API](https://newsapi.org/). 102 | 2. Clone the repo 103 | ```sh 104 | git clone https://github.com/ajvelo/Flutter-News.git 105 | ``` 106 | 3. Install pub packages 107 | ```sh 108 | flutter pub get 109 | ``` 110 | 4. Create a file in `lib/core/` called `constants.dart` 111 | ```dart 112 | class Constants { 113 | static const apiKey = "YOUR-API-KEY"; 114 | } 115 | ``` 116 | 117 | ## Roadmap 118 | 119 | See the [open issues](https://github.com/ajvelo/Flutter-News/issues) for a full list of proposed features (and known issues). 120 | 121 | ## Contributing 122 | 123 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 124 | 125 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 126 | Don't forget to give the project a star! Thanks again! 127 | 128 | 1. Fork the Project 129 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 130 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 131 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 132 | 5. Open a Pull Request 133 | 134 | ## License 135 | 136 | Distributed under the MIT License. See `LICENSE` for more information. 137 | 138 |

(back to top)

139 | 140 | [contributors-shield]: https://img.shields.io/github/contributors/ajvelo/Flutter-News.svg?style=for-the-badge 141 | [contributors-url]: https://github.com/ajvelo/Flutter-News/graphs/contributors 142 | [forks-shield]: https://img.shields.io/github/forks/ajvelo/Flutter-News.svg?style=for-the-badge 143 | [forks-url]: https://github.com/ajvelo/Flutter-News/network/members 144 | [stars-shield]: https://img.shields.io/github/stars/ajvelo/Flutter-News.svg?style=for-the-badge 145 | [stars-url]: https://github.com/ajvelo/Flutter-News/stargazers 146 | [issues-shield]: https://img.shields.io/github/issues/ajvelo/Flutter-News.svg?style=for-the-badge 147 | [issues-url]: https://github.com/ajvelo/Flutter-News/issues 148 | -------------------------------------------------------------------------------- /flutter_news/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | lib/core/constants.dart 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /flutter_news/.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: cf4400006550b70f28e4b4af815151d1e74846c6 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /flutter_news/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | Logo 8 | 9 | 10 |

Clean Architecture

11 | 12 |

13 | How to implement clean architecture in a large-scale production application. 14 |
15 | Explore the docs » 16 |
17 |
18 | View Demo 19 | · 20 | Report Bug 21 | · 22 | Request Feature 23 |

24 |
25 | 26 | 27 | 28 | 29 |
30 | Table of Contents 31 |
    32 |
  1. 33 | About The Project 34 | 39 |
  2. 40 |
  3. 41 | Getting Started 42 | 47 |
  4. 48 |
  5. 49 | Testing 50 | 53 |
  6. 54 |
55 |
56 | 57 | ## About The Project 58 | 59 | Deciding on architecture is likely the most important part of your application. It can make your code incredibly easy to work with or can be a nightmare for implementing features. When deciding upon an architecture to use, it’s important to take into account the following: 60 | 61 | * Independent of Frameworks. The architecture does not depend on the existence of some library of feature-laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints. 62 | 63 | * Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element. 64 | 65 | * Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules. 66 | 67 | * Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database. 68 | 69 | * Independent of any external agency. In fact, your business rules simply don’t know anything at all about the outside world. 70 | 71 | Now that we know from the points above what makes for a good architecture we can deduce there are two key factors that are crucial for the implementation of clean code. The first is that it must be stand-alone, meaning it does not rely on any internal or external dependencies. The second is that its components must testable. In fact, the several points above come from _Robert C. Martin_, who founded the principles of Clean Architecture with these points in mind. It is these principles that we’ll use as the foundation for our Flutter news application. 72 | 73 | 74 | ### Clean Architecture 75 | 76 | The clean architecture cannot be applied to Flutter directly and nor should it. It was created before Flutter even came into existence. We can, however, use its model to inform us as to what clean code should look like and how our application should be structured. The reason for using this particular model is that at its core it uses common, fundamental programming and design principles such as SOLID, DRY and SSOT. 77 | 78 |
79 |
80 | Logo 81 | 82 |
83 |
84 | 85 | From the diagram, you can see that everything stems from the entity. These are, according to its creator, the enterprise business rules, or another way of putting it is they are the business objects contained in our application. These entities should be high-level and (ideally) should never change. They should remain the same when things such as page navigation, security and styling are implemented. 86 | 87 | The use cases are application-specific business rules, meaning they are meant to describe and cover all the features within our application. They dictate the flow of data from the entities to the presentation layer. Changes to the use cases should not affect the entities, as they are within a separate layer and therefore independent. In fact, any changes to any of the external layers should not affect the use cases since it is completely isolated. 88 | 89 | The green layer, which is essentially the presentation layer serves to transform the use cases into UI, the blue outer layer. The presentation layer where the state management of our application will take place and is the most flexible out of the 4 layers. It can be easily swapped out for another state management implementation and most importantly, should not affect the other layers. State management libraries are plentiful and there is no one that is better than the other, but in the application, we’ll use BLoC for its ease of use as well as its wide adoption within the Flutter community for reasons explained later on. 90 | 91 | ### Clean Architecture for Flutter 92 | 93 | It’s now time to use the architectural diagram above and adjust it to meet Flutter's needs. Thankfully, there’s not too much adjusting to do since we have already established our call flow. Our call flow is what defines how our layers should talk to each other and the direction that communication should flow. Since we have our call flow and know the layers needed for our application we can create a diagram to show how our architecture should look like. 94 | 95 |
96 |
97 | Logo 98 | 99 |
100 |
101 | 102 | It should be noted that this diagram is in fact so common within the Flutter community that there’s even a package for it! Nothing new was created or adjusted for the purpose of this project as it works perfectly well as it is. 103 | 104 | The Flutter Clean Architecture diagram is pretty self-explanatory for the most part, but you might have noticed that the repositories sit in both the data and domain layer. Repositories are essentially classes that utilise models and return entities to give to the use cases to be transformed into UI. With this definition in mind, it makes sense that both the data and domain layer should have an understanding of the repository, although we still want to ensure a separation of concerns between the layers and make certain that both have single responsibilities. This is where abstractions and implementations come into play. Let’s take a look at the definition: 105 | 106 |
107 | 108 | >An abstract class, which is similar to an interface, defines a class that cannot be instantiated. Abstract classes keep your code honest and type safe. It ensures that all implementation subclasses define all the properties and methods that the abstract class defines, but leaves the implementation to each subclass. 109 | > 110 | > -- Flutter by Example 111 | 112 |
113 | 114 | What we are achieving by having the repository sit in both the data and domain layer is we are essentially defining a contract between them. 115 | 116 | ### File Structure 117 | 118 | File structures will always differ slightly depending on the project. The important thing is to ensure that the data, domain and presentation layers are divided into their separate folders, with their constituent components within, in order to conform with our clean architecture. Another important factor to mention here is it would be beneficial to replicate this structure for each feature, in order to avoid the different folder layers becoming too large and cluttered. The file structure within this project is modelled on the template below: 119 | 120 | ``` 121 | ├──lib/ 122 | │ ├──core/ 123 | │ ├──features/ 124 | │ └──feature_name/ 125 | │ ├──data/ 126 | │ ├──datasources/ 127 | │ ├──models/ 128 | │ └──repositories/ 129 | │ ├──domain/ 130 | │ ├──entities/ 131 | │ ├──usecases/ 132 | │ └──repositories/ 133 | │ ├──presentation/ 134 | │ ├──bloc/ 135 | │ ├──pages/ 136 | │ └──widgets/ 137 | │ └──main.dart 138 | ``` 139 | 140 | As seen from the example, all the .dart files located within the architectural folders sit inside the lib folder, where `main.dart` is the point of entry for the application. The tests could allow being structured like this as well, in order to mirror the logic being implemented. A core folder could be added on the same level as the feature folders to contain useful helper files that the features may have in common. 141 | 142 | ## Layers 143 | 144 | ### Domain 145 | 146 | The domain layer has 3 key components and sits in between the presentation and data layer. 147 | 148 | * Entities 149 | * Use cases 150 | * Repository (As a contract) 151 | 152 | #### Entities 153 | The entity is the data the application will interact with. Its properties can be constructed from the data retrieved from the data sources and applying the relevant fields. An important thing to mention here is that the class should extend Equatable for easy value comparisons. 154 | 155 | #### Use cases 156 | As mentioned before, use cases are application-specific business rules. They get their data from the repository. Calls to repositories are generally asynchronous (especially if calling an API). We would therefore expect our use cases to return a Future. The future can be _either_ the object being returned or an error. With that in mind, we could extend our use case to allow for error handling, perhaps instead of returning a future of our type we return either an error or our Type. Such functionality is possible with functional programming and can be accomplished with the dartz package. 157 | 158 | #### Repository (As a contract) 159 | We know that the repository sits in both the data and domain layers. To be more precise, the contract is with the domain while the implementation is with the data layer. Since we’re using dart, this will be done via an `abstract` class. 160 | 161 | ### Data 162 | 163 | The data layer is as you guessed where data is exchanged between the different modules. These modules generally come in the form of servers and databases but can also include services such as 3rd party libraries. It’s the lowest level of the three layers and also consists of 3 components 164 | 165 | * Models 166 | * Datasources 167 | * Repositories (Implementation) 168 | 169 | #### Models 170 | Models are similar to the domain’s entities except they have additional functionalities added to them, mainly the ability to serialise and deserialise to and from JSON. Since our APIs will return data in a JSON format, we need to ability to convert this into a custom object of our chosen type. 171 | 172 | Conversion logic for serialisation and deserialisation can be placed either inside the model or in a separate mapper class. Both are perfectly valid solutions although some feel that conversion logic directly inside the model is sufficient and easier to work with. 173 | 174 | To strengthen the relationship between our entities and models, our models should provide an extension to allow for the conversion into an entity (since that is what our repository will return). This is important as the only difference between the two should be one contains conversion logic to convert to and from JSON, which is used only in the data layer while the other does not and is used exclusively within the presentation layer. It is therefore important to seperate the responsibilities of these classes. 175 | 176 | #### Repositories (Implementation) 177 | 178 | Repositories are the brain of the data layer. They handle the retrieval of data from the data sources as well as choosing which one to use for a given purpose. We already have the contract thanks to the domain layer, so the data layer repository simply implements these methods. 179 | 180 | #### Datasources 181 | 182 | Repository implementations need to get their data from somewhere, so they are heavily reliant on their data sources. Data sources tend to come in form of remote and local, where remote will retrieve the data from an API and local will retrieve from the cache. Similar to the repository contract between the data and domain layer, we can create a similar abstraction for the data sources and implement them. 183 | 184 | An important note here is that datasources do not return errors inline. Instead, they catch exceptions as soon as possible and throw them. That way, the repository implementation can have the responsibility of returning Failure objects from the methods. The second point is that we are returning models, not entities. The reason for these differences is that data sources are at the lowest level of implementation, they are at the border crossing between our own logic and those of APIs and 3rd party libraries. 185 | 186 | ### Presentation 187 | 188 | Presentation layers serve as the presentation logic holders, as well as containing all the UI in the form of widgets. This is where state management happens. Whatever library you use for this approach is entirely down to you. `ChangeNotifier`, `BLoC` and `ViewModels` are all perfectly valid and common forms of state management. Some are easier to work with than others, and some require more low-level implementation. In this project, I'll be using BLoC as it was specifically designed with Flutter in mind and is incredibly easy to use. 189 | 190 | #### BLoC 191 | 192 | BLoC stands for Business Logic Component and its purpose (like all other state management approaches) is to make sure that presentation and business logic is kept separate and under the control of a state management layer. BLoC does this by ensuring data only flows in one direction. There are three key steps to the BLoC process: 193 | 194 | * Event Dispatch 195 | * Interpretation 196 | * State Emission 197 | 198 | Event Dispatch is where events are dispatched from widgets as a result of some wanted change in the UI. In our news application an example of this is selecting a category. This is then sent to the BLoC where the interpretation happens. The BLoC receives the event and in turn decides the relevant business logic to execute, which would be calling the use case. Once the BLoC has received a response from the domain layer, it emits a state to the widgets, telling the widgets they should update according to the newly received state. 199 | 200 | ## Testing 201 | 202 | One of the key reasons why clean architecture for Flutter is so popular is because of its testability. As mentioned previously, the folder structure of your tests could mirror exactly the folder structure within your `/lib/` folder, so the logic can be tested function to function. 203 | 204 | ### Unit Testing 205 | 206 | The goal of unit testing is to test a single function. Although in some cases you can also test an entire class. The idea is to verify the correctness of a unit of logic under a variety of conditions. External dependencies of the unit under test are generally mocked out. There are a number of libraries to help with this, but since the introduction of Null Safety in Flutter, I would recommend a package like Mocktail, which generates mocks for you without the need to create them yourself. 207 | 208 |

(back to top)

-------------------------------------------------------------------------------- /flutter_news/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/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 flutter.compileSdkVersion 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.example.flutter_news" 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 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 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/kotlin/com/example/flutter_news/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_news 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_news/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_news/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 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 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /flutter_news/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /flutter_news/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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/assets/images/breaking_news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/assets/images/breaking_news.png -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/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 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter_news/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_news/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_news/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /flutter_news/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | 7 | DEPENDENCIES: 8 | - Flutter (from `Flutter`) 9 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 10 | 11 | EXTERNAL SOURCES: 12 | Flutter: 13 | :path: Flutter 14 | path_provider_foundation: 15 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 16 | 17 | SPEC CHECKSUMS: 18 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 19 | path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c 20 | 21 | PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 22 | 23 | COCOAPODS: 1.15.2 24 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | C03463436B829E9B3CC90BAF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B06129D3FC25518A9F49B00F /* Pods_Runner.framework */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 0A81466FB4061277E8292475 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 20B9DE0FE3F88F10ECD15A58 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 37 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 38 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 39 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 40 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 41 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 42 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 43 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 47 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | B06129D3FC25518A9F49B00F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | BC84611C7B3DB1CE7F0C4F46 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | C03463436B829E9B3CC90BAF /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 2FE8DA8E2231A28D1FBD4368 /* Pods */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 20B9DE0FE3F88F10ECD15A58 /* Pods-Runner.debug.xcconfig */, 68 | 0A81466FB4061277E8292475 /* Pods-Runner.release.xcconfig */, 69 | BC84611C7B3DB1CE7F0C4F46 /* Pods-Runner.profile.xcconfig */, 70 | ); 71 | name = Pods; 72 | path = Pods; 73 | sourceTree = ""; 74 | }; 75 | 9740EEB11CF90186004384FC /* Flutter */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 79 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 80 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 81 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 82 | ); 83 | name = Flutter; 84 | sourceTree = ""; 85 | }; 86 | 97C146E51CF9000F007C117D = { 87 | isa = PBXGroup; 88 | children = ( 89 | 9740EEB11CF90186004384FC /* Flutter */, 90 | 97C146F01CF9000F007C117D /* Runner */, 91 | 97C146EF1CF9000F007C117D /* Products */, 92 | 2FE8DA8E2231A28D1FBD4368 /* Pods */, 93 | C4A821F9D09BB170BC74AA63 /* Frameworks */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 97C146EF1CF9000F007C117D /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 97C146EE1CF9000F007C117D /* Runner.app */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 97C146F01CF9000F007C117D /* Runner */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 109 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 110 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 111 | 97C147021CF9000F007C117D /* Info.plist */, 112 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 113 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 114 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 115 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 116 | ); 117 | path = Runner; 118 | sourceTree = ""; 119 | }; 120 | C4A821F9D09BB170BC74AA63 /* Frameworks */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | B06129D3FC25518A9F49B00F /* Pods_Runner.framework */, 124 | ); 125 | name = Frameworks; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 97C146ED1CF9000F007C117D /* Runner */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 134 | buildPhases = ( 135 | C0DD7A1F37DE48FA472FF66D /* [CP] Check Pods Manifest.lock */, 136 | 9740EEB61CF901F6004384FC /* Run Script */, 137 | 97C146EA1CF9000F007C117D /* Sources */, 138 | 97C146EB1CF9000F007C117D /* Frameworks */, 139 | 97C146EC1CF9000F007C117D /* Resources */, 140 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 141 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 142 | 3EB8137D0A33DEF75EA98AA5 /* [CP] Embed Pods Frameworks */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Runner; 149 | productName = Runner; 150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 97C146E61CF9000F007C117D /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastUpgradeCheck = 1510; 160 | ORGANIZATIONNAME = ""; 161 | TargetAttributes = { 162 | 97C146ED1CF9000F007C117D = { 163 | CreatedOnToolsVersion = 7.3.1; 164 | LastSwiftMigration = 1100; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 169 | compatibilityVersion = "Xcode 9.3"; 170 | developmentRegion = en; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 97C146E51CF9000F007C117D; 177 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 97C146ED1CF9000F007C117D /* Runner */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 97C146EC1CF9000F007C117D /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 193 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXShellScriptBuildPhase section */ 201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | alwaysOutOfDate = 1; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | ); 207 | inputPaths = ( 208 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 209 | ); 210 | name = "Thin Binary"; 211 | outputPaths = ( 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | shellPath = /bin/sh; 215 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 216 | }; 217 | 3EB8137D0A33DEF75EA98AA5 /* [CP] Embed Pods Frameworks */ = { 218 | isa = PBXShellScriptBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | ); 222 | inputFileListPaths = ( 223 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 224 | ); 225 | name = "[CP] Embed Pods Frameworks"; 226 | outputFileListPaths = ( 227 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | shellPath = /bin/sh; 231 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 232 | showEnvVarsInLog = 0; 233 | }; 234 | 9740EEB61CF901F6004384FC /* Run Script */ = { 235 | isa = PBXShellScriptBuildPhase; 236 | alwaysOutOfDate = 1; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | inputPaths = ( 241 | ); 242 | name = "Run Script"; 243 | outputPaths = ( 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | shellPath = /bin/sh; 247 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 248 | }; 249 | C0DD7A1F37DE48FA472FF66D /* [CP] Check Pods Manifest.lock */ = { 250 | isa = PBXShellScriptBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | inputFileListPaths = ( 255 | ); 256 | inputPaths = ( 257 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 258 | "${PODS_ROOT}/Manifest.lock", 259 | ); 260 | name = "[CP] Check Pods Manifest.lock"; 261 | outputFileListPaths = ( 262 | ); 263 | outputPaths = ( 264 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | shellPath = /bin/sh; 268 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 269 | showEnvVarsInLog = 0; 270 | }; 271 | /* End PBXShellScriptBuildPhase section */ 272 | 273 | /* Begin PBXSourcesBuildPhase section */ 274 | 97C146EA1CF9000F007C117D /* Sources */ = { 275 | isa = PBXSourcesBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 279 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | /* End PBXSourcesBuildPhase section */ 284 | 285 | /* Begin PBXVariantGroup section */ 286 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 287 | isa = PBXVariantGroup; 288 | children = ( 289 | 97C146FB1CF9000F007C117D /* Base */, 290 | ); 291 | name = Main.storyboard; 292 | sourceTree = ""; 293 | }; 294 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 295 | isa = PBXVariantGroup; 296 | children = ( 297 | 97C147001CF9000F007C117D /* Base */, 298 | ); 299 | name = LaunchScreen.storyboard; 300 | sourceTree = ""; 301 | }; 302 | /* End PBXVariantGroup section */ 303 | 304 | /* Begin XCBuildConfiguration section */ 305 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_COMMA = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 334 | COPY_PHASE_STRIP = NO; 335 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 336 | ENABLE_NS_ASSERTIONS = NO; 337 | ENABLE_STRICT_OBJC_MSGSEND = YES; 338 | GCC_C_LANGUAGE_STANDARD = gnu99; 339 | GCC_NO_COMMON_BLOCKS = YES; 340 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 341 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 342 | GCC_WARN_UNDECLARED_SELECTOR = YES; 343 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 344 | GCC_WARN_UNUSED_FUNCTION = YES; 345 | GCC_WARN_UNUSED_VARIABLE = YES; 346 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 347 | MTL_ENABLE_DEBUG_INFO = NO; 348 | SDKROOT = iphoneos; 349 | SUPPORTED_PLATFORMS = iphoneos; 350 | TARGETED_DEVICE_FAMILY = "1,2"; 351 | VALIDATE_PRODUCT = YES; 352 | }; 353 | name = Profile; 354 | }; 355 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 356 | isa = XCBuildConfiguration; 357 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | CLANG_ENABLE_MODULES = YES; 361 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 362 | ENABLE_BITCODE = NO; 363 | INFOPLIST_FILE = Runner/Info.plist; 364 | LD_RUNPATH_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "@executable_path/Frameworks", 367 | ); 368 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterNews; 369 | PRODUCT_NAME = "$(TARGET_NAME)"; 370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 371 | SWIFT_VERSION = 5.0; 372 | VERSIONING_SYSTEM = "apple-generic"; 373 | }; 374 | name = Profile; 375 | }; 376 | 97C147031CF9000F007C117D /* Debug */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 382 | CLANG_CXX_LIBRARY = "libc++"; 383 | CLANG_ENABLE_MODULES = YES; 384 | CLANG_ENABLE_OBJC_ARC = YES; 385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_COMMA = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 400 | CLANG_WARN_STRICT_PROTOTYPES = YES; 401 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = dwarf; 407 | ENABLE_STRICT_OBJC_MSGSEND = YES; 408 | ENABLE_TESTABILITY = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu99; 410 | GCC_DYNAMIC_NO_PIC = NO; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_OPTIMIZATION_LEVEL = 0; 413 | GCC_PREPROCESSOR_DEFINITIONS = ( 414 | "DEBUG=1", 415 | "$(inherited)", 416 | ); 417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 419 | GCC_WARN_UNDECLARED_SELECTOR = YES; 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 421 | GCC_WARN_UNUSED_FUNCTION = YES; 422 | GCC_WARN_UNUSED_VARIABLE = YES; 423 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 424 | MTL_ENABLE_DEBUG_INFO = YES; 425 | ONLY_ACTIVE_ARCH = YES; 426 | SDKROOT = iphoneos; 427 | TARGETED_DEVICE_FAMILY = "1,2"; 428 | }; 429 | name = Debug; 430 | }; 431 | 97C147041CF9000F007C117D /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_SEARCH_USER_PATHS = NO; 435 | CLANG_ANALYZER_NONNULL = YES; 436 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 437 | CLANG_CXX_LIBRARY = "libc++"; 438 | CLANG_ENABLE_MODULES = YES; 439 | CLANG_ENABLE_OBJC_ARC = YES; 440 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 441 | CLANG_WARN_BOOL_CONVERSION = YES; 442 | CLANG_WARN_COMMA = YES; 443 | CLANG_WARN_CONSTANT_CONVERSION = YES; 444 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 445 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 446 | CLANG_WARN_EMPTY_BODY = YES; 447 | CLANG_WARN_ENUM_CONVERSION = YES; 448 | CLANG_WARN_INFINITE_RECURSION = YES; 449 | CLANG_WARN_INT_CONVERSION = YES; 450 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 452 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 453 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 455 | CLANG_WARN_STRICT_PROTOTYPES = YES; 456 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 457 | CLANG_WARN_UNREACHABLE_CODE = YES; 458 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 459 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 460 | COPY_PHASE_STRIP = NO; 461 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 462 | ENABLE_NS_ASSERTIONS = NO; 463 | ENABLE_STRICT_OBJC_MSGSEND = YES; 464 | GCC_C_LANGUAGE_STANDARD = gnu99; 465 | GCC_NO_COMMON_BLOCKS = YES; 466 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 467 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 468 | GCC_WARN_UNDECLARED_SELECTOR = YES; 469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 470 | GCC_WARN_UNUSED_FUNCTION = YES; 471 | GCC_WARN_UNUSED_VARIABLE = YES; 472 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 473 | MTL_ENABLE_DEBUG_INFO = NO; 474 | SDKROOT = iphoneos; 475 | SUPPORTED_PLATFORMS = iphoneos; 476 | SWIFT_COMPILATION_MODE = wholemodule; 477 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | VALIDATE_PRODUCT = YES; 480 | }; 481 | name = Release; 482 | }; 483 | 97C147061CF9000F007C117D /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 486 | buildSettings = { 487 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 488 | CLANG_ENABLE_MODULES = YES; 489 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 490 | ENABLE_BITCODE = NO; 491 | INFOPLIST_FILE = Runner/Info.plist; 492 | LD_RUNPATH_SEARCH_PATHS = ( 493 | "$(inherited)", 494 | "@executable_path/Frameworks", 495 | ); 496 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterNews; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 499 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 500 | SWIFT_VERSION = 5.0; 501 | VERSIONING_SYSTEM = "apple-generic"; 502 | }; 503 | name = Debug; 504 | }; 505 | 97C147071CF9000F007C117D /* Release */ = { 506 | isa = XCBuildConfiguration; 507 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 508 | buildSettings = { 509 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 510 | CLANG_ENABLE_MODULES = YES; 511 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 512 | ENABLE_BITCODE = NO; 513 | INFOPLIST_FILE = Runner/Info.plist; 514 | LD_RUNPATH_SEARCH_PATHS = ( 515 | "$(inherited)", 516 | "@executable_path/Frameworks", 517 | ); 518 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterNews; 519 | PRODUCT_NAME = "$(TARGET_NAME)"; 520 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 521 | SWIFT_VERSION = 5.0; 522 | VERSIONING_SYSTEM = "apple-generic"; 523 | }; 524 | name = Release; 525 | }; 526 | /* End XCBuildConfiguration section */ 527 | 528 | /* Begin XCConfigurationList section */ 529 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 530 | isa = XCConfigurationList; 531 | buildConfigurations = ( 532 | 97C147031CF9000F007C117D /* Debug */, 533 | 97C147041CF9000F007C117D /* Release */, 534 | 249021D3217E4FDB00AE95B9 /* Profile */, 535 | ); 536 | defaultConfigurationIsVisible = 0; 537 | defaultConfigurationName = Release; 538 | }; 539 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 540 | isa = XCConfigurationList; 541 | buildConfigurations = ( 542 | 97C147061CF9000F007C117D /* Debug */, 543 | 97C147071CF9000F007C117D /* Release */, 544 | 249021D4217E4FDB00AE95B9 /* Profile */, 545 | ); 546 | defaultConfigurationIsVisible = 0; 547 | defaultConfigurationName = Release; 548 | }; 549 | /* End XCConfigurationList section */ 550 | }; 551 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 552 | } 553 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /flutter_news/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. -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/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 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter News 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_news 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /flutter_news/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /flutter_news/lib/core/clippers/oval_bottom_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class OvalBottomClipper extends CustomClipper { 4 | @override 5 | Path getClip(Size size) { 6 | var path = Path(); 7 | path.lineTo(0, 0); 8 | path.lineTo(0, size.height - 32); 9 | path.quadraticBezierTo(8, size.height, size.width / 2, size.height); 10 | path.quadraticBezierTo( 11 | size.width - 8, size.height, size.width, size.height - 32); 12 | path.lineTo(size.width, 0); 13 | path.lineTo(0, 0); 14 | return path; 15 | } 16 | 17 | @override 18 | bool shouldReclip(CustomClipper oldClipper) { 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flutter_news/lib/core/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Colours { 4 | static const ktextColorOnLight = Color(0xff212427); 5 | static const kTextColorOnDark = Color(0xffF5F5F7); 6 | } 7 | -------------------------------------------------------------------------------- /flutter_news/lib/core/exceptions.dart: -------------------------------------------------------------------------------- 1 | class ServerException implements Exception { 2 | final String message; 3 | 4 | const ServerException({required this.message}); 5 | } 6 | 7 | class CacheException implements Exception {} 8 | -------------------------------------------------------------------------------- /flutter_news/lib/core/failures.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class Failure extends Equatable { 4 | @override 5 | List get props => []; 6 | } 7 | 8 | class ServerFailure extends Failure { 9 | final String message; 10 | ServerFailure({required this.message}); 11 | } 12 | 13 | class CacheFailure extends Failure { 14 | final String message; 15 | CacheFailure({required this.message}); 16 | } 17 | -------------------------------------------------------------------------------- /flutter_news/lib/core/themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_news/core/colors.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | 5 | class Themes { 6 | static ThemeData appTheme = ThemeData( 7 | scaffoldBackgroundColor: Colors.white, 8 | splashColor: Colors.transparent, 9 | highlightColor: Colors.transparent, 10 | primaryColor: Colors.blue, 11 | textTheme: TextTheme( 12 | displayLarge: GoogleFonts.lato( 13 | color: Colours.ktextColorOnLight, 14 | fontSize: 24, 15 | fontWeight: FontWeight.bold), 16 | displayMedium: GoogleFonts.lato( 17 | color: Colours.ktextColorOnLight, 18 | fontSize: 20, 19 | fontWeight: FontWeight.bold), 20 | bodyLarge: GoogleFonts.lato( 21 | color: Colours.ktextColorOnLight, 22 | fontSize: 16, 23 | fontWeight: FontWeight.normal), 24 | bodyMedium: GoogleFonts.lato( 25 | color: Colours.ktextColorOnLight, 26 | fontSize: 12, 27 | fontWeight: FontWeight.normal))); 28 | } 29 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/datasources/news_hive_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | 4 | import 'news_local_data_source.dart'; 5 | 6 | class NewsHiveHelper implements NewsLocalDataSource { 7 | @override 8 | Future> getNews() async { 9 | final box = await Hive.openBox('news'); 10 | final news = box.values.toList().cast(); 11 | return news; 12 | } 13 | 14 | @override 15 | saveNews(List newsModels) async { 16 | final box = await Hive.openBox('news'); 17 | box.clear(); 18 | for (var news in newsModels) { 19 | box.add(news); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/datasources/news_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_news/core/exceptions.dart'; 2 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 3 | 4 | import 'news_hive_helper.dart'; 5 | 6 | abstract class NewsLocalDataSource { 7 | Future> getNews(); 8 | saveNews(List newsModels); 9 | } 10 | 11 | class NewsLocalDataSourceImpl implements NewsLocalDataSource { 12 | final NewsHiveHelper hive; 13 | 14 | NewsLocalDataSourceImpl({required this.hive}); 15 | 16 | @override 17 | Future> getNews() async { 18 | final news = await hive.getNews(); 19 | if (news.isNotEmpty) { 20 | return news; 21 | } else { 22 | throw CacheException(); 23 | } 24 | } 25 | 26 | @override 27 | saveNews(List newsModels) { 28 | return hive.saveNews(newsModels); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/datasources/news_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:flutter_news/core/constants.dart'; 4 | import 'package:flutter_news/core/exceptions.dart'; 5 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 6 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | abstract class NewsRemoteDataSource { 10 | Future> getNews({required NewsParams parameters}); 11 | } 12 | 13 | class NewsRemoteDataSourceImpl implements NewsRemoteDataSource { 14 | final http.Client client; 15 | 16 | final baseUrl = "https://newsapi.org/v2"; 17 | 18 | NewsRemoteDataSourceImpl({required this.client}); 19 | 20 | @override 21 | Future< 22 | List< 23 | NewsModel>> getNews({required NewsParams parameters}) => _getDataFromUrl( 24 | path: 25 | "/top-headlines?country=${parameters.country}&category=${parameters.category}&apiKey=${Constants.apiKey}"); 26 | 27 | Future> _getDataFromUrl({required String path}) async { 28 | try { 29 | final response = await client.get(Uri.parse(baseUrl + path), headers: { 30 | HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8', 31 | }); 32 | switch (response.statusCode) { 33 | case 200: 34 | final results = (json.decode(response.body)['articles']); 35 | final news = 36 | (results as List).map((e) => NewsModel.fromJson(e)).toList(); 37 | return news; 38 | case 400: 39 | throw const ServerException(message: 'Bad Request'); 40 | case 401: 41 | throw const ServerException(message: 'Unauthorized'); 42 | case 500: 43 | throw const ServerException(message: 'Internal Server Error'); 44 | default: 45 | throw const ServerException(message: 'Unknown Error'); 46 | } 47 | } catch (e) { 48 | if (e is ServerException) rethrow; 49 | throw e.toString(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/models/news_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_news/features/news/data/models/source_model.dart'; 3 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 4 | import 'package:hive_flutter/hive_flutter.dart'; 5 | 6 | part 'news_model.g.dart'; 7 | 8 | @HiveType(typeId: 0) 9 | class NewsModel extends Equatable { 10 | @HiveField(0) 11 | final SourceModel source; 12 | 13 | @HiveField(1) 14 | final String? author; 15 | 16 | @HiveField(2) 17 | final String title; 18 | 19 | @HiveField(3) 20 | final String? description; 21 | 22 | @HiveField(4) 23 | final String? urlToImage; 24 | 25 | @HiveField(5) 26 | final DateTime publishedDate; 27 | 28 | @HiveField(6) 29 | final String? content; 30 | 31 | const NewsModel( 32 | {required this.source, 33 | required this.author, 34 | required this.title, 35 | required this.description, 36 | required this.urlToImage, 37 | required this.publishedDate, 38 | required this.content}); 39 | 40 | factory NewsModel.fromJson(Map json) { 41 | return NewsModel( 42 | source: (SourceModel.fromJson(json['source'])), 43 | author: json['author'] == null ? "Not Found" : json["author"], 44 | title: json['title'], 45 | description: json['description'], 46 | urlToImage: json['urlToImage'], 47 | publishedDate: DateTime.parse(json['publishedAt']), 48 | content: json['content']); 49 | } 50 | 51 | @override 52 | List get props => 53 | [source, author, title, description, urlToImage, publishedDate, content]; 54 | } 55 | 56 | extension NewsModelExtension on NewsModel { 57 | News get toNews => News( 58 | author: author, 59 | title: title, 60 | description: description, 61 | urlToImage: urlToImage, 62 | publishedDate: publishedDate, 63 | content: content, 64 | source: source.toSource); 65 | } 66 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/models/news_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'news_model.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class NewsModelAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 0; 12 | 13 | @override 14 | NewsModel read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return NewsModel( 20 | source: fields[0] as SourceModel, 21 | author: fields[1] as String?, 22 | title: fields[2] as String, 23 | description: fields[3] as String?, 24 | urlToImage: fields[4] as String?, 25 | publishedDate: fields[5] as DateTime, 26 | content: fields[6] as String?, 27 | ); 28 | } 29 | 30 | @override 31 | void write(BinaryWriter writer, NewsModel obj) { 32 | writer 33 | ..writeByte(7) 34 | ..writeByte(0) 35 | ..write(obj.source) 36 | ..writeByte(1) 37 | ..write(obj.author) 38 | ..writeByte(2) 39 | ..write(obj.title) 40 | ..writeByte(3) 41 | ..write(obj.description) 42 | ..writeByte(4) 43 | ..write(obj.urlToImage) 44 | ..writeByte(5) 45 | ..write(obj.publishedDate) 46 | ..writeByte(6) 47 | ..write(obj.content); 48 | } 49 | 50 | @override 51 | int get hashCode => typeId.hashCode; 52 | 53 | @override 54 | bool operator ==(Object other) => 55 | identical(this, other) || 56 | other is NewsModelAdapter && 57 | runtimeType == other.runtimeType && 58 | typeId == other.typeId; 59 | } 60 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/models/source_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_news/features/news/domain/entities/source.dart'; 3 | import 'package:hive_flutter/hive_flutter.dart'; 4 | 5 | part 'source_model.g.dart'; 6 | 7 | @HiveType(typeId: 1) 8 | class SourceModel extends Equatable { 9 | @HiveField(0) 10 | final String? name; 11 | 12 | const SourceModel({ 13 | required this.name, 14 | }); 15 | 16 | factory SourceModel.fromJson(Map json) { 17 | return SourceModel( 18 | name: json['name'] == null ? "Unknown" : json["name"], 19 | ); 20 | } 21 | 22 | @override 23 | List get props => [name]; 24 | } 25 | 26 | extension SourceModelExtension on SourceModel { 27 | Source get toSource => Source(name: name); 28 | } 29 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/models/source_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'source_model.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class SourceModelAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 1; 12 | 13 | @override 14 | SourceModel read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return SourceModel( 20 | name: fields[0] as String?, 21 | ); 22 | } 23 | 24 | @override 25 | void write(BinaryWriter writer, SourceModel obj) { 26 | writer 27 | ..writeByte(1) 28 | ..writeByte(0) 29 | ..write(obj.name); 30 | } 31 | 32 | @override 33 | int get hashCode => typeId.hashCode; 34 | 35 | @override 36 | bool operator ==(Object other) => 37 | identical(this, other) || 38 | other is SourceModelAdapter && 39 | runtimeType == other.runtimeType && 40 | typeId == other.typeId; 41 | } 42 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/data/repositories/news_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_news/core/exceptions.dart'; 2 | import 'package:flutter_news/features/news/data/datasources/news_local_data_source.dart'; 3 | import 'package:flutter_news/features/news/data/datasources/news_remote_data_source.dart'; 4 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 5 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 6 | import 'package:flutter_news/core/failures.dart'; 7 | import 'package:dartz/dartz.dart'; 8 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 9 | import 'package:flutter_news/features/news/domain/repositories/news_repository.dart'; 10 | 11 | class NewsRepositoryImpl implements NewsRepository { 12 | final NewsRemoteDataSource remoteDataSource; 13 | final NewsLocalDataSource localDataSource; 14 | 15 | NewsRepositoryImpl( 16 | {required this.localDataSource, required this.remoteDataSource}); 17 | 18 | @override 19 | Future>> getNews( 20 | {required NewsParams parameters}) async { 21 | try { 22 | final newsModels = await remoteDataSource.getNews(parameters: parameters); 23 | await localDataSource.saveNews(newsModels); 24 | final news = newsModels.map((e) => e.toNews).toList(); 25 | return Right(news); 26 | } on ServerException catch (error) { 27 | try { 28 | final newsModels = await localDataSource.getNews(); 29 | final news = newsModels.map((e) => e.toNews).toList(); 30 | return Right(news); 31 | } on CacheException { 32 | return Left(ServerFailure(message: error.message)); 33 | } 34 | } catch (error) { 35 | return Left(ServerFailure(message: error.toString())); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/domain/entities/news.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_news/features/news/domain/entities/source.dart'; 3 | 4 | class News extends Equatable { 5 | final Source source; 6 | final String? author; 7 | final String title; 8 | final String? description; 9 | final String? urlToImage; 10 | final DateTime publishedDate; 11 | final String? content; 12 | 13 | const News( 14 | {required this.author, 15 | required this.title, 16 | required this.description, 17 | required this.urlToImage, 18 | required this.publishedDate, 19 | required this.content, 20 | required this.source}); 21 | @override 22 | List get props => 23 | [source, author, title, description, urlToImage, publishedDate, content]; 24 | } 25 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/domain/entities/source.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Source extends Equatable { 4 | final String? name; 5 | 6 | const Source({required this.name}); 7 | @override 8 | List get props => [name]; 9 | } 10 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/domain/params/news_params.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NewsParams { 4 | final String country; 5 | final String category; 6 | 7 | NewsParams({required this.country, required this.category}); 8 | } 9 | 10 | enum CategoryType { 11 | general, 12 | business, 13 | entertainment, 14 | health, 15 | science, 16 | sports, 17 | technology 18 | } 19 | 20 | extension CategoryTypeExtension on CategoryType { 21 | String get categoryName { 22 | switch (this) { 23 | case CategoryType.general: 24 | return 'General'; 25 | case CategoryType.business: 26 | return 'Business'; 27 | case CategoryType.entertainment: 28 | return 'Entertainment'; 29 | case CategoryType.health: 30 | return 'Health'; 31 | case CategoryType.science: 32 | return 'Science'; 33 | case CategoryType.sports: 34 | return 'Sports'; 35 | case CategoryType.technology: 36 | return 'Technology'; 37 | } 38 | } 39 | 40 | IconData get categoryImage { 41 | switch (this) { 42 | case CategoryType.general: 43 | return Icons.info_rounded; 44 | case CategoryType.business: 45 | return Icons.business_rounded; 46 | case CategoryType.entertainment: 47 | return Icons.tv_rounded; 48 | case CategoryType.health: 49 | return Icons.health_and_safety_outlined; 50 | case CategoryType.science: 51 | return Icons.science_rounded; 52 | case CategoryType.sports: 53 | return Icons.sports_football_rounded; 54 | case CategoryType.technology: 55 | return Icons.computer_rounded; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/domain/repositories/news_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_news/core/failures.dart'; 3 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 4 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 5 | 6 | abstract class NewsRepository { 7 | Future>> getNews({required NewsParams parameters}); 8 | } 9 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/domain/usecases/get_news.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_news/core/failures.dart'; 3 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 4 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 5 | import 'package:flutter_news/features/news/domain/repositories/news_repository.dart'; 6 | 7 | class GetNews { 8 | final NewsRepository repository; 9 | 10 | GetNews({required this.repository}); 11 | 12 | Future>> execute( 13 | {required NewsParams parameters}) async { 14 | return repository.getNews(parameters: parameters); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/bloc/news_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_news/core/failures.dart'; 4 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 5 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 6 | import 'package:flutter_news/features/news/domain/usecases/get_news.dart'; 7 | 8 | part 'news_event.dart'; 9 | part 'news_state.dart'; 10 | 11 | class NewsBloc extends Bloc { 12 | final GetNews getNewsUsecase; 13 | NewsBloc({required this.getNewsUsecase}) : super(NewsInitial()) { 14 | on(_onGetNewsRequested); 15 | } 16 | 17 | _onGetNewsRequested(GetNewsEvent event, Emitter emit) async { 18 | emit(NewsLoading()); 19 | 20 | final result = await getNewsUsecase.execute(parameters: event.parameters); 21 | emit(result.fold((l) => NewsLoadedWithError(message: _getErrorMessage(l)), 22 | (r) => NewsLoadedWithSuccess(news: r))); 23 | } 24 | 25 | String _getErrorMessage(Failure failure) { 26 | switch (failure.runtimeType) { 27 | case ServerFailure: 28 | return (failure as ServerFailure).message; 29 | case CacheFailure: 30 | return (failure as CacheFailure).message; 31 | default: 32 | return 'An unknown error has occured'; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/bloc/news_event.dart: -------------------------------------------------------------------------------- 1 | part of 'news_bloc.dart'; 2 | 3 | abstract class NewsEvent extends Equatable {} 4 | 5 | class GetNewsEvent extends NewsEvent { 6 | final NewsParams parameters; 7 | 8 | GetNewsEvent({required this.parameters}); 9 | @override 10 | List get props => []; 11 | } 12 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/bloc/news_state.dart: -------------------------------------------------------------------------------- 1 | part of 'news_bloc.dart'; 2 | 3 | abstract class NewsState extends Equatable {} 4 | 5 | class NewsInitial extends NewsState { 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class NewsLoading extends NewsState { 11 | @override 12 | List get props => []; 13 | } 14 | 15 | class NewsLoadedWithSuccess extends NewsState { 16 | final List news; 17 | 18 | NewsLoadedWithSuccess({required this.news}); 19 | @override 20 | List get props => [news]; 21 | } 22 | 23 | class NewsLoadedWithError extends NewsState { 24 | final String message; 25 | 26 | NewsLoadedWithError({required this.message}); 27 | @override 28 | List get props => [message]; 29 | } 30 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/pages/detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_news/core/clippers/oval_bottom_clipper.dart'; 3 | import 'package:flutter_news/core/colors.dart'; 4 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 5 | import 'package:intl/intl.dart'; 6 | 7 | class DetailPage extends StatelessWidget { 8 | final News news; 9 | const DetailPage({Key? key, required this.news}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final size = MediaQuery.of(context).size; 14 | return Scaffold( 15 | extendBodyBehindAppBar: true, 16 | appBar: AppBar( 17 | backgroundColor: Colors.transparent, 18 | elevation: 0, 19 | ), 20 | body: Column( 21 | children: [ 22 | Stack( 23 | children: [ 24 | ClipPath( 25 | clipper: OvalBottomClipper(), 26 | child: Container( 27 | height: size.height / 2.5, 28 | decoration: BoxDecoration( 29 | image: DecorationImage( 30 | colorFilter: ColorFilter.mode( 31 | Colors.black.withOpacity(0.4), 32 | BlendMode.multiply), 33 | fit: BoxFit.cover, 34 | image: news.urlToImage != null 35 | ? NetworkImage(news.urlToImage!) 36 | : const AssetImage( 37 | 'assets/images/breaking_news.png') 38 | as ImageProvider)), 39 | child: Padding( 40 | padding: const EdgeInsets.all(16.0), 41 | child: Column( 42 | mainAxisAlignment: MainAxisAlignment.end, 43 | crossAxisAlignment: CrossAxisAlignment.start, 44 | children: [ 45 | Text( 46 | news.title, 47 | style: Theme.of(context) 48 | .textTheme 49 | .displayLarge! 50 | .copyWith(color: Colours.kTextColorOnDark), 51 | ), 52 | const SizedBox(height: 32), 53 | news.description != null 54 | ? Text( 55 | news.description!, 56 | style: Theme.of(context) 57 | .textTheme 58 | .bodyLarge! 59 | .copyWith( 60 | color: Colours.kTextColorOnDark), 61 | ) 62 | : const SizedBox.shrink(), 63 | const SizedBox(height: 32), 64 | ], 65 | ), 66 | ), 67 | ), 68 | ) 69 | ], 70 | ), 71 | SizedBox( 72 | height: size.height / 2, 73 | width: size.width, 74 | child: SingleChildScrollView( 75 | child: Padding( 76 | padding: const EdgeInsets.all(16.0), 77 | child: Column( 78 | mainAxisAlignment: MainAxisAlignment.start, 79 | crossAxisAlignment: CrossAxisAlignment.start, 80 | children: [ 81 | Text( 82 | DateFormat.yMMMMEEEEd().format(news.publishedDate), 83 | style: Theme.of(context).textTheme.displayMedium, 84 | ), 85 | const SizedBox(height: 16), 86 | news.author != null 87 | ? Text( 88 | news.author!, 89 | style: Theme.of(context) 90 | .textTheme 91 | .bodyLarge! 92 | .copyWith(color: Colors.grey[700]), 93 | ) 94 | : const SizedBox.shrink(), 95 | const SizedBox(height: 16), 96 | Text( 97 | news.title, 98 | style: Theme.of(context).textTheme.displayMedium, 99 | ), 100 | const SizedBox(height: 16), 101 | news.content != null 102 | ? Text( 103 | news.content!, 104 | style: Theme.of(context).textTheme.bodyLarge, 105 | ) 106 | : const SizedBox.shrink(), 107 | ], 108 | ), 109 | ), 110 | ), 111 | ), 112 | ], 113 | )); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 5 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 6 | import 'package:flutter_news/features/news/presentation/bloc/news_bloc.dart'; 7 | import 'package:flutter_news/features/news/presentation/pages/detail_page.dart'; 8 | import 'package:flutter_news/features/news/presentation/widgets/category_chip.dart'; 9 | import 'package:flutter_news/features/news/presentation/widgets/headlines.dart'; 10 | import 'package:flutter_news/features/news/presentation/widgets/news_of_the_day.dart'; 11 | 12 | class HomePage extends StatefulWidget { 13 | const HomePage({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _HomePageState(); 17 | } 18 | 19 | class _HomePageState extends State { 20 | int _selectedIndex = 0; 21 | onNewsSelected({required News news, required BuildContext context}) { 22 | Navigator.push( 23 | context, 24 | MaterialPageRoute( 25 | builder: (context) => DetailPage(news: news), 26 | )); 27 | } 28 | 29 | onCategorySelected( 30 | {required int index, 31 | required bool selected, 32 | required BuildContext context}) { 33 | setState(() { 34 | if (selected) { 35 | _selectedIndex = index; 36 | BlocProvider.of(context).add(GetNewsEvent( 37 | parameters: NewsParams( 38 | country: WidgetsBinding 39 | .instance.platformDispatcher.locale.countryCode ?? 40 | 'GB', 41 | category: CategoryType.values[_selectedIndex].categoryName))); 42 | } 43 | }); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | final size = MediaQuery.of(context).size; 49 | return Scaffold( 50 | body: BlocBuilder(builder: (context, state) { 51 | if (state is NewsLoading) { 52 | return const Center( 53 | child: CircularProgressIndicator(), 54 | ); 55 | } else if (state is NewsLoadedWithSuccess) { 56 | final news = state.news.sublist(1); 57 | final newsOfTheDay = state.news.first; 58 | return Column( 59 | children: [ 60 | SizedBox( 61 | height: size.height / 2.5, 62 | child: Stack( 63 | children: [ 64 | NewsOfTheDay( 65 | newsOfTheDay: newsOfTheDay, 66 | onPressed: (News news) { 67 | onNewsSelected(news: news, context: context); 68 | }, 69 | ) 70 | ], 71 | ), 72 | ), 73 | Padding( 74 | padding: const EdgeInsets.all(16), 75 | child: SizedBox( 76 | width: size.width, 77 | child: Text('Breaking News', 78 | textAlign: TextAlign.left, 79 | style: Theme.of(context).textTheme.displayLarge), 80 | ), 81 | ), 82 | SizedBox( 83 | height: 96, 84 | child: CategoryChips( 85 | selectedIndex: _selectedIndex, 86 | onSelected: (index, selected) => onCategorySelected( 87 | index: index, selected: selected, context: context), 88 | ), 89 | ), 90 | Expanded( 91 | child: Headlines( 92 | news: news, 93 | size: size, 94 | onPressed: (news) { 95 | onNewsSelected(news: news, context: context); 96 | }, 97 | ), 98 | ), 99 | ], 100 | ); 101 | } else if (state is NewsLoadedWithError) { 102 | return Center( 103 | child: Text(state.message), 104 | ); 105 | } 106 | return const Center( 107 | child: CircularProgressIndicator(), 108 | ); 109 | }), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/widgets/category_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 3 | 4 | class CategoryChips extends StatelessWidget { 5 | final int selectedIndex; 6 | final Function(int, bool) onSelected; 7 | const CategoryChips( 8 | {Key? key, required this.selectedIndex, required this.onSelected}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return ListView.separated( 14 | shrinkWrap: true, 15 | padding: const EdgeInsets.all(16), 16 | scrollDirection: Axis.horizontal, 17 | itemBuilder: (context, index) { 18 | return ChoiceChip( 19 | label: Text( 20 | CategoryType.values[index].categoryName, 21 | ), 22 | avatar: Icon(CategoryType.values[index].categoryImage), 23 | labelStyle: Theme.of(context).textTheme.displayMedium, 24 | padding: const EdgeInsets.all(16), 25 | onSelected: (selected) => onSelected(index, selected), 26 | selected: selectedIndex == index); 27 | }, 28 | separatorBuilder: (context, index) { 29 | return const SizedBox(width: 16); 30 | }, 31 | itemCount: CategoryType.values.length); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/widgets/headlines.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 3 | 4 | class Headlines extends StatelessWidget { 5 | const Headlines( 6 | {Key? key, 7 | required this.news, 8 | required this.size, 9 | required this.onPressed}) 10 | : super(key: key); 11 | 12 | final List news; 13 | final Size size; 14 | final Function(News news) onPressed; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ListView.builder( 19 | scrollDirection: Axis.horizontal, 20 | itemCount: news.length, 21 | itemBuilder: (context, index) { 22 | final newsSingle = news[index]; 23 | final dateDifference = 24 | DateTime.now().difference(newsSingle.publishedDate).inHours; 25 | return Padding( 26 | padding: const EdgeInsets.all(16.0), 27 | child: GestureDetector( 28 | onTap: () => onPressed(newsSingle), 29 | child: SizedBox( 30 | width: size.width / 1.5, 31 | child: Column( 32 | crossAxisAlignment: CrossAxisAlignment.start, 33 | mainAxisAlignment: MainAxisAlignment.start, 34 | children: [ 35 | Container( 36 | height: size.height / 6, 37 | decoration: BoxDecoration( 38 | borderRadius: BorderRadius.circular(16), 39 | image: DecorationImage( 40 | fit: BoxFit.fill, 41 | image: newsSingle.urlToImage != null 42 | ? NetworkImage(newsSingle.urlToImage!) 43 | : const AssetImage( 44 | 'assets/images/breaking_news.png') 45 | as ImageProvider)), 46 | ), 47 | const SizedBox(height: 8), 48 | Text( 49 | newsSingle.title, 50 | style: Theme.of(context).textTheme.displayMedium, 51 | maxLines: 2, 52 | overflow: TextOverflow.ellipsis, 53 | ), 54 | const SizedBox(height: 4), 55 | Text( 56 | "${dateDifference.toString()} hours ago", 57 | style: Theme.of(context) 58 | .textTheme 59 | .bodyLarge! 60 | .copyWith(color: Colors.grey[700]), 61 | ), 62 | const SizedBox(height: 4), 63 | Text( 64 | "Source: ${newsSingle.source.name}", 65 | style: Theme.of(context) 66 | .textTheme 67 | .bodyLarge! 68 | .copyWith(color: Colors.grey[700]), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ), 74 | ); 75 | }, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /flutter_news/lib/features/news/presentation/widgets/news_of_the_day.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_news/core/clippers/oval_bottom_clipper.dart'; 3 | import 'package:flutter_news/core/colors.dart'; 4 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 5 | 6 | class NewsOfTheDay extends StatelessWidget { 7 | const NewsOfTheDay({ 8 | Key? key, 9 | required this.newsOfTheDay, 10 | required this.onPressed, 11 | }) : super(key: key); 12 | 13 | final News newsOfTheDay; 14 | final Function(News news) onPressed; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ClipPath( 19 | clipper: OvalBottomClipper(), 20 | child: Container( 21 | decoration: BoxDecoration( 22 | image: DecorationImage( 23 | colorFilter: ColorFilter.mode( 24 | Colors.black.withOpacity(0.4), BlendMode.multiply), 25 | fit: BoxFit.cover, 26 | image: newsOfTheDay.urlToImage != null 27 | ? NetworkImage(newsOfTheDay.urlToImage!) 28 | : const AssetImage('assets/images/breaking_news.png') 29 | as ImageProvider)), 30 | child: Padding( 31 | padding: const EdgeInsets.all(16.0), 32 | child: Column( 33 | mainAxisAlignment: MainAxisAlignment.center, 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | Container( 37 | decoration: BoxDecoration( 38 | borderRadius: BorderRadius.circular(24), 39 | color: Colors.grey.withOpacity(0.5)), 40 | child: Padding( 41 | padding: 42 | const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 43 | child: Text( 44 | 'News of the day', 45 | style: Theme.of(context) 46 | .textTheme 47 | .displayMedium! 48 | .copyWith(color: Colours.kTextColorOnDark), 49 | ), 50 | ), 51 | ), 52 | const SizedBox( 53 | height: 16, 54 | ), 55 | Text( 56 | newsOfTheDay.title, 57 | style: Theme.of(context) 58 | .textTheme 59 | .displayLarge! 60 | .copyWith(color: Colours.kTextColorOnDark), 61 | ), 62 | const SizedBox( 63 | height: 16, 64 | ), 65 | GestureDetector( 66 | onTap: () => onPressed(newsOfTheDay), 67 | child: Row( 68 | crossAxisAlignment: CrossAxisAlignment.center, 69 | children: [ 70 | Text( 71 | 'Learn more', 72 | style: Theme.of(context) 73 | .textTheme 74 | .displayMedium! 75 | .copyWith(color: Colours.kTextColorOnDark), 76 | ), 77 | const SizedBox( 78 | width: 8, 79 | ), 80 | const Icon( 81 | Icons.arrow_forward_rounded, 82 | color: Colours.kTextColorOnDark, 83 | size: 24, 84 | ) 85 | ], 86 | ), 87 | ) 88 | ], 89 | ), 90 | ), 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /flutter_news/lib/injection_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_news/features/news/data/datasources/news_hive_helper.dart'; 2 | import 'package:flutter_news/features/news/data/datasources/news_local_data_source.dart'; 3 | import 'package:flutter_news/features/news/data/datasources/news_remote_data_source.dart'; 4 | import 'package:flutter_news/features/news/data/repositories/news_repository_impl.dart'; 5 | import 'package:flutter_news/features/news/domain/repositories/news_repository.dart'; 6 | import 'package:flutter_news/features/news/domain/usecases/get_news.dart'; 7 | import 'package:flutter_news/features/news/presentation/bloc/news_bloc.dart'; 8 | import 'package:get_it/get_it.dart'; 9 | import 'package:http/http.dart' as http; 10 | 11 | final sl = GetIt.instance; 12 | 13 | Future init() async { 14 | // NEWS: 15 | 16 | // Data 17 | 18 | // DataSources 19 | sl.registerLazySingleton( 20 | () => NewsRemoteDataSourceImpl(client: sl())); 21 | sl.registerLazySingleton( 22 | () => NewsLocalDataSourceImpl(hive: sl())); 23 | 24 | // Repositories 25 | sl.registerLazySingleton( 26 | () => NewsRepositoryImpl(remoteDataSource: sl(), localDataSource: sl())); 27 | 28 | // Domain 29 | 30 | // Usecases 31 | sl.registerLazySingleton(() => GetNews(repository: sl())); 32 | 33 | // Presentation 34 | 35 | // BLoC 36 | sl.registerFactory(() => NewsBloc(getNewsUsecase: sl())); 37 | 38 | // Misc 39 | sl.registerLazySingleton(() => http.Client()); 40 | sl.registerLazySingleton(() => NewsHiveHelper()); 41 | } 42 | -------------------------------------------------------------------------------- /flutter_news/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_news/core/themes.dart'; 4 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 5 | import 'package:flutter_news/features/news/data/models/source_model.dart'; 6 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 7 | import 'package:flutter_news/features/news/presentation/bloc/news_bloc.dart'; 8 | import 'package:flutter_news/features/news/presentation/pages/home_page.dart'; 9 | import 'package:hive_flutter/hive_flutter.dart'; 10 | import 'injection_container.dart' as di; 11 | 12 | void main() async { 13 | WidgetsFlutterBinding.ensureInitialized(); 14 | await Hive.initFlutter(); 15 | Hive.registerAdapter(NewsModelAdapter()); 16 | Hive.registerAdapter(SourceModelAdapter()); 17 | await di.init(); 18 | runApp(const MyApp()); 19 | } 20 | 21 | class MyApp extends StatelessWidget { 22 | const MyApp({Key? key}) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return MultiBlocProvider( 27 | providers: [ 28 | BlocProvider( 29 | create: (context) => di.sl() 30 | ..add(GetNewsEvent( 31 | parameters: NewsParams( 32 | country: WidgetsBinding 33 | .instance.platformDispatcher.locale.countryCode ?? 34 | 'GB', 35 | category: CategoryType.general.categoryName))), 36 | ) 37 | ], 38 | child: MaterialApp( 39 | debugShowCheckedModeBanner: false, 40 | title: 'Flutter News', 41 | theme: Themes.appTheme, 42 | home: const HomePage(), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /flutter_news/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | bloc: 37 | dependency: transitive 38 | description: 39 | name: bloc 40 | sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "8.1.3" 44 | bloc_test: 45 | dependency: "direct main" 46 | description: 47 | name: bloc_test 48 | sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "9.1.6" 52 | boolean_selector: 53 | dependency: transitive 54 | description: 55 | name: boolean_selector 56 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.1.1" 60 | build: 61 | dependency: transitive 62 | description: 63 | name: build 64 | sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.4.1" 68 | build_config: 69 | dependency: transitive 70 | description: 71 | name: build_config 72 | sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.1.1" 76 | build_daemon: 77 | dependency: transitive 78 | description: 79 | name: build_daemon 80 | sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "4.0.1" 84 | build_resolvers: 85 | dependency: transitive 86 | description: 87 | name: build_resolvers 88 | sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.4.2" 92 | build_runner: 93 | dependency: "direct dev" 94 | description: 95 | name: build_runner 96 | sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "2.4.8" 100 | build_runner_core: 101 | dependency: transitive 102 | description: 103 | name: build_runner_core 104 | sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "7.3.0" 108 | built_collection: 109 | dependency: transitive 110 | description: 111 | name: built_collection 112 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "5.1.1" 116 | built_value: 117 | dependency: transitive 118 | description: 119 | name: built_value 120 | sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "8.9.1" 124 | characters: 125 | dependency: transitive 126 | description: 127 | name: characters 128 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.3.0" 132 | checked_yaml: 133 | dependency: transitive 134 | description: 135 | name: checked_yaml 136 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.0.3" 140 | clock: 141 | dependency: transitive 142 | description: 143 | name: clock 144 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.1.1" 148 | code_builder: 149 | dependency: transitive 150 | description: 151 | name: code_builder 152 | sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "4.10.0" 156 | collection: 157 | dependency: transitive 158 | description: 159 | name: collection 160 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.18.0" 164 | convert: 165 | dependency: transitive 166 | description: 167 | name: convert 168 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "3.1.1" 172 | coverage: 173 | dependency: transitive 174 | description: 175 | name: coverage 176 | sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "1.7.2" 180 | crypto: 181 | dependency: transitive 182 | description: 183 | name: crypto 184 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "3.0.3" 188 | cupertino_icons: 189 | dependency: "direct main" 190 | description: 191 | name: cupertino_icons 192 | sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "1.0.6" 196 | dart_style: 197 | dependency: transitive 198 | description: 199 | name: dart_style 200 | sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "2.3.4" 204 | dartz: 205 | dependency: "direct main" 206 | description: 207 | name: dartz 208 | sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "0.10.1" 212 | diff_match_patch: 213 | dependency: transitive 214 | description: 215 | name: diff_match_patch 216 | sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "0.4.1" 220 | equatable: 221 | dependency: "direct main" 222 | description: 223 | name: equatable 224 | sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "2.0.5" 228 | fake_async: 229 | dependency: transitive 230 | description: 231 | name: fake_async 232 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.3.1" 236 | ffi: 237 | dependency: transitive 238 | description: 239 | name: ffi 240 | sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "2.1.2" 244 | file: 245 | dependency: transitive 246 | description: 247 | name: file 248 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "7.0.0" 252 | fixnum: 253 | dependency: transitive 254 | description: 255 | name: fixnum 256 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.1.0" 260 | flutter: 261 | dependency: "direct main" 262 | description: flutter 263 | source: sdk 264 | version: "0.0.0" 265 | flutter_bloc: 266 | dependency: "direct main" 267 | description: 268 | name: flutter_bloc 269 | sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" 270 | url: "https://pub.dev" 271 | source: hosted 272 | version: "8.1.4" 273 | flutter_lints: 274 | dependency: "direct dev" 275 | description: 276 | name: flutter_lints 277 | sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 278 | url: "https://pub.dev" 279 | source: hosted 280 | version: "3.0.1" 281 | flutter_test: 282 | dependency: "direct dev" 283 | description: flutter 284 | source: sdk 285 | version: "0.0.0" 286 | frontend_server_client: 287 | dependency: transitive 288 | description: 289 | name: frontend_server_client 290 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 291 | url: "https://pub.dev" 292 | source: hosted 293 | version: "3.2.0" 294 | get_it: 295 | dependency: "direct main" 296 | description: 297 | name: get_it 298 | sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 299 | url: "https://pub.dev" 300 | source: hosted 301 | version: "7.6.7" 302 | glob: 303 | dependency: transitive 304 | description: 305 | name: glob 306 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 307 | url: "https://pub.dev" 308 | source: hosted 309 | version: "2.1.2" 310 | google_fonts: 311 | dependency: "direct main" 312 | description: 313 | name: google_fonts 314 | sha256: f0b8d115a13ecf827013ec9fc883390ccc0e87a96ed5347a3114cac177ef18e8 315 | url: "https://pub.dev" 316 | source: hosted 317 | version: "6.1.0" 318 | graphs: 319 | dependency: transitive 320 | description: 321 | name: graphs 322 | sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 323 | url: "https://pub.dev" 324 | source: hosted 325 | version: "2.3.1" 326 | hive: 327 | dependency: transitive 328 | description: 329 | name: hive 330 | sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" 331 | url: "https://pub.dev" 332 | source: hosted 333 | version: "2.2.3" 334 | hive_flutter: 335 | dependency: "direct main" 336 | description: 337 | name: hive_flutter 338 | sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc 339 | url: "https://pub.dev" 340 | source: hosted 341 | version: "1.1.0" 342 | hive_generator: 343 | dependency: "direct main" 344 | description: 345 | name: hive_generator 346 | sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" 347 | url: "https://pub.dev" 348 | source: hosted 349 | version: "2.0.1" 350 | http: 351 | dependency: "direct main" 352 | description: 353 | name: http 354 | sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" 355 | url: "https://pub.dev" 356 | source: hosted 357 | version: "1.2.1" 358 | http_multi_server: 359 | dependency: transitive 360 | description: 361 | name: http_multi_server 362 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 363 | url: "https://pub.dev" 364 | source: hosted 365 | version: "3.2.1" 366 | http_parser: 367 | dependency: transitive 368 | description: 369 | name: http_parser 370 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 371 | url: "https://pub.dev" 372 | source: hosted 373 | version: "4.0.2" 374 | intl: 375 | dependency: "direct main" 376 | description: 377 | name: intl 378 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 379 | url: "https://pub.dev" 380 | source: hosted 381 | version: "0.19.0" 382 | io: 383 | dependency: transitive 384 | description: 385 | name: io 386 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 387 | url: "https://pub.dev" 388 | source: hosted 389 | version: "1.0.4" 390 | js: 391 | dependency: transitive 392 | description: 393 | name: js 394 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 395 | url: "https://pub.dev" 396 | source: hosted 397 | version: "0.6.7" 398 | json_annotation: 399 | dependency: transitive 400 | description: 401 | name: json_annotation 402 | sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 403 | url: "https://pub.dev" 404 | source: hosted 405 | version: "4.8.1" 406 | leak_tracker: 407 | dependency: transitive 408 | description: 409 | name: leak_tracker 410 | sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" 411 | url: "https://pub.dev" 412 | source: hosted 413 | version: "10.0.0" 414 | leak_tracker_flutter_testing: 415 | dependency: transitive 416 | description: 417 | name: leak_tracker_flutter_testing 418 | sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 419 | url: "https://pub.dev" 420 | source: hosted 421 | version: "2.0.1" 422 | leak_tracker_testing: 423 | dependency: transitive 424 | description: 425 | name: leak_tracker_testing 426 | sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 427 | url: "https://pub.dev" 428 | source: hosted 429 | version: "2.0.1" 430 | lints: 431 | dependency: transitive 432 | description: 433 | name: lints 434 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 435 | url: "https://pub.dev" 436 | source: hosted 437 | version: "3.0.0" 438 | logging: 439 | dependency: transitive 440 | description: 441 | name: logging 442 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 443 | url: "https://pub.dev" 444 | source: hosted 445 | version: "1.2.0" 446 | matcher: 447 | dependency: transitive 448 | description: 449 | name: matcher 450 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 451 | url: "https://pub.dev" 452 | source: hosted 453 | version: "0.12.16+1" 454 | material_color_utilities: 455 | dependency: transitive 456 | description: 457 | name: material_color_utilities 458 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" 459 | url: "https://pub.dev" 460 | source: hosted 461 | version: "0.8.0" 462 | meta: 463 | dependency: transitive 464 | description: 465 | name: meta 466 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 467 | url: "https://pub.dev" 468 | source: hosted 469 | version: "1.11.0" 470 | mime: 471 | dependency: transitive 472 | description: 473 | name: mime 474 | sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" 475 | url: "https://pub.dev" 476 | source: hosted 477 | version: "1.0.5" 478 | mocktail: 479 | dependency: "direct main" 480 | description: 481 | name: mocktail 482 | sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 483 | url: "https://pub.dev" 484 | source: hosted 485 | version: "1.0.3" 486 | nested: 487 | dependency: transitive 488 | description: 489 | name: nested 490 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 491 | url: "https://pub.dev" 492 | source: hosted 493 | version: "1.0.0" 494 | node_preamble: 495 | dependency: transitive 496 | description: 497 | name: node_preamble 498 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 499 | url: "https://pub.dev" 500 | source: hosted 501 | version: "2.0.2" 502 | package_config: 503 | dependency: transitive 504 | description: 505 | name: package_config 506 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 507 | url: "https://pub.dev" 508 | source: hosted 509 | version: "2.1.0" 510 | path: 511 | dependency: transitive 512 | description: 513 | name: path 514 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 515 | url: "https://pub.dev" 516 | source: hosted 517 | version: "1.9.0" 518 | path_provider: 519 | dependency: transitive 520 | description: 521 | name: path_provider 522 | sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b 523 | url: "https://pub.dev" 524 | source: hosted 525 | version: "2.1.2" 526 | path_provider_android: 527 | dependency: transitive 528 | description: 529 | name: path_provider_android 530 | sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" 531 | url: "https://pub.dev" 532 | source: hosted 533 | version: "2.2.2" 534 | path_provider_foundation: 535 | dependency: transitive 536 | description: 537 | name: path_provider_foundation 538 | sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" 539 | url: "https://pub.dev" 540 | source: hosted 541 | version: "2.3.2" 542 | path_provider_linux: 543 | dependency: transitive 544 | description: 545 | name: path_provider_linux 546 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 547 | url: "https://pub.dev" 548 | source: hosted 549 | version: "2.2.1" 550 | path_provider_platform_interface: 551 | dependency: transitive 552 | description: 553 | name: path_provider_platform_interface 554 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 555 | url: "https://pub.dev" 556 | source: hosted 557 | version: "2.1.2" 558 | path_provider_windows: 559 | dependency: transitive 560 | description: 561 | name: path_provider_windows 562 | sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" 563 | url: "https://pub.dev" 564 | source: hosted 565 | version: "2.2.1" 566 | platform: 567 | dependency: transitive 568 | description: 569 | name: platform 570 | sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" 571 | url: "https://pub.dev" 572 | source: hosted 573 | version: "3.1.4" 574 | plugin_platform_interface: 575 | dependency: transitive 576 | description: 577 | name: plugin_platform_interface 578 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 579 | url: "https://pub.dev" 580 | source: hosted 581 | version: "2.1.8" 582 | pool: 583 | dependency: transitive 584 | description: 585 | name: pool 586 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 587 | url: "https://pub.dev" 588 | source: hosted 589 | version: "1.5.1" 590 | provider: 591 | dependency: transitive 592 | description: 593 | name: provider 594 | sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" 595 | url: "https://pub.dev" 596 | source: hosted 597 | version: "6.1.1" 598 | pub_semver: 599 | dependency: transitive 600 | description: 601 | name: pub_semver 602 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 603 | url: "https://pub.dev" 604 | source: hosted 605 | version: "2.1.4" 606 | pubspec_parse: 607 | dependency: transitive 608 | description: 609 | name: pubspec_parse 610 | sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 611 | url: "https://pub.dev" 612 | source: hosted 613 | version: "1.2.3" 614 | shelf: 615 | dependency: transitive 616 | description: 617 | name: shelf 618 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 619 | url: "https://pub.dev" 620 | source: hosted 621 | version: "1.4.1" 622 | shelf_packages_handler: 623 | dependency: transitive 624 | description: 625 | name: shelf_packages_handler 626 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 627 | url: "https://pub.dev" 628 | source: hosted 629 | version: "3.0.2" 630 | shelf_static: 631 | dependency: transitive 632 | description: 633 | name: shelf_static 634 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 635 | url: "https://pub.dev" 636 | source: hosted 637 | version: "1.1.2" 638 | shelf_web_socket: 639 | dependency: transitive 640 | description: 641 | name: shelf_web_socket 642 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 643 | url: "https://pub.dev" 644 | source: hosted 645 | version: "1.0.4" 646 | sky_engine: 647 | dependency: transitive 648 | description: flutter 649 | source: sdk 650 | version: "0.0.99" 651 | source_gen: 652 | dependency: transitive 653 | description: 654 | name: source_gen 655 | sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" 656 | url: "https://pub.dev" 657 | source: hosted 658 | version: "1.5.0" 659 | source_helper: 660 | dependency: transitive 661 | description: 662 | name: source_helper 663 | sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" 664 | url: "https://pub.dev" 665 | source: hosted 666 | version: "1.3.4" 667 | source_map_stack_trace: 668 | dependency: transitive 669 | description: 670 | name: source_map_stack_trace 671 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 672 | url: "https://pub.dev" 673 | source: hosted 674 | version: "2.1.1" 675 | source_maps: 676 | dependency: transitive 677 | description: 678 | name: source_maps 679 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 680 | url: "https://pub.dev" 681 | source: hosted 682 | version: "0.10.12" 683 | source_span: 684 | dependency: transitive 685 | description: 686 | name: source_span 687 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 688 | url: "https://pub.dev" 689 | source: hosted 690 | version: "1.10.0" 691 | stack_trace: 692 | dependency: transitive 693 | description: 694 | name: stack_trace 695 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 696 | url: "https://pub.dev" 697 | source: hosted 698 | version: "1.11.1" 699 | stream_channel: 700 | dependency: transitive 701 | description: 702 | name: stream_channel 703 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 704 | url: "https://pub.dev" 705 | source: hosted 706 | version: "2.1.2" 707 | stream_transform: 708 | dependency: transitive 709 | description: 710 | name: stream_transform 711 | sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" 712 | url: "https://pub.dev" 713 | source: hosted 714 | version: "2.1.0" 715 | string_scanner: 716 | dependency: transitive 717 | description: 718 | name: string_scanner 719 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 720 | url: "https://pub.dev" 721 | source: hosted 722 | version: "1.2.0" 723 | term_glyph: 724 | dependency: transitive 725 | description: 726 | name: term_glyph 727 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 728 | url: "https://pub.dev" 729 | source: hosted 730 | version: "1.2.1" 731 | test: 732 | dependency: transitive 733 | description: 734 | name: test 735 | sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f 736 | url: "https://pub.dev" 737 | source: hosted 738 | version: "1.24.9" 739 | test_api: 740 | dependency: transitive 741 | description: 742 | name: test_api 743 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 744 | url: "https://pub.dev" 745 | source: hosted 746 | version: "0.6.1" 747 | test_core: 748 | dependency: transitive 749 | description: 750 | name: test_core 751 | sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a 752 | url: "https://pub.dev" 753 | source: hosted 754 | version: "0.5.9" 755 | timing: 756 | dependency: transitive 757 | description: 758 | name: timing 759 | sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" 760 | url: "https://pub.dev" 761 | source: hosted 762 | version: "1.0.1" 763 | typed_data: 764 | dependency: transitive 765 | description: 766 | name: typed_data 767 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 768 | url: "https://pub.dev" 769 | source: hosted 770 | version: "1.3.2" 771 | vector_math: 772 | dependency: transitive 773 | description: 774 | name: vector_math 775 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 776 | url: "https://pub.dev" 777 | source: hosted 778 | version: "2.1.4" 779 | vm_service: 780 | dependency: transitive 781 | description: 782 | name: vm_service 783 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 784 | url: "https://pub.dev" 785 | source: hosted 786 | version: "13.0.0" 787 | watcher: 788 | dependency: transitive 789 | description: 790 | name: watcher 791 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 792 | url: "https://pub.dev" 793 | source: hosted 794 | version: "1.1.0" 795 | web: 796 | dependency: transitive 797 | description: 798 | name: web 799 | sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" 800 | url: "https://pub.dev" 801 | source: hosted 802 | version: "0.5.0" 803 | web_socket_channel: 804 | dependency: transitive 805 | description: 806 | name: web_socket_channel 807 | sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" 808 | url: "https://pub.dev" 809 | source: hosted 810 | version: "2.4.4" 811 | webkit_inspection_protocol: 812 | dependency: transitive 813 | description: 814 | name: webkit_inspection_protocol 815 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 816 | url: "https://pub.dev" 817 | source: hosted 818 | version: "1.2.1" 819 | win32: 820 | dependency: transitive 821 | description: 822 | name: win32 823 | sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" 824 | url: "https://pub.dev" 825 | source: hosted 826 | version: "5.2.0" 827 | xdg_directories: 828 | dependency: transitive 829 | description: 830 | name: xdg_directories 831 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d 832 | url: "https://pub.dev" 833 | source: hosted 834 | version: "1.0.4" 835 | yaml: 836 | dependency: transitive 837 | description: 838 | name: yaml 839 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 840 | url: "https://pub.dev" 841 | source: hosted 842 | version: "3.1.2" 843 | sdks: 844 | dart: ">=3.3.0 <3.7.1" 845 | flutter: ">=3.10.0" 846 | -------------------------------------------------------------------------------- /flutter_news/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_news 2 | description: A new Flutter project. 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.1.0+1 19 | 20 | environment: 21 | sdk: ">=2.15.0 <3.7.1" 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 | 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.5 37 | get_it: ^7.2.0 38 | flutter_bloc: ^8.1.1 39 | equatable: ^2.0.5 40 | dartz: ^0.10.1 41 | http: ^1.2.1 42 | intl: ^0.19.0 43 | mocktail: ^1.0.3 44 | bloc_test: ^9.1.0 45 | hive_flutter: ^1.1.0 46 | hive_generator: ^2.0.0 47 | google_fonts: ^6.1.0 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | build_runner: ^2.3.3 53 | 54 | # The "flutter_lints" package below contains a set of recommended lints to 55 | # encourage good coding practices. The lint set provided by the package is 56 | # activated in the `analysis_options.yaml` file located at the root of your 57 | # package. See that file for information about deactivating specific lint 58 | # rules and activating additional ones. 59 | flutter_lints: ^3.0.1 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 | - assets/images/ 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 details 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 details regarding fonts from package dependencies, 101 | # see https://flutter.dev/custom-fonts/#from-packages 102 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/data/datasources/news_local_data_source_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_news/core/exceptions.dart'; 4 | import 'package:flutter_news/features/news/data/datasources/news_hive_helper.dart'; 5 | import 'package:flutter_news/features/news/data/datasources/news_local_data_source.dart'; 6 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mocktail/mocktail.dart'; 9 | 10 | import '../../fixtures/fixture_reader.dart'; 11 | 12 | class MockNewsHiveHelper extends Mock implements NewsHiveHelper {} 13 | 14 | void main() { 15 | late NewsLocalDataSourceImpl localDataSourceImpl; 16 | late MockNewsHiveHelper hive; 17 | 18 | setUp(() { 19 | hive = MockNewsHiveHelper(); 20 | localDataSourceImpl = NewsLocalDataSourceImpl(hive: hive); 21 | }); 22 | 23 | final expectedResult = (json.decode(fixture('cached_news.json')) as List) 24 | .map((e) => NewsModel.fromJson(e)) 25 | .toList(); 26 | 27 | group('Get News', () { 28 | test('Should return a list of news from cache when there is data in cache', 29 | () async { 30 | // Arrange 31 | when(() => hive.getNews()) 32 | .thenAnswer((invocation) => Future.value(expectedResult)); 33 | // Act 34 | final news = await localDataSourceImpl.getNews(); 35 | // Assert 36 | expect(news, expectedResult); 37 | }); 38 | 39 | test('Should throw a cache exception cache when there is no data in cache', 40 | () async { 41 | // Arrange 42 | when(() => hive.getNews()) 43 | .thenAnswer((invocation) => Future.value(List.empty())); 44 | // Act 45 | // Assert 46 | expect(() async => localDataSourceImpl.getNews(), 47 | throwsA(predicate((e) => e is CacheException))); 48 | }); 49 | }); 50 | 51 | group('Save News', () { 52 | test('Should call Hive to cache data', () async { 53 | // Arrange 54 | when(() => hive.saveNews(any())) 55 | .thenAnswer((invocation) => Future.value(true)); 56 | // Act 57 | await localDataSourceImpl.saveNews(expectedResult); 58 | // Assert 59 | verify(() => hive.saveNews(expectedResult)).called(1); 60 | verifyNoMoreInteractions(hive); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/data/datasources/news_remote_data_source_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter_news/core/exceptions.dart'; 3 | import 'package:flutter_news/features/news/data/datasources/news_remote_data_source.dart'; 4 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | import '../../fixtures/fixture_reader.dart'; 10 | 11 | class MockClient extends Mock implements http.Client {} 12 | 13 | void main() { 14 | late NewsRemoteDataSourceImpl remoteDataSourceImpl; 15 | late MockClient mockClient; 16 | 17 | setUp(() { 18 | mockClient = MockClient(); 19 | remoteDataSourceImpl = NewsRemoteDataSourceImpl(client: mockClient); 20 | registerFallbackValue(Uri()); 21 | }); 22 | 23 | void setUpMockHttpClient(String fixtureName, int statusCode) { 24 | when(() => mockClient.get(any(), headers: any(named: 'headers'))) 25 | .thenAnswer((invocation) async => http.Response( 26 | fixture(fixtureName), 27 | statusCode, 28 | headers: { 29 | HttpHeaders.contentTypeHeader: 30 | 'application/json; charset=utf-8', 31 | }, 32 | )); 33 | } 34 | 35 | final newsParams = NewsParams(country: 'GB', category: 'health'); 36 | 37 | group('GET News', () { 38 | test( 39 | 'Should return a list of news models when a status code of 200 is received', 40 | () async { 41 | // Arrange 42 | setUpMockHttpClient('news.json', 200); 43 | // Act 44 | final news = await remoteDataSourceImpl.getNews(parameters: newsParams); 45 | // Assert 46 | expect(news.length, equals(20)); 47 | }); 48 | 49 | test('should throw an exception when a status code of 400 is received', 50 | () async { 51 | // Arrange 52 | setUpMockHttpClient('news.json', 400); 53 | // Act 54 | // Assert 55 | expect( 56 | () => remoteDataSourceImpl.getNews(parameters: newsParams), 57 | throwsA(predicate( 58 | (e) => e is ServerException && e.message == 'Bad Request'))); 59 | }); 60 | 61 | test('should throw an exception when a status code of 401 is received', 62 | () async { 63 | // Arrange 64 | setUpMockHttpClient('news.json', 401); 65 | // Act 66 | // Assert 67 | expect( 68 | () => remoteDataSourceImpl.getNews(parameters: newsParams), 69 | throwsA(predicate( 70 | (e) => e is ServerException && e.message == 'Unauthorized'))); 71 | }); 72 | 73 | test('should throw an exception when a status code of 500 is received', 74 | () async { 75 | // Arrange 76 | setUpMockHttpClient('news.json', 500); 77 | // Act 78 | // Assert 79 | expect( 80 | () => remoteDataSourceImpl.getNews(parameters: newsParams), 81 | throwsA(predicate((e) => 82 | e is ServerException && e.message == 'Internal Server Error'))); 83 | }); 84 | 85 | test( 86 | 'should throw an exception of Unknown Error when a non recognised status code is received', 87 | () async { 88 | // Arrange 89 | setUpMockHttpClient('news.json', 300); 90 | // Act 91 | // Assert 92 | expect( 93 | () => remoteDataSourceImpl.getNews(parameters: newsParams), 94 | throwsA(predicate( 95 | (e) => e is ServerException && e.message == 'Unknown Error'))); 96 | }); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/data/models/news_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 4 | import 'package:flutter_news/features/news/data/models/source_model.dart'; 5 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import '../../fixtures/fixture_reader.dart'; 9 | 10 | void main() { 11 | final model = NewsModel( 12 | source: const SourceModel(name: "The Guardian"), 13 | author: "Graeme Wearden", 14 | title: 15 | "UK inflation soars to 10-year high of 5.1% as cost of living squeeze tightens – business live - The Guardian", 16 | description: "Rolling coverage of the latest economic and financial news", 17 | urlToImage: 18 | "https://i.guim.co.uk/img/media/2c70c5eef5b248710501db9a8f76273c905862fe/0_71_6000_3600/master/6000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctbGl2ZS5wbmc&enable=upscale&s=51300a897c72bbc43fc4e4baee6ff552", 19 | publishedDate: DateTime.parse("2021-12-15T08:43:40Z"), 20 | content: "test content"); 21 | 22 | group('Model matches JSON', () { 23 | test('should successfully convert to a News entity', () { 24 | // Assert 25 | expect(model.toNews, isA()); 26 | }); 27 | 28 | test('Should deserialize model from JSON', () { 29 | // Arrange 30 | final jsonMap = json.decode(fixture('news_model.json')); 31 | // Act 32 | final news = NewsModel.fromJson(jsonMap); 33 | // Assert 34 | expect(news, equals(model)); 35 | expect(news.author, model.author); 36 | expect(news.source, model.source); 37 | expect(news.content, model.content); 38 | expect(news.description, model.description); 39 | expect(news.publishedDate, model.publishedDate); 40 | expect(news.title, model.title); 41 | expect(news.urlToImage, model.urlToImage); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/data/repositories/news_repository_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter_news/core/exceptions.dart'; 3 | import 'package:flutter_news/core/failures.dart'; 4 | import 'package:flutter_news/features/news/data/datasources/news_local_data_source.dart'; 5 | import 'package:flutter_news/features/news/data/datasources/news_remote_data_source.dart'; 6 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 7 | import 'package:flutter_news/features/news/data/repositories/news_repository_impl.dart'; 8 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:mocktail/mocktail.dart'; 11 | 12 | import '../../fixtures/fixture_reader.dart'; 13 | 14 | class MockNewsRemoteDataSource extends Mock implements NewsRemoteDataSource {} 15 | 16 | class MockNewsLocalDataSource extends Mock implements NewsLocalDataSource {} 17 | 18 | void main() { 19 | late NewsRepositoryImpl repositoryImpl; 20 | late MockNewsRemoteDataSource mockNewsRemoteDataSource; 21 | late MockNewsLocalDataSource mockNewsLocalDataSource; 22 | 23 | final cachedNewsModel = (json.decode(fixture('cached_news.json')) as List) 24 | .map((e) => NewsModel.fromJson(e)) 25 | .toList(); 26 | final newsModel = (json.decode(fixture('news.json'))['articles'] as List) 27 | .map((e) => NewsModel.fromJson(e)) 28 | .toList(); 29 | final newsEntityFromRemote = newsModel.map((e) => e.toNews).toList(); 30 | final newsEntityFromLocal = cachedNewsModel.map((e) => e.toNews).toList(); 31 | 32 | setUp(() { 33 | mockNewsRemoteDataSource = MockNewsRemoteDataSource(); 34 | mockNewsLocalDataSource = MockNewsLocalDataSource(); 35 | repositoryImpl = NewsRepositoryImpl( 36 | localDataSource: mockNewsLocalDataSource, 37 | remoteDataSource: mockNewsRemoteDataSource); 38 | }); 39 | 40 | final newsParams = NewsParams(country: 'GB', category: 'health'); 41 | 42 | group('Get News', () { 43 | test( 44 | 'Should return remote data when call to remote data source is successful', 45 | () async { 46 | // Arrange 47 | when(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)) 48 | .thenAnswer((invocation) async => newsModel); 49 | // Act 50 | final result = await repositoryImpl.getNews(parameters: newsParams); 51 | final resultFolded = 52 | result.fold((l) => ServerFailure(message: l.toString()), (r) => r); 53 | // Assert 54 | verify(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)); 55 | expect(result.isRight(), true); 56 | expect(resultFolded, newsEntityFromRemote); 57 | expect(resultFolded, equals(newsEntityFromRemote)); 58 | }); 59 | 60 | test('News are saved to cache when retrieved from remote data source', 61 | () async { 62 | // Arrange 63 | when(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)) 64 | .thenAnswer((invocation) async => newsModel); 65 | // Act 66 | await repositoryImpl.getNews(parameters: newsParams); 67 | // Assert 68 | verify(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)); 69 | verify(() => mockNewsLocalDataSource.saveNews(newsModel)); 70 | }); 71 | 72 | test( 73 | 'Should return failure when remote data source and local data source fail', 74 | () async { 75 | // Arrange 76 | when(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)) 77 | .thenThrow(const ServerException(message: 'Error')); 78 | when(() => mockNewsLocalDataSource.getNews()).thenThrow(CacheException()); 79 | // Act 80 | final result = await repositoryImpl.getNews(parameters: newsParams); 81 | final resultFolded = 82 | result.fold((l) => ServerFailure(message: l.toString()), (r) => r); 83 | // Assert 84 | verify(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)); 85 | verify(() => mockNewsLocalDataSource.getNews()); 86 | expect(result.isLeft(), true); 87 | expect(resultFolded, ServerFailure(message: 'Error')); 88 | }); 89 | 90 | test('News are retrieved from cache when call to remote data source fails', 91 | () async { 92 | // Arrange 93 | when(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)) 94 | .thenThrow(const ServerException(message: 'Error')); 95 | when(() => mockNewsLocalDataSource.getNews()) 96 | .thenAnswer((invocation) async => cachedNewsModel); 97 | // Act 98 | final result = await repositoryImpl.getNews(parameters: newsParams); 99 | final resultFolded = 100 | result.fold((l) => ServerFailure(message: l.toString()), (r) => r); 101 | // Assert 102 | verify(() => mockNewsRemoteDataSource.getNews(parameters: newsParams)); 103 | verify(() => mockNewsLocalDataSource.getNews()); 104 | expect(result.isRight(), true); 105 | expect(resultFolded, newsEntityFromLocal); 106 | expect(resultFolded, equals(newsEntityFromLocal)); 107 | }); 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/domain/usecases/get_news_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_news/core/failures.dart'; 3 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 4 | import 'package:flutter_news/features/news/domain/entities/source.dart'; 5 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 6 | import 'package:flutter_news/features/news/domain/repositories/news_repository.dart'; 7 | import 'package:flutter_news/features/news/domain/usecases/get_news.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:mocktail/mocktail.dart'; 10 | 11 | class MockNewsRepository extends Mock implements NewsRepository {} 12 | 13 | main() { 14 | late GetNews usecase; 15 | late MockNewsRepository mockNewsRepository; 16 | 17 | setUp(() { 18 | mockNewsRepository = MockNewsRepository(); 19 | usecase = GetNews(repository: mockNewsRepository); 20 | }); 21 | 22 | final news = [ 23 | News( 24 | author: "author", 25 | title: "title", 26 | description: "description", 27 | urlToImage: "https://img.youtube.com/vi/CA-Xe_M8mpA/maxresdefault.jpg", 28 | publishedDate: DateTime.now(), 29 | content: "content", 30 | source: const Source(name: "name")) 31 | ]; 32 | 33 | final newsParams = NewsParams(country: 'GB', category: 'health'); 34 | 35 | group('Retrieve news from repository', () { 36 | test('Should return entity from repository when call is successfull', 37 | () async { 38 | // Arrange 39 | when(() => mockNewsRepository.getNews(parameters: newsParams)) 40 | .thenAnswer((invocation) async => Right(news)); 41 | // Act 42 | final result = await usecase.execute(parameters: newsParams); 43 | // Assert 44 | expect(result, Right(news)); 45 | verify(() => mockNewsRepository.getNews(parameters: newsParams)); 46 | verifyNoMoreInteractions(mockNewsRepository); 47 | }); 48 | 49 | test('Should return failure from repository when call is unsuccessfull', 50 | () async { 51 | // Arrange 52 | final errorMessage = ServerFailure(message: "Internal Server Error"); 53 | when(() => mockNewsRepository.getNews(parameters: newsParams)) 54 | .thenAnswer((invocation) async => Left(errorMessage)); 55 | // Act 56 | final result = await usecase.execute(parameters: newsParams); 57 | // Assert 58 | expect(result, Left(errorMessage)); 59 | verify(() => mockNewsRepository.getNews(parameters: newsParams)); 60 | verifyNoMoreInteractions(mockNewsRepository); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/fixtures/cached_news.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source": { 4 | "id": null, 5 | "name": "The Guardian" 6 | }, 7 | "author": "Graeme Wearden", 8 | "title": "UK inflation soars to 10-year high of 5.1% as cost of living squeeze tightens – business live - The Guardian", 9 | "description": "Rolling coverage of the latest economic and financial news", 10 | "url": "https://www.theguardian.com/business/live/2021/dec/15/uk-inflation-soars-cost-of-living-squeeze-energy-housing-clothing-footwear-federal-reserve-business-live", 11 | "urlToImage": "https://i.guim.co.uk/img/media/2c70c5eef5b248710501db9a8f76273c905862fe/0_71_6000_3600/master/6000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctbGl2ZS5wbmc&enable=upscale&s=51300a897c72bbc43fc4e4baee6ff552", 12 | "publishedAt": "2021-12-15T08:43:40Z", 13 | "content": null 14 | }, 15 | { 16 | "source": { 17 | "id": null, 18 | "name": "Get Reading" 19 | }, 20 | "author": "Jenna Outhwaite", 21 | "title": "Updates: Reading fire leaves one dead as Grovelands Road remains closed - Berkshire Live", 22 | "description": "Grovelands Road has been closed since around 3.20am and several people remain unaccounted for", 23 | "url": "https://www.getreading.co.uk/news/reading-berkshire-news/live-reading-fire-leaves-road-22464983", 24 | "urlToImage": "https://i2-prod.getreading.co.uk/news/article19495331.ece/ALTERNATES/s1200/0_Fire-engine-stxJPG.jpg", 25 | "publishedAt": "2021-12-15T08:27:00Z", 26 | "content": "A Reading road is shut due to a 'major incident' today (Wednesday, December 15) - with one person believed to have died and several others unaccounted for.\r\nGrovelands Road is closed to traffic in bo… [+1765 chars]" 27 | }, 28 | { 29 | "source": { 30 | "id": null, 31 | "name": "Sky.com" 32 | }, 33 | "author": "Sky", 34 | "title": "Hundreds left trapped on roof of Hong Kong's World Trade Centre as fire breaks out - Sky News", 35 | "description": "", 36 | "url": "https://news.sky.com/story/dozens-trapped-as-fire-breaks-out-at-hong-kongs-world-trade-centre-12496123", 37 | "urlToImage": "https://e3.365dm.com/21/12/1600x900/skynews-world-trade-centre_5615543.jpg?20211215073020", 38 | "publishedAt": "2021-12-15T08:15:00Z", 39 | "content": null 40 | }, 41 | { 42 | "source": { 43 | "id": null, 44 | "name": "The Guardian" 45 | }, 46 | "author": "Alex von Tunzelmann", 47 | "title": "‘Chamberlain was a great man’: why has the PM fooled by Hitler been recast as a hero in new film Munich? - The Guardian", 48 | "description": "He is seen as the appeaser who fell for Hitler’s lies. But was Chamberlain scapegoated? Writer Robert Harris and actor Jeremy Irons discuss taking on history with their controversial new film", 49 | "url": "https://amp.theguardian.com/film/2021/dec/15/hitler-chamberlain-munich-edge-reason-robert-harris-jeremy-irons", 50 | "urlToImage": null, 51 | "publishedAt": "2021-12-15T07:32:00Z", 52 | "content": "FilmHe is seen as the appeaser who fell for Hitlers lies. But was Chamberlain scapegoated? Writer Robert Harris and actor Jeremy Irons discuss taking on history with their controversial new film\r\nAny… [+9593 chars]" 53 | }, 54 | { 55 | "source": { 56 | "id": null, 57 | "name": "Sky Sports" 58 | }, 59 | "author": "Sky Sports", 60 | "title": "The Ashes: England leave Mark Wood out as James Anderson returns to squad for second Test in Adelaide - Sky Sports", 61 | "description": "England have left Mark Wood out of their squad for the second Ashes Test as James Anderson and Stuart Broad are included for the day-night contest in Adelaide.", 62 | "url": "https://www.skysports.com/cricket/news/12123/12496128/the-ashes-england-leave-mark-wood-out-as-james-anderson-returns-to-squad-for-second-test-in-adelaide", 63 | "urlToImage": "https://e1.365dm.com/21/12/1600x900/skysports-mark-wood-england_5615599.jpg", 64 | "publishedAt": "2021-12-15T07:30:45Z", 65 | "content": "England have rolled the dice with another huge selection gamble ahead of the second Ashes Test, leaving out their fastest bowler Mark Wood for the day-night contest in Adelaide.\r\nWood has been omitte… [+2459 chars]" 66 | }, 67 | { 68 | "source": { 69 | "id": null, 70 | "name": "Mirror Online" 71 | }, 72 | "author": "Fraser Watson", 73 | "title": "Lewis Hamilton and Mercedes receive hope as lawyers give verdict on Max Verstappen drama - The Mirror", 74 | "description": "Mercedes' hopes of overturning the outcome from the Abu Dhabi Grand Prix have been boosted after a second leading lawyer said they have legal grounds to challenge the result", 75 | "url": "https://www.mirror.co.uk/sport/formula-1/lewis-hamilton-verstappen-lawyers-f1-25700005", 76 | "urlToImage": "https://i2-prod.mirror.co.uk/incoming/article25694489.ece/ALTERNATES/s1200/1_Abu-Dhabi-Grand-Prix-Race-Yas-Marina-Circuit.jpg", 77 | "publishedAt": "2021-12-15T07:28:26Z", 78 | "content": "Mercedes have been handed fresh hope in their bid to overturn the result of the Abu Dhabi Grand Prix after a second leading lawyer said they have strong legal grounds for a case. \r\nToto Wolff and Co … [+2721 chars]" 79 | }, 80 | { 81 | "source": { 82 | "id": "bbc-news", 83 | "name": "BBC News" 84 | }, 85 | "author": "https://www.facebook.com/bbcnews", 86 | "title": "Console shortages: Why can't I buy the Xbox Series X or PlayStation 5? - BBC News", 87 | "description": "Chinese power cuts, the pandemic and other reasons you can't get your hands on gaming hardware.", 88 | "url": "https://www.bbc.co.uk/news/newsbeat-59476611", 89 | "urlToImage": "https://ichef.bbci.co.uk/news/1024/branded_news/78E8/production/_122225903_gettyimages-883147534.jpg", 90 | "publishedAt": "2021-12-15T07:26:53Z", 91 | "content": "By Sam GruetNewsbeat reporter\r\nImage source, Getty Images\r\nRefreshing your phone, waiting for the notification that says: \"In stock.\"\r\nIf you've been trying to get your hands on a new console in the … [+4413 chars]" 92 | }, 93 | { 94 | "source": { 95 | "id": null, 96 | "name": "Teamtalk.com" 97 | }, 98 | "author": "Samuel Bannister", 99 | "title": "Arsenal given chance to snatch Chelsea target in Aubameyang solution - TEAMtalk", 100 | "description": "Arsenal could consider swapping Pierre-Emerick Aubameyang for Ousmane Dembele after Barcelona expressed an interest in the former captain, it is claimed.", 101 | "url": "https://www.teamtalk.com/arsenal/gunners-offered-ousmane-dembele-pierre-emerick-aubameyang-swap-barcelona", 102 | "urlToImage": "https://d3vlf99qeg6bpx.cloudfront.net/content/uploads/2021/03/29120130/dembele.kounde.jpg", 103 | "publishedAt": "2021-12-15T07:21:16Z", 104 | "content": "Arsenal could consider swapping Pierre-Emerick Aubameyang for Ousmane Dembele after Barcelona expressed an interest in the former captain, according to a report.\r\nAubameyang has been removed from his… [+2724 chars]" 105 | }, 106 | { 107 | "source": { 108 | "id": "bbc-news", 109 | "name": "BBC News" 110 | }, 111 | "author": "https://www.facebook.com/bbcnews", 112 | "title": "Energy firms face stricter tests after collapses - BBC News", 113 | "description": "Bosses of firms will also face vetting after criticism of the regulator about competition in the market.", 114 | "url": "https://www.bbc.co.uk/news/business-59664188", 115 | "urlToImage": "https://ichef.bbci.co.uk/news/1024/branded_news/0D34/production/_121908330_gettyimages-677955874.jpg", 116 | "publishedAt": "2021-12-15T07:15:04Z", 117 | "content": "By Kevin PeacheyPersonal finance correspondent, BBC News\r\nImage source, Getty Images\r\nEnergy companies will face more robust financial checks from January after a host of companies failed owing to a … [+3666 chars]" 118 | } 119 | 120 | ] -------------------------------------------------------------------------------- /flutter_news/test/features/news/fixtures/fixture_reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_news/features/news/data/models/news_model.dart'; 5 | 6 | String fixture(String name) => 7 | File('test/features/news/fixtures/$name').readAsStringSync(); 8 | 9 | List getMockFilms() { 10 | return (json.decode(fixture('news.json')) as List) 11 | .map((e) => NewsModel.fromJson(e)) 12 | .toList(); 13 | } 14 | -------------------------------------------------------------------------------- /flutter_news/test/features/news/fixtures/news.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "ok", 3 | "totalResults": 38, 4 | "articles": [ 5 | { 6 | "source": { 7 | "id": null, 8 | "name": "The Guardian" 9 | }, 10 | "author": "Graeme Wearden", 11 | "title": "UK inflation soars to 10-year high of 5.1% as cost of living squeeze tightens – business live - The Guardian", 12 | "description": "Rolling coverage of the latest economic and financial news", 13 | "url": "https://www.theguardian.com/business/live/2021/dec/15/uk-inflation-soars-cost-of-living-squeeze-energy-housing-clothing-footwear-federal-reserve-business-live", 14 | "urlToImage": "https://i.guim.co.uk/img/media/2c70c5eef5b248710501db9a8f76273c905862fe/0_71_6000_3600/master/6000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctbGl2ZS5wbmc&enable=upscale&s=51300a897c72bbc43fc4e4baee6ff552", 15 | "publishedAt": "2021-12-15T08:43:40Z", 16 | "content": null 17 | }, 18 | { 19 | "source": { 20 | "id": null, 21 | "name": "Get Reading" 22 | }, 23 | "author": "Jenna Outhwaite", 24 | "title": "Updates: Reading fire leaves one dead as Grovelands Road remains closed - Berkshire Live", 25 | "description": "Grovelands Road has been closed since around 3.20am and several people remain unaccounted for", 26 | "url": "https://www.getreading.co.uk/news/reading-berkshire-news/live-reading-fire-leaves-road-22464983", 27 | "urlToImage": "https://i2-prod.getreading.co.uk/news/article19495331.ece/ALTERNATES/s1200/0_Fire-engine-stxJPG.jpg", 28 | "publishedAt": "2021-12-15T08:27:00Z", 29 | "content": "A Reading road is shut due to a 'major incident' today (Wednesday, December 15) - with one person believed to have died and several others unaccounted for.\r\nGrovelands Road is closed to traffic in bo… [+1765 chars]" 30 | }, 31 | { 32 | "source": { 33 | "id": null, 34 | "name": "Sky.com" 35 | }, 36 | "author": "Sky", 37 | "title": "Hundreds left trapped on roof of Hong Kong's World Trade Centre as fire breaks out - Sky News", 38 | "description": "", 39 | "url": "https://news.sky.com/story/dozens-trapped-as-fire-breaks-out-at-hong-kongs-world-trade-centre-12496123", 40 | "urlToImage": "https://e3.365dm.com/21/12/1600x900/skynews-world-trade-centre_5615543.jpg?20211215073020", 41 | "publishedAt": "2021-12-15T08:15:00Z", 42 | "content": null 43 | }, 44 | { 45 | "source": { 46 | "id": null, 47 | "name": "The Guardian" 48 | }, 49 | "author": "Alex von Tunzelmann", 50 | "title": "‘Chamberlain was a great man’: why has the PM fooled by Hitler been recast as a hero in new film Munich? - The Guardian", 51 | "description": "He is seen as the appeaser who fell for Hitler’s lies. But was Chamberlain scapegoated? Writer Robert Harris and actor Jeremy Irons discuss taking on history with their controversial new film", 52 | "url": "https://amp.theguardian.com/film/2021/dec/15/hitler-chamberlain-munich-edge-reason-robert-harris-jeremy-irons", 53 | "urlToImage": null, 54 | "publishedAt": "2021-12-15T07:32:00Z", 55 | "content": "FilmHe is seen as the appeaser who fell for Hitlers lies. But was Chamberlain scapegoated? Writer Robert Harris and actor Jeremy Irons discuss taking on history with their controversial new film\r\nAny… [+9593 chars]" 56 | }, 57 | { 58 | "source": { 59 | "id": null, 60 | "name": "Sky Sports" 61 | }, 62 | "author": "Sky Sports", 63 | "title": "The Ashes: England leave Mark Wood out as James Anderson returns to squad for second Test in Adelaide - Sky Sports", 64 | "description": "England have left Mark Wood out of their squad for the second Ashes Test as James Anderson and Stuart Broad are included for the day-night contest in Adelaide.", 65 | "url": "https://www.skysports.com/cricket/news/12123/12496128/the-ashes-england-leave-mark-wood-out-as-james-anderson-returns-to-squad-for-second-test-in-adelaide", 66 | "urlToImage": "https://e1.365dm.com/21/12/1600x900/skysports-mark-wood-england_5615599.jpg", 67 | "publishedAt": "2021-12-15T07:30:45Z", 68 | "content": "England have rolled the dice with another huge selection gamble ahead of the second Ashes Test, leaving out their fastest bowler Mark Wood for the day-night contest in Adelaide.\r\nWood has been omitte… [+2459 chars]" 69 | }, 70 | { 71 | "source": { 72 | "id": null, 73 | "name": "Mirror Online" 74 | }, 75 | "author": "Fraser Watson", 76 | "title": "Lewis Hamilton and Mercedes receive hope as lawyers give verdict on Max Verstappen drama - The Mirror", 77 | "description": "Mercedes' hopes of overturning the outcome from the Abu Dhabi Grand Prix have been boosted after a second leading lawyer said they have legal grounds to challenge the result", 78 | "url": "https://www.mirror.co.uk/sport/formula-1/lewis-hamilton-verstappen-lawyers-f1-25700005", 79 | "urlToImage": "https://i2-prod.mirror.co.uk/incoming/article25694489.ece/ALTERNATES/s1200/1_Abu-Dhabi-Grand-Prix-Race-Yas-Marina-Circuit.jpg", 80 | "publishedAt": "2021-12-15T07:28:26Z", 81 | "content": "Mercedes have been handed fresh hope in their bid to overturn the result of the Abu Dhabi Grand Prix after a second leading lawyer said they have strong legal grounds for a case. \r\nToto Wolff and Co … [+2721 chars]" 82 | }, 83 | { 84 | "source": { 85 | "id": "bbc-news", 86 | "name": "BBC News" 87 | }, 88 | "author": "https://www.facebook.com/bbcnews", 89 | "title": "Console shortages: Why can't I buy the Xbox Series X or PlayStation 5? - BBC News", 90 | "description": "Chinese power cuts, the pandemic and other reasons you can't get your hands on gaming hardware.", 91 | "url": "https://www.bbc.co.uk/news/newsbeat-59476611", 92 | "urlToImage": "https://ichef.bbci.co.uk/news/1024/branded_news/78E8/production/_122225903_gettyimages-883147534.jpg", 93 | "publishedAt": "2021-12-15T07:26:53Z", 94 | "content": "By Sam GruetNewsbeat reporter\r\nImage source, Getty Images\r\nRefreshing your phone, waiting for the notification that says: \"In stock.\"\r\nIf you've been trying to get your hands on a new console in the … [+4413 chars]" 95 | }, 96 | { 97 | "source": { 98 | "id": null, 99 | "name": "Teamtalk.com" 100 | }, 101 | "author": "Samuel Bannister", 102 | "title": "Arsenal given chance to snatch Chelsea target in Aubameyang solution - TEAMtalk", 103 | "description": "Arsenal could consider swapping Pierre-Emerick Aubameyang for Ousmane Dembele after Barcelona expressed an interest in the former captain, it is claimed.", 104 | "url": "https://www.teamtalk.com/arsenal/gunners-offered-ousmane-dembele-pierre-emerick-aubameyang-swap-barcelona", 105 | "urlToImage": "https://d3vlf99qeg6bpx.cloudfront.net/content/uploads/2021/03/29120130/dembele.kounde.jpg", 106 | "publishedAt": "2021-12-15T07:21:16Z", 107 | "content": "Arsenal could consider swapping Pierre-Emerick Aubameyang for Ousmane Dembele after Barcelona expressed an interest in the former captain, according to a report.\r\nAubameyang has been removed from his… [+2724 chars]" 108 | }, 109 | { 110 | "source": { 111 | "id": "bbc-news", 112 | "name": "BBC News" 113 | }, 114 | "author": "https://www.facebook.com/bbcnews", 115 | "title": "Energy firms face stricter tests after collapses - BBC News", 116 | "description": "Bosses of firms will also face vetting after criticism of the regulator about competition in the market.", 117 | "url": "https://www.bbc.co.uk/news/business-59664188", 118 | "urlToImage": "https://ichef.bbci.co.uk/news/1024/branded_news/0D34/production/_121908330_gettyimages-677955874.jpg", 119 | "publishedAt": "2021-12-15T07:15:04Z", 120 | "content": "By Kevin PeacheyPersonal finance correspondent, BBC News\r\nImage source, Getty Images\r\nEnergy companies will face more robust financial checks from January after a host of companies failed owing to a … [+3666 chars]" 121 | }, 122 | { 123 | "source": { 124 | "id": null, 125 | "name": "Evening Standard" 126 | }, 127 | "author": "Matt Watts", 128 | "title": "Covid passes approved by Parliament as Boris Johnson suffers huge Tory revolt - Evening Standard", 129 | "description": "Boris Johnson has suffered the largest rebellion since he became Prime Minister as nearly 100 Tory MPs voted against Plan B measures which would usher in the mandatory use of Covid passes.", 130 | "url": "https://www.standard.co.uk/news/uk/plan-b-vote-result-tory-rebellion-boris-johnson-vaccine-passports-covid-b971993.html", 131 | "urlToImage": "https://static.standard.co.uk/2021/12/15/06/urnpublicidap.orgafdbdb1b1e41420999e419567ebc828d.jpg?width=1200&width=1200&auto=webp&quality=75", 132 | "publishedAt": "2021-12-15T07:12:17Z", 133 | "content": "The Plan B measures passed the Commons thanks to the support of Labour, but the PMs attempts to quell a rebellion on his own backbenches failed as a large number of his MPs defied the whip to vote ag… [+3181 chars]" 134 | }, 135 | { 136 | "source": { 137 | "id": null, 138 | "name": "Express" 139 | }, 140 | "author": "Ryan Taylor", 141 | "title": "Man City send Liverpool and Chelsea message with ruthless 7-0 Leeds drubbing - Express", 142 | "description": "MANCHESTER CITY hammered Leeds 7-0 at the Etihad Stadium to show they mean business in the Premier League title race.", 143 | "url": "https://www.express.co.uk/sport/football/1536261/Man-City-send-Liverpool-Chelsea-Leeds-7-0-win-Premier-League-title-race", 144 | "urlToImage": "https://cdn.images.express.co.uk/img/dynamic/67/750x445/1536261.jpg", 145 | "publishedAt": "2021-12-15T07:06:00Z", 146 | "content": null 147 | }, 148 | { 149 | "source": { 150 | "id": null, 151 | "name": "Liverpool Echo" 152 | }, 153 | "author": "Neil Docking", 154 | "title": "Cocaine dealing mum's phone was 'ringing non stop' - Liverpool Echo", 155 | "description": "She sold cocaine and heroin to fund a drug habit triggered by trauma", 156 | "url": "https://www.liverpoolecho.co.uk/news/liverpool-news/cocaine-dealing-mums-phone-ringing-22456888", 157 | "urlToImage": "https://i2-prod.liverpoolecho.co.uk/incoming/article22456955.ece/ALTERNATES/s1200/1_NDR_LEC_141221carley02.jpg", 158 | "publishedAt": "2021-12-15T06:00:00Z", 159 | "content": "A mum said she sold cocaine and heroin to fund a drug habit triggered by a traumatic event in her childhood. \r\nLiverpool Crown Court heard Jayne Carley, 48, of Bala Street, Anfield, has used Class A … [+4843 chars]" 160 | }, 161 | { 162 | "source": { 163 | "id": "independent", 164 | "name": "Independent" 165 | }, 166 | "author": "Vishwam Sankaran", 167 | "title": "Nasa probe becomes first spacecraft in history to ‘touch’ the Sun - The Independent", 168 | "description": "Probe flew through Sun’s extremely hot upper atmosphere – a feat once thought to be impossible", 169 | "url": "https://www.independent.co.uk/space/nasa-parker-solar-probe-sun-atmosphere-b1976306.html", 170 | "urlToImage": "https://static.independent.co.uk/2021/12/15/04/sunandpspcover_version1_landscape.jpg?width=1200&auto=webp&quality=75", 171 | "publishedAt": "2021-12-15T05:25:32Z", 172 | "content": "In a historic first, Nasas Parker Solar Probe has finally touched the Sun by flying through the stars extremely hot upper atmosphere of about two million degrees Fahrenheit a feat once thought to be … [+4790 chars]" 173 | }, 174 | { 175 | "source": { 176 | "id": "independent", 177 | "name": "Independent" 178 | }, 179 | "author": "Maroosha Muzaffar", 180 | "title": "Mark Meadows texts latest: House votes to hold ex-Trump aide in contempt - The Independent", 181 | "description": "Follow live updates here", 182 | "url": "https://www.independent.co.uk/news/world/americas/us-politics/trump-mark-meadows-text-messages-latest-b1975533.html", 183 | "urlToImage": "https://static.independent.co.uk/2021/12/14/01/EEUU-ASALTO_AL_CAPITOLIO_31283.jpg?width=1200&auto=webp&quality=75", 184 | "publishedAt": "2021-12-15T05:12:02Z", 185 | "content": "Hannity calls committees release of his texts to Meadows a smear attempt\r\nA day after the House committee investigating the Capitol riot released text messages to Mark Meadows during the attack, incl… [+865 chars]" 186 | }, 187 | { 188 | "source": { 189 | "id": null, 190 | "name": "Bournemouth Echo" 191 | }, 192 | "author": "Katie Clark", 193 | "title": "Latest information on Dorset's Covid booster jabs - Bournemouth Echo", 194 | "description": "Since the Prime Minister announced an expansion of the Covid booster jab rollout, people across the country have been booking boosters and queueing…", 195 | "url": "https://www.bournemouthecho.co.uk/news/19784040.latest-information-dorsets-covid-booster-jabs/", 196 | "urlToImage": "https://www.bournemouthecho.co.uk/resources/images/13296868/", 197 | "publishedAt": "2021-12-15T05:00:00Z", 198 | "content": "Since the Prime Minister announced an expansion of the Covid booster jab rollout, people across the country have been booking boosters and queueing for jabs.\r\nIn Dorset, it's been a challenge for som… [+4571 chars]" 199 | }, 200 | { 201 | "source": { 202 | "id": null, 203 | "name": "Evening Standard" 204 | }, 205 | "author": "Pa Reporter", 206 | "title": "Katie Price faces jail time as she is sentenced for drink-driving - Evening Standard", 207 | "description": "The 43-year-old ex-glamour model was arrested after being involved in a road accident near her Sussex home in September.", 208 | "url": "https://www.standard.co.uk/news/uk/katie-price-carl-woods-sussex-kate-instagram-b972017.html", 209 | "urlToImage": "https://static.standard.co.uk/2021/12/15/02/a6667336427e04c88290509280afa5ceY29udGVudHNlYXJjaGFwaSwxNjM5NTk1NDYx-2.41431218.jpg?width=1200&width=1200&auto=webp&quality=75", 210 | "publishedAt": "2021-12-15T02:45:05Z", 211 | "content": "Katie Price is due to be sentenced for drink-driving while disqualified and without insurance following a crash near her home in Sussex\r\nThe former glamour model has been warned she faces jail for th… [+1838 chars]" 212 | }, 213 | { 214 | "source": { 215 | "id": null, 216 | "name": "Daily Mail" 217 | }, 218 | "author": "Laura Parkin", 219 | "title": "Stacey Solomon shares heartfelt festive photos of her family in adorable matching velvet pyjamas - Daily Mail", 220 | "description": "Stacey Solomon, 32, took to social media to share some adorable Christmas snaps of her family on Tuesday", 221 | "url": "https://www.dailymail.co.uk/tvshowbiz/article-10310515/Stacey-Solomon-shares-heartfelt-festive-photos-family-adorable-matching-velvet-pyjamas.html", 222 | "urlToImage": "https://i.dailymail.co.uk/1s/2021/12/14/22/51797415-0-image-a-69_1639522736596.jpg", 223 | "publishedAt": "2021-12-15T02:21:00Z", 224 | "content": "She's a doting mum to three boys and recently welcomed a baby daughter. \r\nAnd, Stacey Solomon, 32, took to social media to share some adorable Christmas snaps of her family on Tuesday. \r\nThe Loose Wo… [+3620 chars]" 225 | }, 226 | { 227 | "source": { 228 | "id": null, 229 | "name": "The Guardian" 230 | }, 231 | "author": "Guardian staff reporter", 232 | "title": "‘A terrible tragedy’: US passes 800,000 Covid deaths – highest in the world - The Guardian", 233 | "description": "Figure deemed doubly heartbreaking considering the widespread availability of vaccines, as WHO warns Omicron is spreading at unprecedented rate", 234 | "url": "https://amp.theguardian.com/us-news/2021/dec/15/a-terrible-tragedy-us-tops-800000-covid-deaths-highest-in-the-world", 235 | "urlToImage": null, 236 | "publishedAt": "2021-12-15T02:07:00Z", 237 | "content": "US newsFigure deemed doubly heartbreaking amid widespread availability of vaccines, as WHO warns Omicron is spreading at unprecedented rate\r\nGuardian staff and agencies\r\nTue 14 Dec 2021 21.05 EST\r\nTh… [+3317 chars]" 238 | }, 239 | { 240 | "source": { 241 | "id": "bbc-news", 242 | "name": "BBC News" 243 | }, 244 | "author": "https://www.facebook.com/bbcnews", 245 | "title": "James Webb: A $10bn machine in search of the end of darkness - BBC News", 246 | "description": "The biggest space telescope ever built is ready to show us the first stars to light up the cosmos.", 247 | "url": "https://www.bbc.co.uk/news/science-environment-59476869", 248 | "urlToImage": "https://ichef.bbci.co.uk/news/1024/branded_news/53D4/production/_122006412_51391443339_fbf125c184_3k.jpg", 249 | "publishedAt": "2021-12-15T00:59:20Z", 250 | "content": "By Jonathan AmosBBC Science Correspondent\r\nImage source, ESO/P.Horálek\r\nDarkness. Total and complete. Few of us get to experience it. \r\nAt the bottom of a cave, perhaps; or in a basement when the pow… [+17443 chars]" 251 | }, 252 | { 253 | "source": { 254 | "id": "bbc-news", 255 | "name": "BBC News" 256 | }, 257 | "author": "https://www.facebook.com/bbcnews", 258 | "title": "Star Hobson babysitter criticises social services response - BBC News", 259 | "description": "Hollie Jones criticises Bradford City Council's response to her referral about the murdered toddler.", 260 | "url": "https://www.bbc.co.uk/news/uk-england-leeds-59660203", 261 | "urlToImage": "https://ichef.bbci.co.uk/news/1024/branded_news/C8DE/production/_122222415_star_garden_david_f.jpg", 262 | "publishedAt": "2021-12-15T00:45:51Z", 263 | "content": "By Pritti MistryBBC News\r\nImage source, Hollie Jones\r\nImage caption, Hollie Jones made the first of five referrals to social services in January 2020 after becoming concerned for Star Hobson\r\nStar Ho… [+3543 chars]" 264 | } 265 | ] 266 | } -------------------------------------------------------------------------------- /flutter_news/test/features/news/fixtures/news_model.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "id": null, 4 | "name": "The Guardian" 5 | }, 6 | "author": "Graeme Wearden", 7 | "title": "UK inflation soars to 10-year high of 5.1% as cost of living squeeze tightens – business live - The Guardian", 8 | "description": "Rolling coverage of the latest economic and financial news", 9 | "url": "https://www.theguardian.com/business/live/2021/dec/15/uk-inflation-soars-cost-of-living-squeeze-energy-housing-clothing-footwear-federal-reserve-business-live", 10 | "urlToImage": "https://i.guim.co.uk/img/media/2c70c5eef5b248710501db9a8f76273c905862fe/0_71_6000_3600/master/6000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctbGl2ZS5wbmc&enable=upscale&s=51300a897c72bbc43fc4e4baee6ff552", 11 | "publishedAt": "2021-12-15T08:43:40Z", 12 | "content": "test content" 13 | } -------------------------------------------------------------------------------- /flutter_news/test/features/news/presentation/bloc/news_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:flutter_news/core/failures.dart'; 4 | import 'package:flutter_news/features/news/domain/entities/news.dart'; 5 | import 'package:flutter_news/features/news/domain/entities/source.dart'; 6 | import 'package:flutter_news/features/news/domain/params/news_params.dart'; 7 | import 'package:flutter_news/features/news/domain/usecases/get_news.dart'; 8 | import 'package:flutter_news/features/news/presentation/bloc/news_bloc.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:mocktail/mocktail.dart'; 11 | 12 | class MockGetNews extends Mock implements GetNews {} 13 | 14 | void main() { 15 | late MockGetNews mockGetNewsUsecase; 16 | late NewsBloc newsBloc; 17 | 18 | final List news = [ 19 | News( 20 | author: 'author', 21 | title: 'title', 22 | description: 'description', 23 | urlToImage: 'urlToImage', 24 | publishedDate: DateTime.now(), 25 | content: 'content', 26 | source: const Source(name: 'name')), 27 | News( 28 | author: 'author 2', 29 | title: 'title 2', 30 | description: 'description 2', 31 | urlToImage: 'urlToImage 2', 32 | publishedDate: DateTime.now(), 33 | content: 'content 2', 34 | source: const Source(name: 'name 2')) 35 | ]; 36 | 37 | final newsParams = NewsParams(country: 'GB', category: 'health'); 38 | 39 | setUp(() { 40 | mockGetNewsUsecase = MockGetNews(); 41 | newsBloc = NewsBloc(getNewsUsecase: mockGetNewsUsecase); 42 | }); 43 | 44 | group('GET News Usecase', () { 45 | test('Inital bloc state should be NewsInitial', () { 46 | expect(newsBloc.state, equals(NewsInitial())); 47 | }); 48 | 49 | test('Bloc calls GetNews usecase', () async { 50 | // Arrange 51 | when(() => mockGetNewsUsecase.execute(parameters: newsParams)) 52 | .thenAnswer((invocation) async => Right(news)); 53 | // Act 54 | newsBloc.add(GetNewsEvent(parameters: newsParams)); 55 | await untilCalled( 56 | () => mockGetNewsUsecase.execute(parameters: newsParams)); 57 | // Assert 58 | verify(() => mockGetNewsUsecase.execute(parameters: newsParams)); 59 | }); 60 | 61 | blocTest( 62 | 'Should emit correct order or states when GetNews is called with success', 63 | build: () { 64 | when(() => mockGetNewsUsecase.execute(parameters: newsParams)) 65 | .thenAnswer((invocation) async => Right(news)); 66 | return newsBloc; 67 | }, 68 | act: (_) { 69 | return newsBloc.add(GetNewsEvent(parameters: newsParams)); 70 | }, 71 | expect: () { 72 | return [NewsLoading(), NewsLoadedWithSuccess(news: news)]; 73 | }, 74 | ); 75 | 76 | blocTest( 77 | 'Should emit correct order or states when GetNews is called with error', 78 | build: () { 79 | when(() => mockGetNewsUsecase.execute(parameters: newsParams)) 80 | .thenAnswer( 81 | (invocation) async => Left(ServerFailure(message: 'Error'))); 82 | return newsBloc; 83 | }, 84 | act: (_) { 85 | return newsBloc.add(GetNewsEvent(parameters: newsParams)); 86 | }, 87 | expect: () { 88 | return [NewsLoading(), NewsLoadedWithError(message: 'Error')]; 89 | }, 90 | ); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /flutter_news/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/web/favicon.png -------------------------------------------------------------------------------- /flutter_news/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/web/icons/Icon-192.png -------------------------------------------------------------------------------- /flutter_news/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/web/icons/Icon-512.png -------------------------------------------------------------------------------- /flutter_news/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /flutter_news/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/flutter_news/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /flutter_news/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_news 33 | 34 | 35 | 36 | 39 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /flutter_news/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_news", 3 | "short_name": "flutter_news", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /images/clean-architecture-flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/images/clean-architecture-flutter.png -------------------------------------------------------------------------------- /images/clean-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/images/clean-architecture.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/images/logo.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/images/screenshot.png -------------------------------------------------------------------------------- /images/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajvelo/Flutter-News/495890640f5e7621f63853641c2fbb2c23519e80/images/screenshot2.png --------------------------------------------------------------------------------