├── 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 |
32 |
33 |
34 |
35 |
36 |
37 | Table of Contents
38 |
39 |
40 | About The Project
41 |
44 |
45 |
46 | Getting Started
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | ## About The Project
58 |
59 |
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 |
25 |
26 |
27 |
28 |
29 |
30 | Table of Contents
31 |
32 |
33 | About The Project
34 |
39 |
40 |
41 | Getting Started
42 |
47 |
48 |
49 | Testing
50 |
53 |
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 |
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 |
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
--------------------------------------------------------------------------------