├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj └── .gitignore ├── lib ├── app │ ├── presentation │ │ ├── home_bloc │ │ │ ├── widgets │ │ │ │ ├── widgets.dart │ │ │ │ ├── fab.dart │ │ │ │ └── articles_view.dart │ │ │ ├── controller │ │ │ │ ├── index.dart │ │ │ │ ├── event.dart │ │ │ │ ├── state.dart │ │ │ │ ├── bloc_controller.dart │ │ │ │ ├── event.freezed.dart │ │ │ │ └── state.freezed.dart │ │ │ └── view │ │ │ │ └── home_view.dart │ │ ├── home_cubit │ │ │ ├── widgets │ │ │ │ ├── widgets.dart │ │ │ │ ├── fab.dart │ │ │ │ └── articles_view.dart │ │ │ ├── controller │ │ │ │ ├── index.dart │ │ │ │ ├── state.dart │ │ │ │ ├── cubit_controller.dart │ │ │ │ └── state.freezed.dart │ │ │ └── view │ │ │ │ └── home_view.dart │ │ ├── home_get │ │ │ ├── widgets │ │ │ │ ├── widgets.dart │ │ │ │ ├── fab.dart │ │ │ │ ├── connectivity_icon.dart │ │ │ │ └── articles_view.dart │ │ │ ├── home_binding.dart │ │ │ ├── home_view.dart │ │ │ └── home_controller.dart │ │ ├── loading │ │ │ ├── loading_binding.dart │ │ │ ├── loading_controller.dart │ │ │ └── loading_view.dart │ │ └── index.dart │ ├── core │ │ ├── assets │ │ │ └── constans.dart │ │ ├── errors │ │ │ └── failure.dart │ │ ├── widgets │ │ │ ├── index.dart │ │ │ ├── loading_widget.dart │ │ │ ├── asset_image_widget.dart │ │ │ ├── error_widget.dart │ │ │ ├── keep_alive_wrapper.dart │ │ │ └── image_handler_widget.dart │ │ ├── utils │ │ │ └── launcher.dart │ │ ├── usecases │ │ │ └── usecase.dart │ │ └── network │ │ │ └── network_info.dart │ ├── routes │ │ ├── app_routes.dart │ │ └── app_pages.dart │ ├── theme │ │ └── theme_data.dart │ ├── domain │ │ ├── repositories │ │ │ └── articles_repository.dart │ │ ├── entities │ │ │ └── article.dart │ │ └── usecases │ │ │ ├── get_remote_articles.dart │ │ │ └── get_local_articles.dart │ └── data │ │ ├── datasources │ │ ├── local │ │ │ ├── articles_local_datasource.dart │ │ │ ├── hive │ │ │ │ ├── article.dart │ │ │ │ ├── article.g.dart │ │ │ │ └── articles_local_datasource_hive.dart │ │ │ ├── sembast │ │ │ │ └── articles_local_datasource_sembast.dart │ │ │ └── sqflite │ │ │ │ └── articles_local_datasource_sqlite.dart │ │ └── remote │ │ │ └── articles_remote_datasource.dart │ │ ├── api │ │ ├── api.dart │ │ └── api.g.dart │ │ ├── models │ │ ├── article_model.dart │ │ └── article_model.g.dart │ │ └── repositories │ │ └── articles_repository_impl.dart ├── main.dart └── di │ ├── injector.g.dart │ └── injector.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ └── Icon-512.png ├── manifest.json └── index.html ├── assets └── images │ ├── icon.png │ ├── loading.jpg │ ├── placeHolder.jpg │ └── splash_icon.png ├── screenshots ├── screenshot_1.jpg ├── screenshot_3.jpg ├── screenshot_4.jpg └── screenshot_5.jpg ├── android ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── omergamliel │ │ │ │ │ └── getx_hacker_news_api │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── .gitignore ├── settings.gradle └── build.gradle ├── test ├── data │ ├── datasources │ │ ├── fake_data.dart │ │ └── articles_remote_datasource_test.dart │ └── repository │ │ └── articles_repository_impl_test.dart ├── test_helper.dart ├── core │ └── network │ │ └── network_info_test.dart ├── domain │ └── usecases │ │ ├── get_local_articles_test.dart │ │ └── get_remote_articles_test.dart └── presentation │ ├── home_bloc │ └── articles_bloc_test.dart │ └── home_cubit │ └── articles_cubit_test.dart ├── test_driver ├── app.dart └── app_test.dart ├── analysis_options.yaml ├── .gitignore ├── README.md └── pubspec.yaml /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'fab.dart'; 2 | export 'articles_view.dart'; 3 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'fab.dart'; 2 | export 'articles_view.dart'; 3 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/web/favicon.png -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/controller/index.dart: -------------------------------------------------------------------------------- 1 | export 'cubit_controller.dart'; 2 | export 'state.dart'; 3 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /assets/images/loading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/assets/images/loading.jpg -------------------------------------------------------------------------------- /assets/images/placeHolder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/assets/images/placeHolder.jpg -------------------------------------------------------------------------------- /assets/images/splash_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/assets/images/splash_icon.png -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/controller/index.dart: -------------------------------------------------------------------------------- 1 | export 'bloc_controller.dart'; 2 | export 'event.dart'; 3 | export 'state.dart'; 4 | -------------------------------------------------------------------------------- /screenshots/screenshot_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/screenshots/screenshot_1.jpg -------------------------------------------------------------------------------- /screenshots/screenshot_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/screenshots/screenshot_3.jpg -------------------------------------------------------------------------------- /screenshots/screenshot_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/screenshots/screenshot_4.jpg -------------------------------------------------------------------------------- /screenshots/screenshot_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/screenshots/screenshot_5.jpg -------------------------------------------------------------------------------- /lib/app/presentation/home_get/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'fab.dart'; 2 | export 'articles_view.dart'; 3 | export 'connectivity_icon.dart'; 4 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /lib/app/core/assets/constans.dart: -------------------------------------------------------------------------------- 1 | const loadingAsset = 'assets/images/loading.jpg'; 2 | const placeholderAsset = 'assets/images/placeHolder.jpg'; 3 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFEB3B 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/app/core/errors/failure.dart: -------------------------------------------------------------------------------- 1 | class Failure { 2 | const Failure(this.message); 3 | final String message; 4 | @override 5 | String toString() { 6 | return message; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /lib/app/core/widgets/index.dart: -------------------------------------------------------------------------------- 1 | export 'error_widget.dart'; 2 | export 'keep_alive_wrapper.dart'; 3 | export 'loading_widget.dart'; 4 | export 'image_handler_widget.dart'; 5 | export 'asset_image_widget.dart'; 6 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omergamliel3/flutter-clean-architecture-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/app/routes/app_routes.dart: -------------------------------------------------------------------------------- 1 | part of 'app_pages.dart'; 2 | // DO NOT EDIT. This is code generated via package:get_cli/get_cli.dart 3 | 4 | abstract class Routes { 5 | static const HOME = '/home'; 6 | static const LOADING = '/loading'; 7 | } 8 | -------------------------------------------------------------------------------- /lib/app/theme/theme_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Light theme data 4 | ThemeData lightThemeData() { 5 | return ThemeData.light(); 6 | } 7 | 8 | // Dark theme data 9 | ThemeData darkThemeData() { 10 | return ThemeData.dark(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/app/presentation/loading/loading_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'loading_controller.dart'; 4 | 5 | class LoadingBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.put(LoadingController()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/app/presentation/home_get/home_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'home_controller.dart'; 4 | 5 | class HomeBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut( 9 | () => HomeController(), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/data/datasources/fake_data.dart: -------------------------------------------------------------------------------- 1 | String fakeData = 2 | // ignore: lines_longer_than_80_chars 3 | '{"articles": "[{"title": "test", "content": "test", "publishedAt":"test", "url":"test", "urlToImage": "urlToImage"}, {"title": "test", "content": "test", "publishedAt":"test", "url":"test", "urlToImage": "urlToImage"}]"}'; 4 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/omergamliel/getx_hacker_news_api/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.omergamliel.getx_hacker_news_api 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | import android.os.Build 6 | import android.view.ViewTreeObserver 7 | import android.view.WindowManager 8 | class MainActivity: FlutterActivity() { 9 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/controller/event.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | part 'event.freezed.dart'; 5 | 6 | @freezed 7 | abstract class ArticlesEvent with _$ArticlesEvent { 8 | const factory ArticlesEvent.getData() = GetData; 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/presentation/index.dart: -------------------------------------------------------------------------------- 1 | export '../presentation/loading/loading_view.dart'; 2 | export '../presentation/loading/loading_binding.dart'; 3 | export '../presentation/home_get/home_view.dart'; 4 | export '../presentation/home_get/home_binding.dart'; 5 | export 'home_cubit/view/home_view.dart'; 6 | 7 | export 'home_bloc/view/home_view.dart'; 8 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | !gradle/wrapper/gradle-wrapper.jar 3 | /.gradle 4 | /captures/ 5 | /gradlew 6 | /gradlew.bat 7 | /local.properties 8 | GeneratedPluginRegistrant.java 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 12 | key.properties 13 | -------------------------------------------------------------------------------- /lib/app/core/utils/launcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:url_launcher/url_launcher.dart' as url_launcher; 3 | 4 | // launch call via url launcher 5 | Future launch(String url) async { 6 | if (await url_launcher.canLaunch(url)) { 7 | url_launcher.launch(url); 8 | } else { 9 | Get.snackbar('Ooops...', 'Can not launch url'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/app/core/widgets/loading_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingWidget extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return const Center( 7 | child: CircularProgressIndicator( 8 | valueColor: AlwaysStoppedAnimation(Colors.black), 9 | ), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/app/domain/repositories/articles_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import '../../domain/entities/article.dart'; 3 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 4 | 5 | abstract class ArticlesRepository { 6 | Future>> getRemoteArticles(); 7 | Future>> getLocalArticles(); 8 | } 9 | -------------------------------------------------------------------------------- /test_driver/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_driver/driver_extension.dart'; 2 | import 'package:getx_hacker_news_api/main.dart' as app; 3 | 4 | void main() { 5 | // This line enables the extension. 6 | enableFlutterDriverExtension(); 7 | // Call the `main()` function of the app, or call `runApp` with 8 | // any widget you are interested in testing. 9 | app.main(); 10 | } 11 | -------------------------------------------------------------------------------- /lib/app/core/widgets/asset_image_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../assets/constans.dart'; 3 | 4 | class AssetImageWidget extends StatelessWidget { 5 | const AssetImageWidget(); 6 | @override 7 | Widget build(BuildContext context) { 8 | return Image.asset( 9 | placeholderAsset, 10 | fit: BoxFit.cover, 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/app/data/datasources/local/articles_local_datasource.dart: -------------------------------------------------------------------------------- 1 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 2 | 3 | abstract class ArticlesLocalDatasource { 4 | Future initDb(); 5 | Future deleteDb(); 6 | Future insertArticles(List articles); 7 | Future deleteAllArticles(); 8 | Future> getArticles(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/core/usecases/usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import '../errors/failure.dart'; 4 | 5 | // ignore: one_member_abstracts 6 | abstract class UseCase { 7 | Future> call(Params params); 8 | } 9 | 10 | class NoParams extends Equatable { 11 | @override 12 | List get props => []; 13 | } 14 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: false 6 | directives_ordering: false 7 | type_annotate_public_apis: false 8 | avoid_classes_with_only_static_members: false 9 | avoid_print: false 10 | constant_identifier_names: false 11 | sized_box_for_whitespace: false 12 | 13 | analyzer: 14 | exclude: 15 | - "**/*.g.dart" 16 | - "**/*.freezed.dart" -------------------------------------------------------------------------------- /test/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:getx_hacker_news_api/app/domain/entities/article.dart'; 2 | 3 | final articles =
[ 4 | Article( 5 | title: 'test1', 6 | content: 'test1', 7 | publishedAt: DateTime.now(), 8 | url: 'url1', 9 | urlToImage: 'urlToImage1'), 10 | Article( 11 | title: 'test2', 12 | content: 'test2', 13 | publishedAt: DateTime.now(), 14 | url: 'url2', 15 | urlToImage: 'urlToImage2') 16 | ]; 17 | -------------------------------------------------------------------------------- /lib/app/routes/app_pages.dart: -------------------------------------------------------------------------------- 1 | import '../presentation/index.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | part 'app_routes.dart'; 5 | 6 | class AppPages { 7 | static const INITIAL = Routes.LOADING; 8 | 9 | static final routes = [ 10 | GetPage( 11 | name: Routes.HOME, 12 | page: () => HomeViewCubit(), 13 | ), 14 | GetPage( 15 | name: Routes.LOADING, 16 | page: () => LoadingView(), 17 | binding: LoadingBinding(), 18 | ), 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/app/data/api/api.dart: -------------------------------------------------------------------------------- 1 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 2 | 3 | import 'package:retrofit/retrofit.dart'; 4 | import 'package:dio/dio.dart'; 5 | 6 | part 'api.g.dart'; 7 | 8 | @RestApi(baseUrl: "https://newsapi.org/v2/") 9 | abstract class RestClient { 10 | factory RestClient(Dio dio, {String baseUrl}) = _RestClient; 11 | 12 | @GET("/top-headlines?language=en&pageSize=100&apiKey={apikey}") 13 | Future> getTopHeadlines(@Path('apikey') String apikey); 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/app/core/widgets/error_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const DEFAULT_MESSAGE = 'Something went wrong'; 4 | 5 | class ErrorWidget extends StatelessWidget { 6 | final String message; 7 | const ErrorWidget([this.message]); 8 | @override 9 | Widget build(BuildContext context) { 10 | return Center( 11 | child: Text( 12 | message ?? DEFAULT_MESSAGE, 13 | style: const TextStyle( 14 | fontSize: 24, 15 | color: Colors.black, 16 | ), 17 | )); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/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 | var flutter_native_splash = 1 11 | UIApplication.shared.isStatusBarHidden = false 12 | 13 | GeneratedPluginRegistrant.register(with: self) 14 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 15 | } 16 | } -------------------------------------------------------------------------------- /lib/app/domain/entities/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class Article extends Equatable { 5 | final String title; 6 | final String content; 7 | final DateTime publishedAt; 8 | final String url; 9 | final String urlToImage; 10 | const Article({ 11 | @required this.title, 12 | @required this.content, 13 | @required this.publishedAt, 14 | @required this.url, 15 | @required this.urlToImage, 16 | }); 17 | 18 | @override 19 | List get props => [title, content, publishedAt, url, urlToImage]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/app/presentation/loading/loading_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:getx_hacker_news_api/app/data/datasources/local/articles_local_datasource.dart'; 5 | import '../../routes/app_pages.dart'; 6 | 7 | import '../../../di/injector.dart'; 8 | 9 | class LoadingController extends GetxController { 10 | @override 11 | Future onInit() async { 12 | super.onInit(); 13 | await Injector.resolve().initDb(); 14 | await Future.delayed(const Duration(seconds: 1)); 15 | Get.offAndToNamed(Routes.HOME); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/app/data/datasources/local/hive/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | part 'article.g.dart'; 4 | 5 | @HiveType(adapterName: 'ArticleAdapter', typeId: 0) 6 | class Article { 7 | @HiveField(0) 8 | final String title; 9 | 10 | @HiveField(1) 11 | final String content; 12 | 13 | @HiveField(2) 14 | final String publishedAt; 15 | 16 | @HiveField(3) 17 | final String url; 18 | 19 | @HiveField(4) 20 | final String urlToImage; 21 | 22 | Article({ 23 | this.title, 24 | this.content, 25 | this.publishedAt, 26 | this.url, 27 | this.urlToImage, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/app/presentation/home_get/widgets/fab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FAB extends StatelessWidget { 4 | final VoidCallback callback; 5 | const FAB({this.callback}); 6 | @override 7 | Widget build(BuildContext context) { 8 | return FloatingActionButton.extended( 9 | onPressed: callback, 10 | backgroundColor: Colors.amber, 11 | icon: const Icon( 12 | Icons.refresh, 13 | color: Colors.black, 14 | ), 15 | label: const Text( 16 | 'REFRESH', 17 | style: TextStyle(color: Colors.black), 18 | )); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/app/domain/usecases/get_remote_articles.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../repositories/articles_repository.dart'; 4 | 5 | import '../../domain/entities/article.dart'; 6 | 7 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 8 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 9 | 10 | class GetRemoteArticles implements UseCase, NoParams> { 11 | final ArticlesRepository repository; 12 | GetRemoteArticles(this.repository); 13 | @override 14 | Future>> call(NoParams params) async { 15 | return repository.getRemoteArticles(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/app/core/widgets/keep_alive_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class KeepAliveWrapper extends StatefulWidget { 5 | final Widget child; 6 | const KeepAliveWrapper({@required this.child}); 7 | @override 8 | _KeepAliveWrapperState createState() => _KeepAliveWrapperState(); 9 | } 10 | 11 | class _KeepAliveWrapperState extends State 12 | with AutomaticKeepAliveClientMixin { 13 | @override 14 | Widget build(BuildContext context) { 15 | super.build(context); 16 | return Container(child: widget.child); 17 | } 18 | 19 | @override 20 | bool get wantKeepAlive => true; 21 | } 22 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/controller/state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 5 | import 'package:getx_hacker_news_api/app/domain/entities/article.dart'; 6 | 7 | part 'state.freezed.dart'; 8 | 9 | @freezed 10 | abstract class ArticlesState with _$ArticlesState { 11 | const factory ArticlesState.initial() = Initial; 12 | const factory ArticlesState.loading() = Loading; 13 | const factory ArticlesState.success(List
articles) = Success; 14 | const factory ArticlesState.error(Failure failure) = Error; 15 | } 16 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/controller/state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 5 | import 'package:getx_hacker_news_api/app/domain/entities/article.dart'; 6 | 7 | part 'state.freezed.dart'; 8 | 9 | @freezed 10 | abstract class ArticlesState with _$ArticlesState { 11 | const factory ArticlesState.initial() = Initial; 12 | const factory ArticlesState.loading() = Loading; 13 | const factory ArticlesState.success(List
articles) = Success; 14 | const factory ArticlesState.error(Failure failure) = Error; 15 | } 16 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getx_hacker_news_api", 3 | "short_name": "getx_hacker_news_api", 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 | } 24 | -------------------------------------------------------------------------------- /lib/app/domain/usecases/get_local_articles.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../repositories/articles_repository.dart'; 4 | 5 | import '../../domain/entities/article.dart'; 6 | 7 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 8 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 9 | 10 | class GetLocalArticles implements UseCase, NoParams> { 11 | final ArticlesRepository repository; 12 | GetLocalArticles(this.repository); 13 | @override 14 | Future>> call(NoParams params) async { 15 | await Future.delayed(const Duration(seconds: 1)); 16 | return repository.getLocalArticles(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/widgets/fab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import '../controller/index.dart'; 4 | 5 | class FAB extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return FloatingActionButton.extended( 9 | onPressed: () => 10 | BlocProvider.of(context).add(const GetData()), 11 | backgroundColor: Colors.amber, 12 | icon: const Icon( 13 | Icons.refresh, 14 | color: Colors.black, 15 | ), 16 | label: const Text( 17 | 'REFRESH', 18 | style: TextStyle(color: Colors.black), 19 | )); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 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 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/widgets/fab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import '../controller/index.dart'; 4 | 5 | class FAB extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return FloatingActionButton.extended( 9 | key: const ValueKey('FAB'), 10 | onPressed: () => BlocProvider.of(context).getArticles(), 11 | backgroundColor: Colors.amber, 12 | icon: const Icon( 13 | Icons.refresh, 14 | color: Colors.black, 15 | ), 16 | label: const Text( 17 | 'REFRESH', 18 | style: TextStyle(color: Colors.black), 19 | )); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:flutter_dotenv/flutter_dotenv.dart' as dotenv; 5 | 6 | import 'package:getx_hacker_news_api/app/theme/theme_data.dart'; 7 | 8 | import 'app/routes/app_pages.dart'; 9 | import 'di/injector.dart'; 10 | 11 | Future main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | Injector.setup(); 14 | await dotenv.load(); 15 | runApp( 16 | GetMaterialApp( 17 | debugShowCheckedModeBanner: false, 18 | defaultTransition: Transition.topLevel, 19 | title: "NewsReader", 20 | initialRoute: AppPages.INITIAL, 21 | getPages: AppPages.routes, 22 | theme: lightThemeData(), 23 | darkTheme: darkThemeData(), 24 | ), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/app/data/models/article_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'package:getx_hacker_news_api/app/domain/entities/article.dart'; 5 | 6 | part 'article_model.g.dart'; 7 | 8 | @JsonSerializable() 9 | class ArticleModel extends Article { 10 | const ArticleModel({ 11 | @required String title, 12 | @required String content, 13 | @required DateTime publishedAt, 14 | @required String url, 15 | @required String urlToImage, 16 | }) : super( 17 | title: title, 18 | content: content, 19 | publishedAt: publishedAt, 20 | url: url, 21 | urlToImage: urlToImage); 22 | 23 | factory ArticleModel.fromJson(Map json) => 24 | _$ArticleModelFromJson(json); 25 | Map toJson() => _$ArticleModelToJson(this); 26 | } 27 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/app/core/widgets/image_handler_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../assets/constans.dart'; 4 | 5 | import 'index.dart'; 6 | 7 | class ImageHandlerWidget extends StatelessWidget { 8 | final String urlToImage; 9 | const ImageHandlerWidget({@required this.urlToImage}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return KeepAliveWrapper( 14 | child: ClipRRect( 15 | borderRadius: BorderRadius.circular(24.0), 16 | child: AspectRatio( 17 | aspectRatio: 1, 18 | child: FadeInImage.assetNetwork( 19 | fit: BoxFit.cover, 20 | fadeInDuration: const Duration(milliseconds: 500), 21 | placeholder: loadingAsset, 22 | image: urlToImage ?? '', 23 | imageErrorBuilder: (context, obj, error) => 24 | const AssetImageWidget(), 25 | ), 26 | )), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/app/core/network/network_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity/connectivity.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | abstract class NetworkInfoI { 5 | Future isConnected(); 6 | Future get connectivityResult; 7 | Stream get onConnectivityChanged; 8 | } 9 | 10 | class NetworkInfo implements NetworkInfoI { 11 | final Connectivity connectivity; 12 | NetworkInfo({@required this.connectivity}); 13 | 14 | @override 15 | Future isConnected() async { 16 | final result = await connectivity.checkConnectivity(); 17 | if (result != ConnectivityResult.none) { 18 | return true; 19 | } 20 | return false; 21 | } 22 | 23 | @override 24 | Future get connectivityResult async { 25 | return connectivity.checkConnectivity(); 26 | } 27 | 28 | @override 29 | Stream get onConnectivityChanged => 30 | connectivity.onConnectivityChanged; 31 | } 32 | -------------------------------------------------------------------------------- /lib/app/presentation/loading/loading_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'loading_controller.dart'; 4 | 5 | class LoadingView extends GetView { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | backgroundColor: Colors.yellow, 10 | body: Column( 11 | mainAxisAlignment: MainAxisAlignment.center, 12 | children: [ 13 | Container( 14 | alignment: Alignment.center, 15 | child: const Icon( 16 | Icons.adb, 17 | size: 55, 18 | color: Colors.black, 19 | )).paddingOnly(top: Get.mediaQuery.size.height * 0.4), 20 | const Spacer(), 21 | const Text( 22 | 'LOADING...', 23 | style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), 24 | ).paddingOnly(bottom: 20.0) 25 | ], 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/app/data/models/article_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'article_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ArticleModel _$ArticleModelFromJson(Map json) { 10 | return ArticleModel( 11 | title: json['title'] as String, 12 | content: json['content'] as String, 13 | publishedAt: json['publishedAt'] == null 14 | ? null 15 | : DateTime.parse(json['publishedAt'] as String), 16 | url: json['url'] as String, 17 | urlToImage: json['urlToImage'] as String, 18 | ); 19 | } 20 | 21 | Map _$ArticleModelToJson(ArticleModel instance) => 22 | { 23 | 'title': instance.title, 24 | 'content': instance.content, 25 | 'publishedAt': instance.publishedAt?.toIso8601String(), 26 | 'url': instance.url, 27 | 'urlToImage': instance.urlToImage, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Exceptions to above rules. 44 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 45 | 46 | pubspec.lock 47 | get_x_hacker_news_a_p_i.iml 48 | .packages 49 | .metadata 50 | private 51 | architecture-proposal.png 52 | trash 53 | 54 | .env* -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /test/core/network/network_info_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity/connectivity.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'package:getx_hacker_news_api/app/core/network/network_info.dart'; 6 | 7 | class ConnectivityMock extends Mock implements Connectivity {} 8 | 9 | void main() { 10 | NetworkInfoI networkInfo; 11 | Connectivity connectivity; 12 | 13 | setUp(() { 14 | connectivity = ConnectivityMock(); 15 | networkInfo = NetworkInfo(connectivity: connectivity); 16 | }); 17 | 18 | group('isConnected ', () { 19 | test( 20 | 'should call Connectivity.checkConnectivity and return true if result is wifi / mobile, else return false', 21 | () async { 22 | // arrange 23 | when(connectivity.checkConnectivity()) 24 | .thenAnswer((_) => Future.value(ConnectivityResult.wifi)); 25 | 26 | // act 27 | final result = await networkInfo.isConnected(); 28 | 29 | // assert 30 | verify(connectivity.checkConnectivity()); 31 | expect(result, true); 32 | }, 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/app/presentation/home_get/widgets/connectivity_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:connectivity/connectivity.dart'; 5 | import '../home_controller.dart'; 6 | 7 | class ConnectivityIcon extends StatelessWidget { 8 | final HomeController controller; 9 | const ConnectivityIcon({@required this.controller}); 10 | @override 11 | Widget build(BuildContext context) { 12 | return Obx(() { 13 | switch (controller.connectvityResult.value) { 14 | case ConnectivityResult.none: 15 | return const Icon( 16 | Icons.signal_wifi_off, 17 | color: Colors.black, 18 | ); 19 | break; 20 | case ConnectivityResult.mobile: 21 | return const Icon( 22 | Icons.network_cell, 23 | color: Colors.black, 24 | ); 25 | case ConnectivityResult.wifi: 26 | return const Icon( 27 | Icons.wifi, 28 | color: Colors.black, 29 | ); 30 | default: 31 | return Container(); 32 | } 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/app/data/datasources/remote/articles_remote_datasource.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 3 | import 'package:meta/meta.dart'; 4 | import 'package:dio/dio.dart'; 5 | 6 | import 'package:getx_hacker_news_api/app/data/api/api.dart'; 7 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 8 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 9 | 10 | const ERROR_MSG = 'Something went wrong'; 11 | 12 | class ArticlesRemoteDatasource { 13 | final RestClient client; 14 | ArticlesRemoteDatasource({@required this.client}); 15 | 16 | /// get articles from api endpoint 17 | /// return Failure if catch error or status code is not 200 18 | /// return decoded data as Map if status code is 200 19 | Future>> getArticles() async { 20 | try { 21 | final articles = await client.getTopHeadlines(env['API_KEY']); 22 | return Right(articles); 23 | } on DioError catch (error) { 24 | print(error.type); 25 | print(error.message); 26 | return Left(Failure(error.message)); 27 | } on Exception catch (_) { 28 | return const Left(Failure(ERROR_MSG)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean architecture flutter app 2 | 3 | A clean architecture news app, made with [Getx](https://pub.dev/packages/get), [GetX CLI](https://pub.dev/packages/get_cli), [NewsAPI](https://newsapi.org/) and Flutter sdk. 4 | 5 | The main goal is to build readble, maintainable, testable, and high-quality flutter app using test-driven-design styled architecture (Inspired by [Reso Coder](https://resocoder.com/)). 6 | 7 | You can learn how to implement the architecture here: [TDD Clean Architecture for Flutter](https://github.com/ResoCoder/flutter-tdd-clean-architecture-course) 8 | 9 | ## Screenshots 10 | 11 | 12 | 13 | 14 | ## Technologies 15 | 16 | ### Architecture 17 | - **Test-driven-design** 18 | - **MVC (generated by GetX CLI)** 19 | 20 | ### Front-end 21 | - **Flutter SDK** 22 | - **GetX (navigation service, dependencies manager, ui components)** 23 | - **Bloc / Cubit / GetX (state managment)** 24 | 25 | ### Back-end 26 | - **SQLite** 27 | - **Sembast** 28 | - **Hive** 29 | - **NewsAPI** 30 | 31 | # Author 🙋 32 | 33 | - **Omer Gamliel** - [LinkedIn](https://www.linkedin.com/in/omer-gamliel-6a813a188/) 34 | -------------------------------------------------------------------------------- /lib/app/data/api/api.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'api.dart'; 4 | 5 | // ************************************************************************** 6 | // RetrofitGenerator 7 | // ************************************************************************** 8 | 9 | class _RestClient implements RestClient { 10 | _RestClient(this._dio, {this.baseUrl}) { 11 | ArgumentError.checkNotNull(_dio, '_dio'); 12 | baseUrl ??= 'https://newsapi.org/v2/'; 13 | } 14 | 15 | final Dio _dio; 16 | 17 | String baseUrl; 18 | 19 | @override 20 | Future> getTopHeadlines(apikey) async { 21 | ArgumentError.checkNotNull(apikey, 'apikey'); 22 | const _extra = {}; 23 | final queryParameters = {}; 24 | final _data = {}; 25 | final _result = await _dio.request>( 26 | '/top-headlines?language=en&pageSize=100&apiKey=$apikey', 27 | queryParameters: queryParameters, 28 | options: RequestOptions( 29 | method: 'GET', 30 | headers: {}, 31 | extra: _extra, 32 | baseUrl: baseUrl), 33 | data: _data); 34 | var value = _result.data 35 | .map((dynamic i) => ArticleModel.fromJson(i as Map)) 36 | .toList(); 37 | return value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/di/injector.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'injector.dart'; 4 | 5 | // ************************************************************************** 6 | // KiwiInjectorGenerator 7 | // ************************************************************************** 8 | 9 | class _$Injector extends Injector { 10 | @override 11 | void _configureCore() { 12 | final KiwiContainer container = KiwiContainer(); 13 | container.registerSingleton((c) => Connectivity()); 14 | container.registerSingleton( 15 | (c) => NetworkInfo(connectivity: c())); 16 | } 17 | 18 | @override 19 | void _configureArticlesFeatureModuleFactories() { 20 | final KiwiContainer container = KiwiContainer(); 21 | container.registerFactory( 22 | (c) => ArticlesRemoteDatasource(client: c())); 23 | container.registerFactory( 24 | (c) => ArticlesLocalDatasourceHiveImpl()); 25 | container.registerFactory((c) => GetLocalArticles(c())); 26 | container 27 | .registerFactory((c) => GetRemoteArticles(c())); 28 | container.registerFactory((c) => ArticlesRepositoryImpl( 29 | localDataSource: c(), 30 | remoteDataSource: c())); 31 | container.registerFactory((c) => ArticlesCubit( 32 | c(), c(), c())); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/app/data/datasources/local/hive/article.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'article.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class ArticleAdapter extends TypeAdapter
{ 10 | @override 11 | final int typeId = 0; 12 | 13 | @override 14 | Article 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 Article( 20 | title: fields[0] as String, 21 | content: fields[1] as String, 22 | publishedAt: fields[2] as String, 23 | url: fields[3] as String, 24 | urlToImage: fields[4] as String, 25 | ); 26 | } 27 | 28 | @override 29 | void write(BinaryWriter writer, Article obj) { 30 | writer 31 | ..writeByte(5) 32 | ..writeByte(0) 33 | ..write(obj.title) 34 | ..writeByte(1) 35 | ..write(obj.content) 36 | ..writeByte(2) 37 | ..write(obj.publishedAt) 38 | ..writeByte(3) 39 | ..write(obj.url) 40 | ..writeByte(4) 41 | ..write(obj.urlToImage); 42 | } 43 | 44 | @override 45 | int get hashCode => typeId.hashCode; 46 | 47 | @override 48 | bool operator ==(Object other) => 49 | identical(this, other) || 50 | other is ArticleAdapter && 51 | runtimeType == other.runtimeType && 52 | typeId == other.typeId; 53 | } 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | getx_hacker_news_api 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | getx_hacker_news_api 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | UIStatusBarHidden 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/app/data/repositories/articles_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../datasources/local/articles_local_datasource.dart'; 5 | import '../datasources/remote/articles_remote_datasource.dart'; 6 | 7 | import '../../domain/entities/article.dart'; 8 | import '../../domain/repositories/articles_repository.dart'; 9 | 10 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 11 | 12 | class ArticlesRepositoryImpl implements ArticlesRepository { 13 | ArticlesRepositoryImpl( 14 | {@required this.localDataSource, @required this.remoteDataSource}); 15 | // local data source 16 | final ArticlesLocalDatasource localDataSource; 17 | // remote data source 18 | final ArticlesRemoteDatasource remoteDataSource; 19 | 20 | /// return either failure or list of articles 21 | @override 22 | Future>> getRemoteArticles() async { 23 | try { 24 | final response = await remoteDataSource.getArticles(); 25 | return response.fold((failure) => Left(failure), (articles) async { 26 | if (articles != null && articles.isNotEmpty) { 27 | await localDataSource.insertArticles(articles); 28 | return Right(articles); 29 | } 30 | return const Left(Failure('Can not find articles right now')); 31 | }); 32 | } on Exception catch (_) { 33 | return const Left(Failure('Something went wrong')); 34 | } 35 | } 36 | 37 | /// return either failure or list of articles from saved local database 38 | @override 39 | Future>> getLocalArticles() async { 40 | final articles = await localDataSource.getArticles(); 41 | if (articles == null || articles.isEmpty) { 42 | return const Left(Failure('No internet connection')); 43 | } 44 | return Right(articles); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/data/datasources/articles_remote_datasource_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | 6 | import 'package:getx_hacker_news_api/app/data/api/api.dart'; 7 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 8 | import 'package:getx_hacker_news_api/app/data/datasources/remote/articles_remote_datasource.dart'; 9 | 10 | class MockRestClient extends Mock implements RestClient {} 11 | 12 | void main() { 13 | RestClient client; 14 | ArticlesRemoteDatasource remoteDatasource; 15 | setUp(() { 16 | client = MockRestClient(); 17 | remoteDatasource = ArticlesRemoteDatasource(client: client); 18 | }); 19 | 20 | final articles = [ 21 | ArticleModel( 22 | title: 'test', 23 | content: 'test', 24 | publishedAt: DateTime.now(), 25 | url: 'url', 26 | urlToImage: 'url') 27 | ]; 28 | 29 | test('should return articles data when the call to rest client is successful', 30 | () async { 31 | // arrange 32 | when(client.getTopHeadlines(any)) 33 | .thenAnswer((realInvocation) => Future.value(articles)); 34 | 35 | // act 36 | final result = await remoteDatasource.getArticles(); 37 | 38 | // assert 39 | expect(result, Right(articles)); 40 | verify(client.getTopHeadlines(any)); 41 | verifyNoMoreInteractions(client); 42 | }); 43 | 44 | test('should return failure when the call to rest client ends with Exception', 45 | () async { 46 | // arrange 47 | when(client.getTopHeadlines(any)).thenThrow(Exception()); 48 | 49 | // act 50 | final result = await remoteDatasource.getArticles(); 51 | 52 | // assert 53 | expect(result, isA()); 54 | verify(client.getTopHeadlines(any)); 55 | verifyNoMoreInteractions(client); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/domain/usecases/get_local_articles_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | 6 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 7 | 8 | import 'package:getx_hacker_news_api/app/domain/repositories/articles_repository.dart'; 9 | import 'package:getx_hacker_news_api/app/domain/usecases/get_local_articles.dart'; 10 | 11 | import '../../test_helper.dart'; 12 | 13 | class ArticlesRepositoryMock extends Mock implements ArticlesRepository {} 14 | 15 | void main() { 16 | GetLocalArticles usecase; 17 | ArticlesRepository articlesRepositoryMock; 18 | 19 | setUp(() { 20 | articlesRepositoryMock = ArticlesRepositoryMock(); 21 | usecase = GetLocalArticles(articlesRepositoryMock); 22 | }); 23 | 24 | test('should get local articles from the repository', () async { 25 | // arrange 26 | when(articlesRepositoryMock.getLocalArticles()) 27 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 28 | 29 | // act 30 | final result = await usecase.call(NoParams()); 31 | 32 | // assert 33 | expect(result, Right(articles)); 34 | verify(articlesRepositoryMock.getLocalArticles()); 35 | verifyNoMoreInteractions(articlesRepositoryMock); 36 | }); 37 | 38 | test('should get failure from the repository', () async { 39 | const failure = Failure('something went wrong'); 40 | // arrange 41 | when(articlesRepositoryMock.getLocalArticles()) 42 | .thenAnswer((realInvocation) => Future.value(const Left(failure))); 43 | 44 | // act 45 | final result = await usecase.call(NoParams()); 46 | 47 | // assert 48 | expect(result, const Left(failure)); 49 | verify(articlesRepositoryMock.getLocalArticles()); 50 | verifyNoMoreInteractions(articlesRepositoryMock); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/domain/usecases/get_remote_articles_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | 6 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 7 | 8 | import 'package:getx_hacker_news_api/app/domain/usecases/get_remote_articles.dart'; 9 | import 'package:getx_hacker_news_api/app/domain/repositories/articles_repository.dart'; 10 | 11 | import '../../test_helper.dart'; 12 | 13 | class ArticlesRepositoryMock extends Mock implements ArticlesRepository {} 14 | 15 | void main() { 16 | GetRemoteArticles usecase; 17 | ArticlesRepository articlesRepositoryMock; 18 | 19 | setUp(() { 20 | articlesRepositoryMock = ArticlesRepositoryMock(); 21 | usecase = GetRemoteArticles(articlesRepositoryMock); 22 | }); 23 | 24 | test('should get remote articles from the repository', () async { 25 | // arrange 26 | when(articlesRepositoryMock.getRemoteArticles()) 27 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 28 | 29 | // act 30 | final result = await usecase.call(NoParams()); 31 | 32 | // assert 33 | expect(result, Right(articles)); 34 | verify(articlesRepositoryMock.getRemoteArticles()); 35 | verifyNoMoreInteractions(ArticlesRepositoryMock()); 36 | }); 37 | 38 | test('should get failure from the repository', () async { 39 | const failure = Failure('something went wrong'); 40 | 41 | // arrange 42 | when(articlesRepositoryMock.getRemoteArticles()) 43 | .thenAnswer((realInvocation) => Future.value(const Left(failure))); 44 | 45 | // act 46 | final result = await usecase.call(NoParams()); 47 | 48 | // assert 49 | expect(result, const Left(failure)); 50 | verify(articlesRepositoryMock.getRemoteArticles()); 51 | verifyNoMoreInteractions(ArticlesRepositoryMock()); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /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 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.omergamliel.getx_hacker_news_api" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/controller/cubit_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity/connectivity.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | import '../../../domain/entities/article.dart'; 9 | import '../../../domain/usecases/get_local_articles.dart'; 10 | import '../../../domain/usecases/get_remote_articles.dart'; 11 | 12 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 13 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 14 | import 'package:getx_hacker_news_api/app/core/network/network_info.dart'; 15 | 16 | import 'state.dart'; 17 | 18 | class ArticlesCubit extends Cubit { 19 | // construct bloc with initial state 20 | ArticlesCubit(this.network, this.getRemoteArticles, this.getLocalArticles) 21 | : super(const Initial()); 22 | 23 | // get dependencies 24 | final NetworkInfoI network; 25 | final GetRemoteArticles getRemoteArticles; 26 | final GetLocalArticles getLocalArticles; 27 | 28 | Future getArticles() async { 29 | emit(const Loading()); 30 | final connectivity = await network.isConnected(); 31 | Either> failureOrArticles; 32 | if (connectivity) { 33 | failureOrArticles = await getRemoteArticles.call(NoParams()); 34 | } else { 35 | failureOrArticles = await getLocalArticles.call(NoParams()); 36 | waitForConnectivityAndCallGetArticles(); 37 | Get.snackbar('Offline mode', 'There is no internet connection', 38 | snackPosition: SnackPosition.BOTTOM); 39 | } 40 | emit(failureOrArticles.fold( 41 | (failure) => Error(failure), (articles) => Success(articles))); 42 | } 43 | 44 | void waitForConnectivityAndCallGetArticles() { 45 | StreamSubscription subscription; 46 | subscription = network.onConnectivityChanged.listen((event) { 47 | if (event != ConnectivityResult.none) { 48 | subscription.cancel(); 49 | getArticles(); 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/app/presentation/home_get/home_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | 5 | import 'home_controller.dart'; 6 | 7 | import '../../core/widgets/index.dart' as core_widgets; 8 | import 'widgets/widgets.dart'; 9 | 10 | class HomeViewGetX extends GetView { 11 | PreferredSizeWidget appBar() { 12 | return AppBar( 13 | elevation: 8.0, 14 | backgroundColor: Colors.yellow, 15 | title: Row( 16 | mainAxisSize: MainAxisSize.min, 17 | children: const [ 18 | Icon( 19 | Icons.adb, 20 | color: Colors.black, 21 | ), 22 | SizedBox( 23 | width: 6.0, 24 | ), 25 | Text( 26 | 'NewsReader', 27 | style: TextStyle( 28 | color: Colors.black, 29 | fontWeight: FontWeight.bold, 30 | letterSpacing: 1.0, 31 | ), 32 | ), 33 | ], 34 | ).paddingAll(4.0), 35 | actions: [ 36 | ConnectivityIcon(controller: controller).paddingOnly(right: 10.0) 37 | ], 38 | ); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return SafeArea( 44 | top: false, 45 | child: Scaffold( 46 | backgroundColor: Colors.yellow, 47 | appBar: appBar(), 48 | body: Obx(() { 49 | switch (controller.viewState.value) { 50 | case ViewState.busy: 51 | return core_widgets.LoadingWidget(); 52 | break; 53 | case ViewState.error: 54 | return const core_widgets.ErrorWidget(); 55 | case ViewState.data: 56 | return ArticlesView( 57 | articles: controller.articles, 58 | fetch: controller.remoteFetch, 59 | ); 60 | default: 61 | return core_widgets.LoadingWidget(); 62 | } 63 | }), 64 | floatingActionButton: FAB( 65 | callback: controller.remoteFetch, 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/view/home_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:getx_hacker_news_api/di/injector.dart'; 6 | 7 | import '../../../core/widgets/index.dart' as core_widgets; 8 | 9 | import '../controller/index.dart'; 10 | import '../widgets/widgets.dart'; 11 | 12 | class HomeViewCubit extends StatelessWidget { 13 | PreferredSizeWidget appBar() { 14 | return AppBar( 15 | elevation: 8.0, 16 | backgroundColor: Colors.yellow, 17 | title: Row( 18 | mainAxisSize: MainAxisSize.min, 19 | children: const [ 20 | Icon( 21 | Icons.adb, 22 | color: Colors.black, 23 | ), 24 | SizedBox( 25 | width: 6.0, 26 | ), 27 | Text( 28 | 'NewsReader', 29 | style: TextStyle( 30 | color: Colors.black, 31 | fontWeight: FontWeight.bold, 32 | letterSpacing: 1.0, 33 | ), 34 | ), 35 | ], 36 | ).paddingAll(4.0), 37 | ); 38 | } 39 | 40 | Widget buildBody(BuildContext context) { 41 | return BlocBuilder( 42 | builder: (context, state) { 43 | return state.when( 44 | initial: () => core_widgets.LoadingWidget(), 45 | loading: () => core_widgets.LoadingWidget(), 46 | success: (articles) => ArticlesView(articles: articles), 47 | error: (failure) => core_widgets.ErrorWidget(failure.message), 48 | ); 49 | }, 50 | ); 51 | } 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return SafeArea( 56 | top: false, 57 | child: BlocProvider( 58 | create: (_) => Injector.resolve()..getArticles(), 59 | child: Scaffold( 60 | backgroundColor: Colors.yellow, 61 | appBar: appBar(), 62 | body: buildBody(context), 63 | floatingActionButton: FAB(), 64 | ), 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/view/home_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:getx_hacker_news_api/di/injector.dart'; 6 | import '../controller/index.dart'; 7 | 8 | import '../../../core/widgets/index.dart' as core_widgets; 9 | 10 | import '../widgets/widgets.dart'; 11 | 12 | class HomeViewBloc extends StatelessWidget { 13 | PreferredSizeWidget appBar() { 14 | return AppBar( 15 | elevation: 8.0, 16 | backgroundColor: Colors.yellow, 17 | title: Row( 18 | mainAxisSize: MainAxisSize.min, 19 | children: const [ 20 | Icon( 21 | Icons.adb, 22 | color: Colors.black, 23 | ), 24 | SizedBox( 25 | width: 6.0, 26 | ), 27 | Text( 28 | 'NewsReader', 29 | style: TextStyle( 30 | color: Colors.black, 31 | fontWeight: FontWeight.bold, 32 | letterSpacing: 1.0, 33 | ), 34 | ), 35 | ], 36 | ).paddingAll(4.0), 37 | ); 38 | } 39 | 40 | Widget buildBody(BuildContext context) { 41 | return BlocBuilder( 42 | builder: (context, state) { 43 | return state.when( 44 | initial: () => core_widgets.LoadingWidget(), 45 | loading: () => core_widgets.LoadingWidget(), 46 | success: (articles) => ArticlesView(articles: articles), 47 | error: (failure) => core_widgets.ErrorWidget(failure.message), 48 | ); 49 | }, 50 | ); 51 | } 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return SafeArea( 56 | top: false, 57 | child: BlocProvider( 58 | create: (_) => Injector.resolve()..add(const GetData()), 59 | child: Scaffold( 60 | backgroundColor: Colors.yellow, 61 | appBar: appBar(), 62 | body: buildBody(context), 63 | floatingActionButton: FAB(), 64 | ), 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: getx_hacker_news_api 2 | description: NewsAPI app with GetX. 3 | 4 | publish_to: 'none' 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.7.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | # api 15 | retrofit: any 16 | logger: any 17 | # network info 18 | connectivity: ^0.4.9+3 19 | # functional programming 20 | dartz: ^0.9.2 21 | # value equality 22 | equatable: ^1.2.5 23 | # json 24 | json_annotation: ^3.1.0 25 | # freezed 26 | freezed_annotation: ^0.12.0 27 | # meta 28 | meta: ^1.2.3 29 | # bloc state managment 30 | flutter_bloc: ^6.0.6 31 | # GetX mico-framework 32 | get: ^3.24.0 33 | # network 34 | http: ^0.12.2 35 | # date formatter 36 | intl: ^0.16.1 37 | # improve code 38 | lint: ^1.3.0 39 | # device native folders 40 | path_provider: ^1.6.18 41 | # sql db (local storage) 42 | sqflite: ^1.3.1+1 43 | # no sql db (local storage) 44 | sembast: ^2.4.7+7 45 | # no sql db (local storage) 46 | hive: ^1.4.4+1 47 | hive_flutter: ^0.3.1 48 | # url helper 49 | url_launcher: ^5.7.2 50 | # IoC 51 | kiwi: ^2.1.1 52 | # env variables 53 | flutter_dotenv: ^3.1.0 54 | 55 | dev_dependencies: 56 | # tests 57 | flutter_driver: 58 | sdk: flutter 59 | flutter_test: 60 | sdk: flutter 61 | mockito: 62 | bloc_test: ^7.1.0 63 | test: any 64 | 65 | # native icons & splash 66 | flutter_launcher_icons: ^0.8.1 67 | flutter_native_splash: ^0.1.9 68 | 69 | # code generators 70 | build_runner: ^1.10.3 71 | freezed: ^0.12.2 72 | json_serializable: ^3.5.0 73 | retrofit_generator: any 74 | hive_generator: ^0.8.1 75 | kiwi_generator: ^2.1.1 76 | 77 | # flutter icons setup 78 | flutter_icons: 79 | android: true 80 | ios: true 81 | image_path: "assets/images/icon.png" 82 | 83 | # flutter splash screen setup 84 | flutter_native_splash: 85 | image: assets/images/splash_icon.png 86 | color: "#FFEB3B" 87 | android_disable_fullscreen: true 88 | 89 | flutter: 90 | uses-material-design: true 91 | assets: 92 | - assets/images/loading.jpg 93 | - assets/images/placeHolder.jpg 94 | -------------------------------------------------------------------------------- /lib/di/injector.dart: -------------------------------------------------------------------------------- 1 | import 'package:kiwi/kiwi.dart'; 2 | 3 | import 'package:connectivity/connectivity.dart'; 4 | import 'package:dio/dio.dart'; 5 | 6 | import '../app/data/api/api.dart'; 7 | import '../app/data/datasources/local/articles_local_datasource.dart'; 8 | import '../app/data/datasources/remote/articles_remote_datasource.dart'; 9 | 10 | import '../app/data/datasources/local/hive/articles_local_datasource_hive.dart'; 11 | 12 | import '../app/data/repositories/articles_repository_impl.dart'; 13 | import '../app/domain/repositories/articles_repository.dart'; 14 | 15 | import '../app/domain/usecases/get_local_articles.dart'; 16 | import '../app/domain/usecases/get_remote_articles.dart'; 17 | 18 | import '../app/core/network/network_info.dart'; 19 | 20 | import '../app/presentation/home_cubit/controller/index.dart'; 21 | 22 | part 'injector.g.dart'; 23 | 24 | abstract class Injector { 25 | static KiwiContainer container; 26 | 27 | static void setup() { 28 | container = KiwiContainer(); 29 | _$Injector()._configure(); 30 | } 31 | 32 | static final resolve = container.resolve; 33 | 34 | void _configure() { 35 | _configureCore(); 36 | _configureArticlesFeatureModule(); 37 | } 38 | 39 | // Core module 40 | @Register.singleton(Connectivity) 41 | @Register.singleton(NetworkInfoI, from: NetworkInfo) 42 | void _configureCore(); 43 | 44 | // Articles Feature module 45 | void _configureArticlesFeatureModule() { 46 | _configureArticlesFeatureModuleInstances(); 47 | _configureArticlesFeatureModuleFactories(); 48 | } 49 | 50 | // Articles Feature module instances 51 | void _configureArticlesFeatureModuleInstances() { 52 | container.registerInstance( 53 | RestClient(Dio(BaseOptions(contentType: "application/json")))); 54 | } 55 | 56 | // Articles Feature module factories 57 | @Register.factory(ArticlesRemoteDatasource) 58 | @Register.factory(ArticlesLocalDatasource, 59 | from: ArticlesLocalDatasourceHiveImpl) 60 | @Register.factory(GetLocalArticles) 61 | @Register.factory(GetRemoteArticles) 62 | @Register.factory(ArticlesRepository, from: ArticlesRepositoryImpl) 63 | @Register.factory(ArticlesCubit) 64 | void _configureArticlesFeatureModuleFactories(); 65 | } 66 | -------------------------------------------------------------------------------- /lib/app/presentation/home_get/widgets/articles_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | import '../../../domain/entities/article.dart'; 7 | 8 | import '../../../core/widgets/index.dart' as core_widgets; 9 | 10 | import '../../../core/utils/launcher.dart'; 11 | 12 | class ArticlesView extends StatelessWidget { 13 | final List
articles; 14 | final Function fetch; 15 | const ArticlesView({this.articles, this.fetch}); 16 | 17 | Widget buildArticleTile(Article article) { 18 | final formattedTime = 19 | DateFormat('dd MMM - HH:mm').format(article.publishedAt); 20 | return Row( 21 | children: [ 22 | core_widgets.ImageHandlerWidget(urlToImage: article.urlToImage), 23 | const SizedBox( 24 | width: 16.0, 25 | ), 26 | Flexible( 27 | child: Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | Text(formattedTime), 31 | Text( 32 | article.title, 33 | overflow: TextOverflow.ellipsis, 34 | style: 35 | const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), 36 | ), 37 | Text( 38 | article.content, 39 | maxLines: 2, 40 | overflow: TextOverflow.ellipsis, 41 | ), 42 | ], 43 | ), 44 | ) 45 | ], 46 | ).paddingAll(5.0); 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | if (articles.isEmpty) { 52 | return const core_widgets.ErrorWidget( 53 | 'Can not find articles :(', 54 | ); 55 | } 56 | 57 | return RefreshIndicator( 58 | onRefresh: () async { 59 | await fetch(); 60 | }, 61 | child: ListView.builder( 62 | itemCount: articles.length, 63 | itemBuilder: (context, index) { 64 | final article = articles[index]; 65 | return GestureDetector( 66 | onTap: () => launch(article.url), 67 | child: Container( 68 | height: 100, 69 | child: buildArticleTile(article), 70 | ), 71 | ); 72 | }, 73 | ), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/controller/bloc_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity/connectivity.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | import '../../../domain/entities/article.dart'; 9 | import '../../../domain/usecases/get_local_articles.dart'; 10 | import '../../../domain/usecases/get_remote_articles.dart'; 11 | 12 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 13 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 14 | import 'package:getx_hacker_news_api/app/core/network/network_info.dart'; 15 | 16 | import 'event.dart'; 17 | import 'state.dart'; 18 | 19 | class ArticlesBloc extends Bloc { 20 | // construct bloc with initial state 21 | ArticlesBloc(this.network, this.getRemoteArticles, this.getLocalArticles) 22 | : super(const Initial()); 23 | 24 | // dependencies 25 | final NetworkInfoI network; 26 | final GetRemoteArticles getRemoteArticles; 27 | final GetLocalArticles getLocalArticles; 28 | 29 | @override 30 | Stream mapEventToState(ArticlesEvent event) async* { 31 | // handle GetData event 32 | if (event is GetData) { 33 | yield const Loading(); 34 | // check for network connection 35 | final connectivity = await network.isConnected(); 36 | Either> failureOrArticles; 37 | if (connectivity) { 38 | failureOrArticles = await getRemoteArticles.call(NoParams()); 39 | } else { 40 | failureOrArticles = await getLocalArticles.call(NoParams()); 41 | waitForConnectivityAndNotifyGetDataEvent(); 42 | Get.snackbar('Offline mode', 'There is no internet connection', 43 | snackPosition: SnackPosition.BOTTOM); 44 | } 45 | // yield new ArticlesState 46 | yield failureOrArticles.fold( 47 | (failure) => Error(failure), (articles) => Success(articles)); 48 | } 49 | } 50 | 51 | void waitForConnectivityAndNotifyGetDataEvent() { 52 | StreamSubscription subscription; 53 | subscription = network.onConnectivityChanged.listen((event) { 54 | if (event != ConnectivityResult.none) { 55 | subscription.cancel(); 56 | add(const GetData()); 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 15 | 19 | 23 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/widgets/articles_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | import '../../../domain/entities/article.dart'; 7 | 8 | import '../../../core/widgets/index.dart' as core_widgets; 9 | import '../../../core/utils/launcher.dart'; 10 | 11 | import 'package:flutter_bloc/flutter_bloc.dart'; 12 | import '../controller/index.dart'; 13 | 14 | class ArticlesView extends StatelessWidget { 15 | final List
articles; 16 | const ArticlesView({@required this.articles}); 17 | 18 | Widget buildArticleTile(Article article) { 19 | final formattedTime = 20 | DateFormat('dd MMM - HH:mm').format(article.publishedAt); 21 | return Row( 22 | children: [ 23 | core_widgets.ImageHandlerWidget( 24 | urlToImage: article.urlToImage, 25 | ), 26 | const SizedBox( 27 | width: 16.0, 28 | ), 29 | Flexible( 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Text(formattedTime ?? ''), 34 | Text( 35 | article.title ?? '', 36 | overflow: TextOverflow.ellipsis, 37 | style: 38 | const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), 39 | ), 40 | Text( 41 | article.content ?? '', 42 | maxLines: 2, 43 | overflow: TextOverflow.ellipsis, 44 | ), 45 | ], 46 | ), 47 | ) 48 | ], 49 | ).paddingAll(5.0); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | if (articles.isEmpty) { 55 | return const core_widgets.ErrorWidget( 56 | 'Can not find articles :(', 57 | ); 58 | } 59 | 60 | return RefreshIndicator( 61 | onRefresh: () async { 62 | BlocProvider.of(context).add(const GetData()); 63 | }, 64 | child: ListView.builder( 65 | itemCount: articles.length, 66 | itemBuilder: (context, index) { 67 | final article = articles[index]; 68 | return GestureDetector( 69 | onTap: () => launch(article.url), 70 | child: Container( 71 | height: 100, 72 | child: buildArticleTile(article), 73 | ), 74 | ); 75 | }, 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/widgets/articles_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | import '../../../domain/entities/article.dart'; 7 | 8 | import '../../../core/widgets/index.dart' as core_widgets; 9 | import '../../../core/utils/launcher.dart'; 10 | 11 | import 'package:flutter_bloc/flutter_bloc.dart'; 12 | import '../controller/index.dart'; 13 | 14 | class ArticlesView extends StatelessWidget { 15 | final List
articles; 16 | const ArticlesView({@required this.articles}); 17 | 18 | Widget buildArticleTile(Article article) { 19 | final formattedTime = 20 | DateFormat('dd MMM - HH:mm').format(article.publishedAt); 21 | return Row( 22 | children: [ 23 | core_widgets.ImageHandlerWidget( 24 | urlToImage: article.urlToImage, 25 | ), 26 | const SizedBox( 27 | width: 16.0, 28 | ), 29 | Flexible( 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Text(formattedTime ?? ''), 34 | Text( 35 | article.title ?? '', 36 | overflow: TextOverflow.ellipsis, 37 | style: 38 | const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), 39 | ), 40 | Text( 41 | article.content ?? '', 42 | maxLines: 2, 43 | overflow: TextOverflow.ellipsis, 44 | ), 45 | ], 46 | ), 47 | ) 48 | ], 49 | ).paddingAll(5.0); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | if (articles.isEmpty) { 55 | return const core_widgets.ErrorWidget( 56 | 'Can not find articles :(', 57 | ); 58 | } 59 | 60 | return RefreshIndicator( 61 | onRefresh: () async { 62 | BlocProvider.of(context).getArticles(); 63 | }, 64 | child: ListView.builder( 65 | key: const ValueKey('articles_list'), 66 | itemCount: articles.length, 67 | itemBuilder: (context, index) { 68 | final article = articles[index]; 69 | return GestureDetector( 70 | onTap: () => launch(article.url), 71 | child: Container( 72 | key: ValueKey('article$index'), 73 | height: 100, 74 | child: buildArticleTile(article), 75 | ), 76 | ); 77 | }, 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test_driver/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart'; 3 | 4 | // Imports the Flutter Driver API. 5 | import 'package:flutter_driver/flutter_driver.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | const path = r'C:\Users\omerg\AppData\Local\Android\Sdk'; 9 | 10 | Future grantPermissions() async { 11 | final adbPath = join( 12 | path, 13 | 'platform-tools', 14 | Platform.isWindows ? 'adb.exe' : 'adb', 15 | ); 16 | await Process.run(adbPath, [ 17 | 'shell', 18 | 'pm', 19 | 'grant', 20 | 'com.whatsappclone.WhatAppClone', 21 | 'android.permission.INTERNET' 22 | ]); 23 | } 24 | 25 | void main() { 26 | group('NewsAPI App Test -', () { 27 | FlutterDriver driver; 28 | 29 | // Connect to the Flutter driver before running any tests. 30 | setUpAll(() async { 31 | // grant device permission 32 | await grantPermissions(); 33 | driver = await FlutterDriver.connect(); 34 | }); 35 | 36 | // Close the connection to the driver after the tests have completed. 37 | tearDownAll(() async { 38 | if (driver != null) { 39 | driver.close(); 40 | } 41 | }); 42 | 43 | test('verifies the articles view list contains a specific article', 44 | () async { 45 | // Create three SerializableFinders and use these to locate specific 46 | // widgets displayed by the app. The names provided to the byValueKey 47 | // method correspond to the Keys provided to the widgets in home_view. 48 | 49 | // Articles listview 50 | final listFinder = find.byValueKey('articles_list'); 51 | // Specifie items in the listview 52 | final firstItemFinder = find.byValueKey('article20'); 53 | final secondItemFinder = find.byValueKey('article10'); 54 | 55 | // 'Refresh' FAB 56 | final refresh = find.byValueKey('FAB'); 57 | 58 | // Scroll through the list 59 | await driver.scrollUntilVisible( 60 | listFinder, 61 | // Until finding this item 62 | firstItemFinder, 63 | // To scroll down the list, provide a negative value to dyScroll. 64 | // Ensure that this value is a small enough increment to 65 | // scroll the item into view without potentially scrolling past it. 66 | // 67 | // To scroll through horizontal lists, provide a dxScroll 68 | // property instead. 69 | dyScroll: -300.0, 70 | ); 71 | 72 | // tap the 'refresh' FAB 73 | await driver.tap(refresh); 74 | 75 | // Scroll through the list 76 | await driver.scrollUntilVisible( 77 | listFinder, 78 | // Until finding this item 79 | secondItemFinder, 80 | // To scroll down the list, provide a negative value to dyScroll. 81 | // Ensure that this value is a small enough increment to 82 | // scroll the item into view without potentially scrolling past it. 83 | dyScroll: -300.0, 84 | ); 85 | // should launch article from the device browser 86 | await driver.tap(secondItemFinder); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /lib/app/data/datasources/local/sembast/articles_local_datasource_sembast.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 4 | 5 | import '../articles_local_datasource.dart'; 6 | 7 | import 'package:path/path.dart'; 8 | import 'package:path_provider/path_provider.dart'; 9 | import 'package:sembast/sembast.dart'; 10 | import 'package:sembast/sembast_io.dart'; 11 | 12 | class ArticlesLocalDatasourceSembastImpl implements ArticlesLocalDatasource { 13 | // Name constants 14 | final _kDbFileName = 'sembast_articles.db'; 15 | final _kArticlesStoreName = 'articles_store'; 16 | 17 | // Sembast database object 18 | Database _database; 19 | 20 | // A Store with int keys and Map values. 21 | // This Store acts like a persistent map, values of which are ArticleModel objects converted to Map 22 | StoreRef> _articlesStore; 23 | 24 | // initialize database 25 | @override 26 | Future initDb() async { 27 | try { 28 | // Get a platform-specific directory where persistent app data can be stored 29 | final appDocumentDir = await getApplicationDocumentsDirectory(); 30 | final dbPath = join(appDocumentDir.path, _kDbFileName); 31 | _database = await databaseFactoryIo.openDatabase(dbPath); 32 | _articlesStore = intMapStoreFactory.store(_kArticlesStoreName); 33 | return true; 34 | } catch (e) { 35 | return false; 36 | } 37 | } 38 | 39 | /// delete database 40 | @override 41 | Future deleteDb() async { 42 | try { 43 | // Get a platform-specific directory where persistent app data can be stored 44 | final appDocumentDir = await getApplicationDocumentsDirectory(); 45 | final dbPath = join(appDocumentDir.path, _kDbFileName); 46 | await databaseFactoryIo.deleteDatabase(dbPath); 47 | return true; 48 | } catch (e) { 49 | return false; 50 | } 51 | } 52 | 53 | /// save articles in database 54 | @override 55 | Future insertArticles(List articles) async { 56 | try { 57 | // delete all articles from store 58 | await _articlesStore.delete(_database); 59 | 60 | // insert all articles to store 61 | for (final article in articles) { 62 | await _articlesStore.add(_database, article.toJson()); 63 | } 64 | return true; 65 | } catch (e) { 66 | return false; 67 | } 68 | } 69 | 70 | /// return all stored articles 71 | @override 72 | Future> getArticles() async { 73 | try { 74 | final recordSnapshots = await _articlesStore.find(_database); 75 | return recordSnapshots 76 | .map((snapshot) => ArticleModel.fromJson(snapshot.value)) 77 | .toList(); 78 | } catch (e) { 79 | return null; 80 | } 81 | } 82 | 83 | /// delete all articles records 84 | @override 85 | Future deleteAllArticles() async { 86 | try { 87 | await _articlesStore.delete(_database); 88 | return true; 89 | } catch (e) { 90 | return false; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/app/data/datasources/local/hive/articles_local_datasource_hive.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart' as foundation; 3 | import 'package:hive/hive.dart'; 4 | 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | import '../articles_local_datasource.dart'; 8 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 9 | import 'article.dart'; 10 | 11 | class ArticlesLocalDatasourceHiveImpl implements ArticlesLocalDatasource { 12 | final _kArticlesBoxName = 'articles_box'; 13 | 14 | @override 15 | Future initDb() async { 16 | try { 17 | if (!foundation.kIsWeb) { 18 | final appDocumentDir = await getApplicationDocumentsDirectory(); 19 | Hive.init(appDocumentDir.path); 20 | } 21 | 22 | Hive.registerAdapter(ArticleAdapter()); 23 | await Hive.openBox
(_kArticlesBoxName); 24 | return true; 25 | } on Exception catch (e) { 26 | print(e); 27 | return false; 28 | } 29 | } 30 | 31 | @override 32 | Future deleteDb() async { 33 | // TODO: implement deleteDb 34 | throw UnimplementedError(); 35 | } 36 | 37 | @override 38 | Future> getArticles() async { 39 | // return articles hive box 40 | final articlesBox = Hive.box
(_kArticlesBoxName); 41 | return articlesBox.values.map((e) { 42 | return ArticleModel( 43 | title: e.title, 44 | content: e.content, 45 | publishedAt: DateTime.parse(e.publishedAt), 46 | url: e.url, 47 | urlToImage: e.urlToImage); 48 | }).toList(); 49 | } 50 | 51 | @override 52 | Future insertArticles(List articles) async { 53 | try { 54 | // return articles hive box 55 | final articlesBox = Hive.box
(_kArticlesBoxName); 56 | // clear all enrties from hive box 57 | final deleted = await articlesBox.clear(); 58 | // print deleted entries 59 | print('delete $deleted entries from hive $_kArticlesBoxName box'); 60 | // convert ArticleModel to HiveType Article 61 | final converted = articles 62 | .map((e) => Article( 63 | title: e.title, 64 | content: e.content, 65 | publishedAt: e.publishedAt.toIso8601String(), 66 | url: e.url, 67 | urlToImage: e.urlToImage)) 68 | .toList(); 69 | // insert all articles to hive box 70 | final entries = await articlesBox.addAll(converted); 71 | print(entries); 72 | return true; 73 | } on Exception catch (e) { 74 | print(e); 75 | return false; 76 | } 77 | } 78 | 79 | @override 80 | Future deleteAllArticles() async { 81 | try { 82 | // return articles hive box 83 | final articlesBox = Hive.box
(_kArticlesBoxName); 84 | // clear all enrties from hive box 85 | final deleted = await articlesBox.clear(); 86 | // print deleted entries 87 | print('delete $deleted entries from hive $_kArticlesBoxName box'); 88 | return true; 89 | } on Exception catch (e) { 90 | print(e); 91 | return false; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/presentation/home_bloc/articles_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc_test/bloc_test.dart'; 4 | import 'package:connectivity/connectivity.dart'; 5 | import 'package:dartz/dartz.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 8 | 9 | import 'package:getx_hacker_news_api/app/domain/usecases/get_local_articles.dart'; 10 | import 'package:getx_hacker_news_api/app/domain/usecases/get_remote_articles.dart'; 11 | 12 | import 'package:getx_hacker_news_api/app/core/network/network_info.dart'; 13 | import 'package:getx_hacker_news_api/app/presentation/home_bloc/controller/index.dart'; 14 | import 'package:mockito/mockito.dart'; 15 | 16 | import '../../test_helper.dart'; 17 | 18 | class MockNetworkInfo extends Mock implements NetworkInfoI {} 19 | 20 | class MockGetRemoteArticles extends Mock implements GetRemoteArticles {} 21 | 22 | class MockGetLocalArticles extends Mock implements GetLocalArticles {} 23 | 24 | void main() { 25 | NetworkInfoI networkInfo; 26 | GetRemoteArticles getRemoteArticles; 27 | GetLocalArticles getLocalArticles; 28 | ArticlesBloc articlesBloc; 29 | 30 | setUp(() { 31 | networkInfo = MockNetworkInfo(); 32 | getRemoteArticles = MockGetRemoteArticles(); 33 | getLocalArticles = MockGetLocalArticles(); 34 | articlesBloc = 35 | ArticlesBloc(networkInfo, getRemoteArticles, getLocalArticles); 36 | }); 37 | 38 | group('is online', () { 39 | blocTest( 40 | 'should emits [Loading, Success] when [GetData] event is called succesfuly.', 41 | build: () { 42 | when(networkInfo.isConnected()) 43 | .thenAnswer((realInvocation) => Future.value(true)); 44 | when(getRemoteArticles.call(NoParams())) 45 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 46 | return articlesBloc; 47 | }, 48 | act: (bloc) => bloc.add(const GetData()), 49 | expect: [isA(), Success(articles)], 50 | verify: (_) { 51 | verifyInOrder( 52 | [networkInfo.isConnected(), getRemoteArticles.call(NoParams())]); 53 | verifyNoMoreInteractions(networkInfo); 54 | verifyNoMoreInteractions(getRemoteArticles); 55 | verifyZeroInteractions(getLocalArticles); 56 | }, 57 | ); 58 | }); 59 | 60 | group('is offline', () { 61 | blocTest( 62 | 'should emits [Loading, Success] when [GetData] event is called succesfuly.', 63 | build: () { 64 | when(networkInfo.isConnected()) 65 | .thenAnswer((realInvocation) => Future.value(false)); 66 | when(networkInfo.onConnectivityChanged).thenAnswer( 67 | (realInvocation) => Stream.fromIterable([ConnectivityResult.none])); 68 | when(getLocalArticles.call(NoParams())) 69 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 70 | return articlesBloc; 71 | }, 72 | act: (bloc) => bloc.add(const GetData()), 73 | expect: [isA(), Success(articles)], 74 | verify: (_) { 75 | verifyInOrder([ 76 | networkInfo.isConnected(), 77 | getLocalArticles.call(NoParams()), 78 | networkInfo.onConnectivityChanged 79 | ]); 80 | verifyNoMoreInteractions(networkInfo); 81 | verifyNoMoreInteractions(getLocalArticles); 82 | verifyZeroInteractions(getRemoteArticles); 83 | }, 84 | ); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /test/presentation/home_cubit/articles_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc_test/bloc_test.dart'; 4 | import 'package:connectivity/connectivity.dart'; 5 | import 'package:dartz/dartz.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 8 | 9 | import 'package:getx_hacker_news_api/app/domain/usecases/get_local_articles.dart'; 10 | import 'package:getx_hacker_news_api/app/domain/usecases/get_remote_articles.dart'; 11 | 12 | import 'package:getx_hacker_news_api/app/core/network/network_info.dart'; 13 | import 'package:getx_hacker_news_api/app/presentation/home_cubit/controller/index.dart'; 14 | import 'package:mockito/mockito.dart'; 15 | 16 | import '../../test_helper.dart'; 17 | 18 | class MockNetworkInfo extends Mock implements NetworkInfoI {} 19 | 20 | class MockGetRemoteArticles extends Mock implements GetRemoteArticles {} 21 | 22 | class MockGetLocalArticles extends Mock implements GetLocalArticles {} 23 | 24 | void main() { 25 | NetworkInfoI networkInfo; 26 | GetRemoteArticles getRemoteArticles; 27 | GetLocalArticles getLocalArticles; 28 | ArticlesCubit articlesCubit; 29 | 30 | setUp(() { 31 | networkInfo = MockNetworkInfo(); 32 | getRemoteArticles = MockGetRemoteArticles(); 33 | getLocalArticles = MockGetLocalArticles(); 34 | articlesCubit = 35 | ArticlesCubit(networkInfo, getRemoteArticles, getLocalArticles); 36 | }); 37 | 38 | group('is online', () { 39 | blocTest( 40 | 'should emits [Loading, Success] when cubit.getArticles is called succesfuly.', 41 | build: () { 42 | when(networkInfo.isConnected()) 43 | .thenAnswer((realInvocation) => Future.value(true)); 44 | when(getRemoteArticles.call(NoParams())) 45 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 46 | return articlesCubit; 47 | }, 48 | act: (cubit) => cubit.getArticles(), 49 | expect: [isA(), Success(articles)], 50 | verify: (_) { 51 | verifyInOrder( 52 | [networkInfo.isConnected(), getRemoteArticles.call(NoParams())]); 53 | verifyNoMoreInteractions(networkInfo); 54 | verifyNoMoreInteractions(getRemoteArticles); 55 | verifyZeroInteractions(getLocalArticles); 56 | }, 57 | ); 58 | }); 59 | 60 | group('is offline', () { 61 | blocTest( 62 | 'should emits [Loading, Success] when cubit.getArticles is called succesfuly.', 63 | build: () { 64 | when(networkInfo.isConnected()) 65 | .thenAnswer((realInvocation) => Future.value(false)); 66 | when(networkInfo.onConnectivityChanged).thenAnswer( 67 | (realInvocation) => Stream.fromIterable([ConnectivityResult.none])); 68 | when(getLocalArticles.call(NoParams())) 69 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 70 | return articlesCubit; 71 | }, 72 | act: (cubit) => cubit.getArticles(), 73 | expect: [isA(), Success(articles)], 74 | verify: (_) { 75 | verifyInOrder([ 76 | networkInfo.isConnected(), 77 | getLocalArticles.call(NoParams()), 78 | networkInfo.onConnectivityChanged 79 | ]); 80 | verifyNoMoreInteractions(networkInfo); 81 | verifyNoMoreInteractions(getLocalArticles); 82 | verifyZeroInteractions(getRemoteArticles); 83 | }, 84 | ); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /test/data/repository/articles_repository_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:dartz/dartz.dart'; 4 | 5 | import 'package:getx_hacker_news_api/app/core/errors/failure.dart'; 6 | import 'package:getx_hacker_news_api/app/data/datasources/remote/articles_remote_datasource.dart'; 7 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 8 | import 'package:getx_hacker_news_api/app/data/repositories/articles_repository_impl.dart'; 9 | import 'package:getx_hacker_news_api/app/data/datasources/local/articles_local_datasource.dart'; 10 | 11 | class ArticlesLocalDatasourceMock extends Mock 12 | implements ArticlesLocalDatasource {} 13 | 14 | class ArticlesRemoteDatasourceMock extends Mock 15 | implements ArticlesRemoteDatasource {} 16 | 17 | void main() { 18 | ArticlesLocalDatasource localDatasource; 19 | ArticlesRemoteDatasource remoteDatasource; 20 | ArticlesRepositoryImpl repository; 21 | 22 | setUp(() { 23 | localDatasource = ArticlesLocalDatasourceMock(); 24 | remoteDatasource = ArticlesRemoteDatasourceMock(); 25 | repository = ArticlesRepositoryImpl( 26 | localDataSource: localDatasource, remoteDataSource: remoteDatasource); 27 | }); 28 | 29 | final articles = [ 30 | ArticleModel( 31 | title: 'test', 32 | content: 'test', 33 | publishedAt: DateTime.now(), 34 | url: 'url', 35 | urlToImage: 'url') 36 | ]; 37 | group('remote articles:', () { 38 | test( 39 | 'should return articles data when the call to remoteDataSource is successful, and insert articles to localDataSource', 40 | () async { 41 | // arrange 42 | when(remoteDatasource.getArticles()) 43 | .thenAnswer((realInvocation) => Future.value(Right(articles))); 44 | 45 | // act 46 | final result = await repository.getRemoteArticles(); 47 | 48 | // assert 49 | expect(result, Right(articles)); 50 | verify(remoteDatasource.getArticles()); 51 | verify(localDatasource.insertArticles(articles)); 52 | verifyNoMoreInteractions(remoteDatasource); 53 | verifyNoMoreInteractions(localDatasource); 54 | }); 55 | 56 | test('should return failure when the call to remoteDataSource is failed', 57 | () async { 58 | const failure = Failure('something went wrong'); 59 | 60 | // arrange 61 | when(remoteDatasource.getArticles()) 62 | .thenAnswer((realInvocation) => Future.value(const Left(failure))); 63 | 64 | // act 65 | final result = await repository.getRemoteArticles(); 66 | 67 | // assert 68 | expect(result, const Left(failure)); 69 | verify(remoteDatasource.getArticles()); 70 | verifyNoMoreInteractions(remoteDatasource); 71 | verifyZeroInteractions(localDatasource); 72 | }); 73 | }); 74 | 75 | group('local articles:', () { 76 | test( 77 | 'should return articles data when the call to localDataSource return with none empty or null data', 78 | () async { 79 | // arrange 80 | when(localDatasource.getArticles()) 81 | .thenAnswer((realInvocation) => Future.value(articles)); 82 | 83 | // act 84 | final result = await repository.getLocalArticles(); 85 | 86 | // assert 87 | expect(result, Right(articles)); 88 | verify(localDatasource.getArticles()); 89 | verifyNoMoreInteractions(localDatasource); 90 | }); 91 | 92 | test( 93 | 'should return failure when the call to localDataSource return with empty or null data', 94 | () async { 95 | // arrange 96 | when(localDatasource.getArticles()) 97 | .thenAnswer((realInvocation) => Future.value([])); 98 | 99 | // act 100 | final result = await repository.getLocalArticles(); 101 | 102 | // assert 103 | expect(result, isA()); 104 | verify(localDatasource.getArticles()); 105 | verifyNoMoreInteractions(localDatasource); 106 | }); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /lib/app/presentation/home_get/home_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity/connectivity.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:get/get.dart'; 4 | import '../../../di/injector.dart'; 5 | 6 | import '../../domain/usecases/get_local_articles.dart'; 7 | import '../../domain/usecases/get_remote_articles.dart'; 8 | 9 | import '../../core/network/network_info.dart'; 10 | import 'package:getx_hacker_news_api/app/core/usecases/usecase.dart'; 11 | import '../../domain/entities/article.dart'; 12 | import '../../core/errors/failure.dart'; 13 | 14 | enum ViewState { initial, busy, error, data } 15 | 16 | class HomeController extends GetxController { 17 | // network info 18 | final network = Injector.resolve(); 19 | final getRemoteArticles = Injector.resolve(); 20 | final getLocalArticles = Injector.resolve(); 21 | // view state reactive value 22 | final viewState = ViewState.initial.obs; 23 | // device connectivity state reactive value 24 | final connectvityResult = ConnectivityResult.none.obs; 25 | // view state history 26 | final historyViewState = []; 27 | 28 | // articles data 29 | List
_articles; 30 | // articles getter 31 | List
get articles => List.from(_articles); 32 | // track local/remote articles state in view 33 | bool localArticlesView = false; 34 | 35 | @override 36 | Future onInit() async { 37 | super.onInit(); 38 | // check for connectivity 39 | connectvityResult.value = await network.connectivityResult; 40 | 41 | if (connectvityResult.value == ConnectivityResult.none) { 42 | localFetch(); 43 | } else { 44 | remoteFetch(); 45 | } 46 | 47 | // listen to connectivity changed event and update connectvityResult value 48 | network.onConnectivityChanged.listen((event) { 49 | connectvityResult.value = event; 50 | // automatically evoke remote fetch if device is offline 51 | // and articles data is empty, null or in local view 52 | if (event != ConnectivityResult.none && 53 | (_articles == null || _articles.isEmpty || localArticlesView)) { 54 | remoteFetch(); 55 | } 56 | }); 57 | } 58 | 59 | @override 60 | void onClose() { 61 | // close subscriptions for rx values 62 | viewState.close(); 63 | connectvityResult.close(); 64 | } 65 | 66 | // feth data from articles service 67 | Future remoteFetch() async { 68 | localArticlesView = false; 69 | if (viewState.value == ViewState.busy) return; 70 | if (connectvityResult.value == ConnectivityResult.none) { 71 | Get.snackbar("Can't refresh when offline", 72 | "Please connect your device to wifi or mobile network", 73 | snackPosition: SnackPosition.BOTTOM); 74 | return; 75 | } 76 | _setState(ViewState.busy); 77 | final result = await getRemoteArticles.call(NoParams()); 78 | _handleFetchResult(result); 79 | } 80 | 81 | // feth data from local database 82 | Future localFetch() async { 83 | localArticlesView = true; 84 | if (viewState.value == ViewState.busy) return; 85 | _setState(ViewState.busy); 86 | final result = await getLocalArticles.call(NoParams()); 87 | _handleFetchResult(result, true); 88 | } 89 | 90 | // handle api fetch result 91 | void _handleFetchResult(Either> result, 92 | [bool local = false]) { 93 | result.fold((feilure) { 94 | _articles?.clear(); 95 | _setState(ViewState.error); 96 | Get.snackbar('Refresh failed!', "Can't load articles", 97 | snackPosition: SnackPosition.BOTTOM); 98 | }, (data) { 99 | _articles = data; 100 | _setState(ViewState.data); 101 | final notifyLocal = local ? '(offline mode)' : ''; 102 | Get.snackbar('Refresh successfuly!', 103 | ' ${_articles.length} new articles ready for reading $notifyLocal', 104 | snackPosition: SnackPosition.BOTTOM); 105 | }); 106 | } 107 | 108 | // set viewstate 109 | void _setState(ViewState state) { 110 | viewState.value = state; 111 | historyViewState.add(state); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/controller/event.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'event.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ArticlesEventTearOff { 14 | const _$ArticlesEventTearOff(); 15 | 16 | // ignore: unused_element 17 | GetData getData() { 18 | return const GetData(); 19 | } 20 | } 21 | 22 | /// @nodoc 23 | // ignore: unused_element 24 | const $ArticlesEvent = _$ArticlesEventTearOff(); 25 | 26 | /// @nodoc 27 | mixin _$ArticlesEvent { 28 | @optionalTypeArgs 29 | Result when({ 30 | @required Result getData(), 31 | }); 32 | @optionalTypeArgs 33 | Result maybeWhen({ 34 | Result getData(), 35 | @required Result orElse(), 36 | }); 37 | @optionalTypeArgs 38 | Result map({ 39 | @required Result getData(GetData value), 40 | }); 41 | @optionalTypeArgs 42 | Result maybeMap({ 43 | Result getData(GetData value), 44 | @required Result orElse(), 45 | }); 46 | } 47 | 48 | /// @nodoc 49 | abstract class $ArticlesEventCopyWith<$Res> { 50 | factory $ArticlesEventCopyWith( 51 | ArticlesEvent value, $Res Function(ArticlesEvent) then) = 52 | _$ArticlesEventCopyWithImpl<$Res>; 53 | } 54 | 55 | /// @nodoc 56 | class _$ArticlesEventCopyWithImpl<$Res> 57 | implements $ArticlesEventCopyWith<$Res> { 58 | _$ArticlesEventCopyWithImpl(this._value, this._then); 59 | 60 | final ArticlesEvent _value; 61 | // ignore: unused_field 62 | final $Res Function(ArticlesEvent) _then; 63 | } 64 | 65 | /// @nodoc 66 | abstract class $GetDataCopyWith<$Res> { 67 | factory $GetDataCopyWith(GetData value, $Res Function(GetData) then) = 68 | _$GetDataCopyWithImpl<$Res>; 69 | } 70 | 71 | /// @nodoc 72 | class _$GetDataCopyWithImpl<$Res> extends _$ArticlesEventCopyWithImpl<$Res> 73 | implements $GetDataCopyWith<$Res> { 74 | _$GetDataCopyWithImpl(GetData _value, $Res Function(GetData) _then) 75 | : super(_value, (v) => _then(v as GetData)); 76 | 77 | @override 78 | GetData get _value => super._value as GetData; 79 | } 80 | 81 | /// @nodoc 82 | class _$GetData with DiagnosticableTreeMixin implements GetData { 83 | const _$GetData(); 84 | 85 | @override 86 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 87 | return 'ArticlesEvent.getData()'; 88 | } 89 | 90 | @override 91 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 92 | super.debugFillProperties(properties); 93 | properties..add(DiagnosticsProperty('type', 'ArticlesEvent.getData')); 94 | } 95 | 96 | @override 97 | bool operator ==(dynamic other) { 98 | return identical(this, other) || (other is GetData); 99 | } 100 | 101 | @override 102 | int get hashCode => runtimeType.hashCode; 103 | 104 | @override 105 | @optionalTypeArgs 106 | Result when({ 107 | @required Result getData(), 108 | }) { 109 | assert(getData != null); 110 | return getData(); 111 | } 112 | 113 | @override 114 | @optionalTypeArgs 115 | Result maybeWhen({ 116 | Result getData(), 117 | @required Result orElse(), 118 | }) { 119 | assert(orElse != null); 120 | if (getData != null) { 121 | return getData(); 122 | } 123 | return orElse(); 124 | } 125 | 126 | @override 127 | @optionalTypeArgs 128 | Result map({ 129 | @required Result getData(GetData value), 130 | }) { 131 | assert(getData != null); 132 | return getData(this); 133 | } 134 | 135 | @override 136 | @optionalTypeArgs 137 | Result maybeMap({ 138 | Result getData(GetData value), 139 | @required Result orElse(), 140 | }) { 141 | assert(orElse != null); 142 | if (getData != null) { 143 | return getData(this); 144 | } 145 | return orElse(); 146 | } 147 | } 148 | 149 | abstract class GetData implements ArticlesEvent { 150 | const factory GetData() = _$GetData; 151 | } 152 | -------------------------------------------------------------------------------- /lib/app/data/datasources/local/sqflite/articles_local_datasource_sqlite.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:sqflite/sqflite.dart'; 4 | import 'package:path/path.dart'; 5 | 6 | import '../articles_local_datasource.dart'; 7 | import 'package:getx_hacker_news_api/app/data/models/article_model.dart'; 8 | 9 | class ArticlesLocalDatasourceSqlImpl implements ArticlesLocalDatasource { 10 | final _kDbFileName = 'sqflite_ex.db'; 11 | final _kDBTableName = 'articles_table'; 12 | Database _db; 13 | 14 | /// Opens a db local file. Creates the db table if it's not yet created. 15 | @override 16 | Future initDb() async { 17 | try { 18 | // get database path directory 19 | final dbFolder = await getDatabasesPath(); 20 | if (!await Directory(dbFolder).exists()) { 21 | await Directory(dbFolder).create(recursive: true); 22 | } 23 | final dbPath = join(dbFolder, _kDbFileName); 24 | // open db 25 | _db = await openDatabase( 26 | dbPath, 27 | version: 1, 28 | onCreate: (db, version) async { 29 | await _initArticlesTable(db); 30 | }, 31 | ); 32 | // success init db 33 | return true; 34 | } on DatabaseException catch (e) { 35 | // failed to init db 36 | print(e); 37 | return false; 38 | } 39 | } 40 | 41 | /// delete the database 42 | @override 43 | Future deleteDb() async { 44 | try { 45 | final dbFolder = await getDatabasesPath(); 46 | if (!await Directory(dbFolder).exists()) { 47 | await Directory(dbFolder).create(recursive: true); 48 | } 49 | final dbPath = join(dbFolder, _kDbFileName); 50 | await deleteDatabase(dbPath); 51 | _db = null; 52 | return true; 53 | } catch (_) { 54 | return false; 55 | } 56 | } 57 | 58 | // creates articles table 59 | Future _initArticlesTable(Database db) async { 60 | await db.execute(''' 61 | CREATE TABLE $_kDBTableName( 62 | id INTEGER PRIMARY KEY, 63 | title TEXT, 64 | content TEXT, 65 | publishedAt TEXT, 66 | url TEXT, 67 | urlToImage TEXT 68 | ) 69 | '''); 70 | } 71 | 72 | /// save articles 73 | @override 74 | Future insertArticles(List articles) async { 75 | if (articles == null || articles.isEmpty) return false; 76 | await deleteAllArticles(); 77 | final validatedArticles = validateData(articles); 78 | try { 79 | for (final article in validatedArticles) { 80 | await _db.transaction( 81 | (txn) async { 82 | await txn.rawInsert(''' 83 | INSERT INTO $_kDBTableName 84 | ( 85 | title, 86 | content, 87 | publishedAt, 88 | url, 89 | urlToImage 90 | ) 91 | VALUES 92 | ( 93 | "${article.title}", 94 | "${article.content}", 95 | "${article.publishedAt.toIso8601String()}", 96 | "${article.url}", 97 | "${article.urlToImage}" 98 | )'''); 99 | }, 100 | ); 101 | } 102 | // success inserted data 103 | return true; 104 | } on DatabaseException catch (e) { 105 | // failed to insert data 106 | print(e); 107 | return false; 108 | } 109 | } 110 | 111 | /// delete all articles 112 | @override 113 | Future deleteAllArticles() async { 114 | try { 115 | await _db.rawDelete(''' 116 | DELETE FROM $_kDBTableName 117 | '''); 118 | return true; 119 | } on Exception catch (e) { 120 | print(e); 121 | return false; 122 | } 123 | } 124 | 125 | /// get all saved articles from db 126 | @override 127 | Future> getArticles() async { 128 | final jsons = await _db.rawQuery('SELECT * FROM $_kDBTableName'); 129 | return jsons.map((e) => ArticleModel.fromJson(e)).toList(); 130 | } 131 | 132 | /// validate Articles data to evoid database exception 133 | List validateData(List articles) { 134 | final validArticles = []; 135 | for (final article in articles) { 136 | final validTitle = article.title?.replaceAll('"', "'"); 137 | final validContent = article.content?.replaceAll('"', "'"); 138 | final validArticle = ArticleModel( 139 | title: validTitle, 140 | content: validContent, 141 | publishedAt: article.publishedAt, 142 | url: article.url, 143 | urlToImage: article.urlToImage); 144 | validArticles.add(validArticle); 145 | } 146 | return validArticles; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/app/presentation/home_bloc/controller/state.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'state.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ArticlesStateTearOff { 14 | const _$ArticlesStateTearOff(); 15 | 16 | // ignore: unused_element 17 | Initial initial() { 18 | return const Initial(); 19 | } 20 | 21 | // ignore: unused_element 22 | Loading loading() { 23 | return const Loading(); 24 | } 25 | 26 | // ignore: unused_element 27 | Success success(List
articles) { 28 | return Success( 29 | articles, 30 | ); 31 | } 32 | 33 | // ignore: unused_element 34 | Error error(Failure failure) { 35 | return Error( 36 | failure, 37 | ); 38 | } 39 | } 40 | 41 | /// @nodoc 42 | // ignore: unused_element 43 | const $ArticlesState = _$ArticlesStateTearOff(); 44 | 45 | /// @nodoc 46 | mixin _$ArticlesState { 47 | @optionalTypeArgs 48 | Result when({ 49 | @required Result initial(), 50 | @required Result loading(), 51 | @required Result success(List
articles), 52 | @required Result error(Failure failure), 53 | }); 54 | @optionalTypeArgs 55 | Result maybeWhen({ 56 | Result initial(), 57 | Result loading(), 58 | Result success(List
articles), 59 | Result error(Failure failure), 60 | @required Result orElse(), 61 | }); 62 | @optionalTypeArgs 63 | Result map({ 64 | @required Result initial(Initial value), 65 | @required Result loading(Loading value), 66 | @required Result success(Success value), 67 | @required Result error(Error value), 68 | }); 69 | @optionalTypeArgs 70 | Result maybeMap({ 71 | Result initial(Initial value), 72 | Result loading(Loading value), 73 | Result success(Success value), 74 | Result error(Error value), 75 | @required Result orElse(), 76 | }); 77 | } 78 | 79 | /// @nodoc 80 | abstract class $ArticlesStateCopyWith<$Res> { 81 | factory $ArticlesStateCopyWith( 82 | ArticlesState value, $Res Function(ArticlesState) then) = 83 | _$ArticlesStateCopyWithImpl<$Res>; 84 | } 85 | 86 | /// @nodoc 87 | class _$ArticlesStateCopyWithImpl<$Res> 88 | implements $ArticlesStateCopyWith<$Res> { 89 | _$ArticlesStateCopyWithImpl(this._value, this._then); 90 | 91 | final ArticlesState _value; 92 | // ignore: unused_field 93 | final $Res Function(ArticlesState) _then; 94 | } 95 | 96 | /// @nodoc 97 | abstract class $InitialCopyWith<$Res> { 98 | factory $InitialCopyWith(Initial value, $Res Function(Initial) then) = 99 | _$InitialCopyWithImpl<$Res>; 100 | } 101 | 102 | /// @nodoc 103 | class _$InitialCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 104 | implements $InitialCopyWith<$Res> { 105 | _$InitialCopyWithImpl(Initial _value, $Res Function(Initial) _then) 106 | : super(_value, (v) => _then(v as Initial)); 107 | 108 | @override 109 | Initial get _value => super._value as Initial; 110 | } 111 | 112 | /// @nodoc 113 | class _$Initial with DiagnosticableTreeMixin implements Initial { 114 | const _$Initial(); 115 | 116 | @override 117 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 118 | return 'ArticlesState.initial()'; 119 | } 120 | 121 | @override 122 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 123 | super.debugFillProperties(properties); 124 | properties..add(DiagnosticsProperty('type', 'ArticlesState.initial')); 125 | } 126 | 127 | @override 128 | bool operator ==(dynamic other) { 129 | return identical(this, other) || (other is Initial); 130 | } 131 | 132 | @override 133 | int get hashCode => runtimeType.hashCode; 134 | 135 | @override 136 | @optionalTypeArgs 137 | Result when({ 138 | @required Result initial(), 139 | @required Result loading(), 140 | @required Result success(List
articles), 141 | @required Result error(Failure failure), 142 | }) { 143 | assert(initial != null); 144 | assert(loading != null); 145 | assert(success != null); 146 | assert(error != null); 147 | return initial(); 148 | } 149 | 150 | @override 151 | @optionalTypeArgs 152 | Result maybeWhen({ 153 | Result initial(), 154 | Result loading(), 155 | Result success(List
articles), 156 | Result error(Failure failure), 157 | @required Result orElse(), 158 | }) { 159 | assert(orElse != null); 160 | if (initial != null) { 161 | return initial(); 162 | } 163 | return orElse(); 164 | } 165 | 166 | @override 167 | @optionalTypeArgs 168 | Result map({ 169 | @required Result initial(Initial value), 170 | @required Result loading(Loading value), 171 | @required Result success(Success value), 172 | @required Result error(Error value), 173 | }) { 174 | assert(initial != null); 175 | assert(loading != null); 176 | assert(success != null); 177 | assert(error != null); 178 | return initial(this); 179 | } 180 | 181 | @override 182 | @optionalTypeArgs 183 | Result maybeMap({ 184 | Result initial(Initial value), 185 | Result loading(Loading value), 186 | Result success(Success value), 187 | Result error(Error value), 188 | @required Result orElse(), 189 | }) { 190 | assert(orElse != null); 191 | if (initial != null) { 192 | return initial(this); 193 | } 194 | return orElse(); 195 | } 196 | } 197 | 198 | abstract class Initial implements ArticlesState { 199 | const factory Initial() = _$Initial; 200 | } 201 | 202 | /// @nodoc 203 | abstract class $LoadingCopyWith<$Res> { 204 | factory $LoadingCopyWith(Loading value, $Res Function(Loading) then) = 205 | _$LoadingCopyWithImpl<$Res>; 206 | } 207 | 208 | /// @nodoc 209 | class _$LoadingCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 210 | implements $LoadingCopyWith<$Res> { 211 | _$LoadingCopyWithImpl(Loading _value, $Res Function(Loading) _then) 212 | : super(_value, (v) => _then(v as Loading)); 213 | 214 | @override 215 | Loading get _value => super._value as Loading; 216 | } 217 | 218 | /// @nodoc 219 | class _$Loading with DiagnosticableTreeMixin implements Loading { 220 | const _$Loading(); 221 | 222 | @override 223 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 224 | return 'ArticlesState.loading()'; 225 | } 226 | 227 | @override 228 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 229 | super.debugFillProperties(properties); 230 | properties..add(DiagnosticsProperty('type', 'ArticlesState.loading')); 231 | } 232 | 233 | @override 234 | bool operator ==(dynamic other) { 235 | return identical(this, other) || (other is Loading); 236 | } 237 | 238 | @override 239 | int get hashCode => runtimeType.hashCode; 240 | 241 | @override 242 | @optionalTypeArgs 243 | Result when({ 244 | @required Result initial(), 245 | @required Result loading(), 246 | @required Result success(List
articles), 247 | @required Result error(Failure failure), 248 | }) { 249 | assert(initial != null); 250 | assert(loading != null); 251 | assert(success != null); 252 | assert(error != null); 253 | return loading(); 254 | } 255 | 256 | @override 257 | @optionalTypeArgs 258 | Result maybeWhen({ 259 | Result initial(), 260 | Result loading(), 261 | Result success(List
articles), 262 | Result error(Failure failure), 263 | @required Result orElse(), 264 | }) { 265 | assert(orElse != null); 266 | if (loading != null) { 267 | return loading(); 268 | } 269 | return orElse(); 270 | } 271 | 272 | @override 273 | @optionalTypeArgs 274 | Result map({ 275 | @required Result initial(Initial value), 276 | @required Result loading(Loading value), 277 | @required Result success(Success value), 278 | @required Result error(Error value), 279 | }) { 280 | assert(initial != null); 281 | assert(loading != null); 282 | assert(success != null); 283 | assert(error != null); 284 | return loading(this); 285 | } 286 | 287 | @override 288 | @optionalTypeArgs 289 | Result maybeMap({ 290 | Result initial(Initial value), 291 | Result loading(Loading value), 292 | Result success(Success value), 293 | Result error(Error value), 294 | @required Result orElse(), 295 | }) { 296 | assert(orElse != null); 297 | if (loading != null) { 298 | return loading(this); 299 | } 300 | return orElse(); 301 | } 302 | } 303 | 304 | abstract class Loading implements ArticlesState { 305 | const factory Loading() = _$Loading; 306 | } 307 | 308 | /// @nodoc 309 | abstract class $SuccessCopyWith<$Res> { 310 | factory $SuccessCopyWith(Success value, $Res Function(Success) then) = 311 | _$SuccessCopyWithImpl<$Res>; 312 | $Res call({List
articles}); 313 | } 314 | 315 | /// @nodoc 316 | class _$SuccessCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 317 | implements $SuccessCopyWith<$Res> { 318 | _$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then) 319 | : super(_value, (v) => _then(v as Success)); 320 | 321 | @override 322 | Success get _value => super._value as Success; 323 | 324 | @override 325 | $Res call({ 326 | Object articles = freezed, 327 | }) { 328 | return _then(Success( 329 | articles == freezed ? _value.articles : articles as List
, 330 | )); 331 | } 332 | } 333 | 334 | /// @nodoc 335 | class _$Success with DiagnosticableTreeMixin implements Success { 336 | const _$Success(this.articles) : assert(articles != null); 337 | 338 | @override 339 | final List
articles; 340 | 341 | @override 342 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 343 | return 'ArticlesState.success(articles: $articles)'; 344 | } 345 | 346 | @override 347 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 348 | super.debugFillProperties(properties); 349 | properties 350 | ..add(DiagnosticsProperty('type', 'ArticlesState.success')) 351 | ..add(DiagnosticsProperty('articles', articles)); 352 | } 353 | 354 | @override 355 | bool operator ==(dynamic other) { 356 | return identical(this, other) || 357 | (other is Success && 358 | (identical(other.articles, articles) || 359 | const DeepCollectionEquality() 360 | .equals(other.articles, articles))); 361 | } 362 | 363 | @override 364 | int get hashCode => 365 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(articles); 366 | 367 | @override 368 | $SuccessCopyWith get copyWith => 369 | _$SuccessCopyWithImpl(this, _$identity); 370 | 371 | @override 372 | @optionalTypeArgs 373 | Result when({ 374 | @required Result initial(), 375 | @required Result loading(), 376 | @required Result success(List
articles), 377 | @required Result error(Failure failure), 378 | }) { 379 | assert(initial != null); 380 | assert(loading != null); 381 | assert(success != null); 382 | assert(error != null); 383 | return success(articles); 384 | } 385 | 386 | @override 387 | @optionalTypeArgs 388 | Result maybeWhen({ 389 | Result initial(), 390 | Result loading(), 391 | Result success(List
articles), 392 | Result error(Failure failure), 393 | @required Result orElse(), 394 | }) { 395 | assert(orElse != null); 396 | if (success != null) { 397 | return success(articles); 398 | } 399 | return orElse(); 400 | } 401 | 402 | @override 403 | @optionalTypeArgs 404 | Result map({ 405 | @required Result initial(Initial value), 406 | @required Result loading(Loading value), 407 | @required Result success(Success value), 408 | @required Result error(Error value), 409 | }) { 410 | assert(initial != null); 411 | assert(loading != null); 412 | assert(success != null); 413 | assert(error != null); 414 | return success(this); 415 | } 416 | 417 | @override 418 | @optionalTypeArgs 419 | Result maybeMap({ 420 | Result initial(Initial value), 421 | Result loading(Loading value), 422 | Result success(Success value), 423 | Result error(Error value), 424 | @required Result orElse(), 425 | }) { 426 | assert(orElse != null); 427 | if (success != null) { 428 | return success(this); 429 | } 430 | return orElse(); 431 | } 432 | } 433 | 434 | abstract class Success implements ArticlesState { 435 | const factory Success(List
articles) = _$Success; 436 | 437 | List
get articles; 438 | $SuccessCopyWith get copyWith; 439 | } 440 | 441 | /// @nodoc 442 | abstract class $ErrorCopyWith<$Res> { 443 | factory $ErrorCopyWith(Error value, $Res Function(Error) then) = 444 | _$ErrorCopyWithImpl<$Res>; 445 | $Res call({Failure failure}); 446 | } 447 | 448 | /// @nodoc 449 | class _$ErrorCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 450 | implements $ErrorCopyWith<$Res> { 451 | _$ErrorCopyWithImpl(Error _value, $Res Function(Error) _then) 452 | : super(_value, (v) => _then(v as Error)); 453 | 454 | @override 455 | Error get _value => super._value as Error; 456 | 457 | @override 458 | $Res call({ 459 | Object failure = freezed, 460 | }) { 461 | return _then(Error( 462 | failure == freezed ? _value.failure : failure as Failure, 463 | )); 464 | } 465 | } 466 | 467 | /// @nodoc 468 | class _$Error with DiagnosticableTreeMixin implements Error { 469 | const _$Error(this.failure) : assert(failure != null); 470 | 471 | @override 472 | final Failure failure; 473 | 474 | @override 475 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 476 | return 'ArticlesState.error(failure: $failure)'; 477 | } 478 | 479 | @override 480 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 481 | super.debugFillProperties(properties); 482 | properties 483 | ..add(DiagnosticsProperty('type', 'ArticlesState.error')) 484 | ..add(DiagnosticsProperty('failure', failure)); 485 | } 486 | 487 | @override 488 | bool operator ==(dynamic other) { 489 | return identical(this, other) || 490 | (other is Error && 491 | (identical(other.failure, failure) || 492 | const DeepCollectionEquality().equals(other.failure, failure))); 493 | } 494 | 495 | @override 496 | int get hashCode => 497 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(failure); 498 | 499 | @override 500 | $ErrorCopyWith get copyWith => 501 | _$ErrorCopyWithImpl(this, _$identity); 502 | 503 | @override 504 | @optionalTypeArgs 505 | Result when({ 506 | @required Result initial(), 507 | @required Result loading(), 508 | @required Result success(List
articles), 509 | @required Result error(Failure failure), 510 | }) { 511 | assert(initial != null); 512 | assert(loading != null); 513 | assert(success != null); 514 | assert(error != null); 515 | return error(failure); 516 | } 517 | 518 | @override 519 | @optionalTypeArgs 520 | Result maybeWhen({ 521 | Result initial(), 522 | Result loading(), 523 | Result success(List
articles), 524 | Result error(Failure failure), 525 | @required Result orElse(), 526 | }) { 527 | assert(orElse != null); 528 | if (error != null) { 529 | return error(failure); 530 | } 531 | return orElse(); 532 | } 533 | 534 | @override 535 | @optionalTypeArgs 536 | Result map({ 537 | @required Result initial(Initial value), 538 | @required Result loading(Loading value), 539 | @required Result success(Success value), 540 | @required Result error(Error value), 541 | }) { 542 | assert(initial != null); 543 | assert(loading != null); 544 | assert(success != null); 545 | assert(error != null); 546 | return error(this); 547 | } 548 | 549 | @override 550 | @optionalTypeArgs 551 | Result maybeMap({ 552 | Result initial(Initial value), 553 | Result loading(Loading value), 554 | Result success(Success value), 555 | Result error(Error value), 556 | @required Result orElse(), 557 | }) { 558 | assert(orElse != null); 559 | if (error != null) { 560 | return error(this); 561 | } 562 | return orElse(); 563 | } 564 | } 565 | 566 | abstract class Error implements ArticlesState { 567 | const factory Error(Failure failure) = _$Error; 568 | 569 | Failure get failure; 570 | $ErrorCopyWith get copyWith; 571 | } 572 | -------------------------------------------------------------------------------- /lib/app/presentation/home_cubit/controller/state.freezed.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies 3 | 4 | part of 'state.dart'; 5 | 6 | // ************************************************************************** 7 | // FreezedGenerator 8 | // ************************************************************************** 9 | 10 | T _$identity(T value) => value; 11 | 12 | /// @nodoc 13 | class _$ArticlesStateTearOff { 14 | const _$ArticlesStateTearOff(); 15 | 16 | // ignore: unused_element 17 | Initial initial() { 18 | return const Initial(); 19 | } 20 | 21 | // ignore: unused_element 22 | Loading loading() { 23 | return const Loading(); 24 | } 25 | 26 | // ignore: unused_element 27 | Success success(List
articles) { 28 | return Success( 29 | articles, 30 | ); 31 | } 32 | 33 | // ignore: unused_element 34 | Error error(Failure failure) { 35 | return Error( 36 | failure, 37 | ); 38 | } 39 | } 40 | 41 | /// @nodoc 42 | // ignore: unused_element 43 | const $ArticlesState = _$ArticlesStateTearOff(); 44 | 45 | /// @nodoc 46 | mixin _$ArticlesState { 47 | @optionalTypeArgs 48 | Result when({ 49 | @required Result initial(), 50 | @required Result loading(), 51 | @required Result success(List
articles), 52 | @required Result error(Failure failure), 53 | }); 54 | @optionalTypeArgs 55 | Result maybeWhen({ 56 | Result initial(), 57 | Result loading(), 58 | Result success(List
articles), 59 | Result error(Failure failure), 60 | @required Result orElse(), 61 | }); 62 | @optionalTypeArgs 63 | Result map({ 64 | @required Result initial(Initial value), 65 | @required Result loading(Loading value), 66 | @required Result success(Success value), 67 | @required Result error(Error value), 68 | }); 69 | @optionalTypeArgs 70 | Result maybeMap({ 71 | Result initial(Initial value), 72 | Result loading(Loading value), 73 | Result success(Success value), 74 | Result error(Error value), 75 | @required Result orElse(), 76 | }); 77 | } 78 | 79 | /// @nodoc 80 | abstract class $ArticlesStateCopyWith<$Res> { 81 | factory $ArticlesStateCopyWith( 82 | ArticlesState value, $Res Function(ArticlesState) then) = 83 | _$ArticlesStateCopyWithImpl<$Res>; 84 | } 85 | 86 | /// @nodoc 87 | class _$ArticlesStateCopyWithImpl<$Res> 88 | implements $ArticlesStateCopyWith<$Res> { 89 | _$ArticlesStateCopyWithImpl(this._value, this._then); 90 | 91 | final ArticlesState _value; 92 | // ignore: unused_field 93 | final $Res Function(ArticlesState) _then; 94 | } 95 | 96 | /// @nodoc 97 | abstract class $InitialCopyWith<$Res> { 98 | factory $InitialCopyWith(Initial value, $Res Function(Initial) then) = 99 | _$InitialCopyWithImpl<$Res>; 100 | } 101 | 102 | /// @nodoc 103 | class _$InitialCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 104 | implements $InitialCopyWith<$Res> { 105 | _$InitialCopyWithImpl(Initial _value, $Res Function(Initial) _then) 106 | : super(_value, (v) => _then(v as Initial)); 107 | 108 | @override 109 | Initial get _value => super._value as Initial; 110 | } 111 | 112 | /// @nodoc 113 | class _$Initial with DiagnosticableTreeMixin implements Initial { 114 | const _$Initial(); 115 | 116 | @override 117 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 118 | return 'ArticlesState.initial()'; 119 | } 120 | 121 | @override 122 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 123 | super.debugFillProperties(properties); 124 | properties..add(DiagnosticsProperty('type', 'ArticlesState.initial')); 125 | } 126 | 127 | @override 128 | bool operator ==(dynamic other) { 129 | return identical(this, other) || (other is Initial); 130 | } 131 | 132 | @override 133 | int get hashCode => runtimeType.hashCode; 134 | 135 | @override 136 | @optionalTypeArgs 137 | Result when({ 138 | @required Result initial(), 139 | @required Result loading(), 140 | @required Result success(List
articles), 141 | @required Result error(Failure failure), 142 | }) { 143 | assert(initial != null); 144 | assert(loading != null); 145 | assert(success != null); 146 | assert(error != null); 147 | return initial(); 148 | } 149 | 150 | @override 151 | @optionalTypeArgs 152 | Result maybeWhen({ 153 | Result initial(), 154 | Result loading(), 155 | Result success(List
articles), 156 | Result error(Failure failure), 157 | @required Result orElse(), 158 | }) { 159 | assert(orElse != null); 160 | if (initial != null) { 161 | return initial(); 162 | } 163 | return orElse(); 164 | } 165 | 166 | @override 167 | @optionalTypeArgs 168 | Result map({ 169 | @required Result initial(Initial value), 170 | @required Result loading(Loading value), 171 | @required Result success(Success value), 172 | @required Result error(Error value), 173 | }) { 174 | assert(initial != null); 175 | assert(loading != null); 176 | assert(success != null); 177 | assert(error != null); 178 | return initial(this); 179 | } 180 | 181 | @override 182 | @optionalTypeArgs 183 | Result maybeMap({ 184 | Result initial(Initial value), 185 | Result loading(Loading value), 186 | Result success(Success value), 187 | Result error(Error value), 188 | @required Result orElse(), 189 | }) { 190 | assert(orElse != null); 191 | if (initial != null) { 192 | return initial(this); 193 | } 194 | return orElse(); 195 | } 196 | } 197 | 198 | abstract class Initial implements ArticlesState { 199 | const factory Initial() = _$Initial; 200 | } 201 | 202 | /// @nodoc 203 | abstract class $LoadingCopyWith<$Res> { 204 | factory $LoadingCopyWith(Loading value, $Res Function(Loading) then) = 205 | _$LoadingCopyWithImpl<$Res>; 206 | } 207 | 208 | /// @nodoc 209 | class _$LoadingCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 210 | implements $LoadingCopyWith<$Res> { 211 | _$LoadingCopyWithImpl(Loading _value, $Res Function(Loading) _then) 212 | : super(_value, (v) => _then(v as Loading)); 213 | 214 | @override 215 | Loading get _value => super._value as Loading; 216 | } 217 | 218 | /// @nodoc 219 | class _$Loading with DiagnosticableTreeMixin implements Loading { 220 | const _$Loading(); 221 | 222 | @override 223 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 224 | return 'ArticlesState.loading()'; 225 | } 226 | 227 | @override 228 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 229 | super.debugFillProperties(properties); 230 | properties..add(DiagnosticsProperty('type', 'ArticlesState.loading')); 231 | } 232 | 233 | @override 234 | bool operator ==(dynamic other) { 235 | return identical(this, other) || (other is Loading); 236 | } 237 | 238 | @override 239 | int get hashCode => runtimeType.hashCode; 240 | 241 | @override 242 | @optionalTypeArgs 243 | Result when({ 244 | @required Result initial(), 245 | @required Result loading(), 246 | @required Result success(List
articles), 247 | @required Result error(Failure failure), 248 | }) { 249 | assert(initial != null); 250 | assert(loading != null); 251 | assert(success != null); 252 | assert(error != null); 253 | return loading(); 254 | } 255 | 256 | @override 257 | @optionalTypeArgs 258 | Result maybeWhen({ 259 | Result initial(), 260 | Result loading(), 261 | Result success(List
articles), 262 | Result error(Failure failure), 263 | @required Result orElse(), 264 | }) { 265 | assert(orElse != null); 266 | if (loading != null) { 267 | return loading(); 268 | } 269 | return orElse(); 270 | } 271 | 272 | @override 273 | @optionalTypeArgs 274 | Result map({ 275 | @required Result initial(Initial value), 276 | @required Result loading(Loading value), 277 | @required Result success(Success value), 278 | @required Result error(Error value), 279 | }) { 280 | assert(initial != null); 281 | assert(loading != null); 282 | assert(success != null); 283 | assert(error != null); 284 | return loading(this); 285 | } 286 | 287 | @override 288 | @optionalTypeArgs 289 | Result maybeMap({ 290 | Result initial(Initial value), 291 | Result loading(Loading value), 292 | Result success(Success value), 293 | Result error(Error value), 294 | @required Result orElse(), 295 | }) { 296 | assert(orElse != null); 297 | if (loading != null) { 298 | return loading(this); 299 | } 300 | return orElse(); 301 | } 302 | } 303 | 304 | abstract class Loading implements ArticlesState { 305 | const factory Loading() = _$Loading; 306 | } 307 | 308 | /// @nodoc 309 | abstract class $SuccessCopyWith<$Res> { 310 | factory $SuccessCopyWith(Success value, $Res Function(Success) then) = 311 | _$SuccessCopyWithImpl<$Res>; 312 | $Res call({List
articles}); 313 | } 314 | 315 | /// @nodoc 316 | class _$SuccessCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 317 | implements $SuccessCopyWith<$Res> { 318 | _$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then) 319 | : super(_value, (v) => _then(v as Success)); 320 | 321 | @override 322 | Success get _value => super._value as Success; 323 | 324 | @override 325 | $Res call({ 326 | Object articles = freezed, 327 | }) { 328 | return _then(Success( 329 | articles == freezed ? _value.articles : articles as List
, 330 | )); 331 | } 332 | } 333 | 334 | /// @nodoc 335 | class _$Success with DiagnosticableTreeMixin implements Success { 336 | const _$Success(this.articles) : assert(articles != null); 337 | 338 | @override 339 | final List
articles; 340 | 341 | @override 342 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 343 | return 'ArticlesState.success(articles: $articles)'; 344 | } 345 | 346 | @override 347 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 348 | super.debugFillProperties(properties); 349 | properties 350 | ..add(DiagnosticsProperty('type', 'ArticlesState.success')) 351 | ..add(DiagnosticsProperty('articles', articles)); 352 | } 353 | 354 | @override 355 | bool operator ==(dynamic other) { 356 | return identical(this, other) || 357 | (other is Success && 358 | (identical(other.articles, articles) || 359 | const DeepCollectionEquality() 360 | .equals(other.articles, articles))); 361 | } 362 | 363 | @override 364 | int get hashCode => 365 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(articles); 366 | 367 | @override 368 | $SuccessCopyWith get copyWith => 369 | _$SuccessCopyWithImpl(this, _$identity); 370 | 371 | @override 372 | @optionalTypeArgs 373 | Result when({ 374 | @required Result initial(), 375 | @required Result loading(), 376 | @required Result success(List
articles), 377 | @required Result error(Failure failure), 378 | }) { 379 | assert(initial != null); 380 | assert(loading != null); 381 | assert(success != null); 382 | assert(error != null); 383 | return success(articles); 384 | } 385 | 386 | @override 387 | @optionalTypeArgs 388 | Result maybeWhen({ 389 | Result initial(), 390 | Result loading(), 391 | Result success(List
articles), 392 | Result error(Failure failure), 393 | @required Result orElse(), 394 | }) { 395 | assert(orElse != null); 396 | if (success != null) { 397 | return success(articles); 398 | } 399 | return orElse(); 400 | } 401 | 402 | @override 403 | @optionalTypeArgs 404 | Result map({ 405 | @required Result initial(Initial value), 406 | @required Result loading(Loading value), 407 | @required Result success(Success value), 408 | @required Result error(Error value), 409 | }) { 410 | assert(initial != null); 411 | assert(loading != null); 412 | assert(success != null); 413 | assert(error != null); 414 | return success(this); 415 | } 416 | 417 | @override 418 | @optionalTypeArgs 419 | Result maybeMap({ 420 | Result initial(Initial value), 421 | Result loading(Loading value), 422 | Result success(Success value), 423 | Result error(Error value), 424 | @required Result orElse(), 425 | }) { 426 | assert(orElse != null); 427 | if (success != null) { 428 | return success(this); 429 | } 430 | return orElse(); 431 | } 432 | } 433 | 434 | abstract class Success implements ArticlesState { 435 | const factory Success(List
articles) = _$Success; 436 | 437 | List
get articles; 438 | $SuccessCopyWith get copyWith; 439 | } 440 | 441 | /// @nodoc 442 | abstract class $ErrorCopyWith<$Res> { 443 | factory $ErrorCopyWith(Error value, $Res Function(Error) then) = 444 | _$ErrorCopyWithImpl<$Res>; 445 | $Res call({Failure failure}); 446 | } 447 | 448 | /// @nodoc 449 | class _$ErrorCopyWithImpl<$Res> extends _$ArticlesStateCopyWithImpl<$Res> 450 | implements $ErrorCopyWith<$Res> { 451 | _$ErrorCopyWithImpl(Error _value, $Res Function(Error) _then) 452 | : super(_value, (v) => _then(v as Error)); 453 | 454 | @override 455 | Error get _value => super._value as Error; 456 | 457 | @override 458 | $Res call({ 459 | Object failure = freezed, 460 | }) { 461 | return _then(Error( 462 | failure == freezed ? _value.failure : failure as Failure, 463 | )); 464 | } 465 | } 466 | 467 | /// @nodoc 468 | class _$Error with DiagnosticableTreeMixin implements Error { 469 | const _$Error(this.failure) : assert(failure != null); 470 | 471 | @override 472 | final Failure failure; 473 | 474 | @override 475 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 476 | return 'ArticlesState.error(failure: $failure)'; 477 | } 478 | 479 | @override 480 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 481 | super.debugFillProperties(properties); 482 | properties 483 | ..add(DiagnosticsProperty('type', 'ArticlesState.error')) 484 | ..add(DiagnosticsProperty('failure', failure)); 485 | } 486 | 487 | @override 488 | bool operator ==(dynamic other) { 489 | return identical(this, other) || 490 | (other is Error && 491 | (identical(other.failure, failure) || 492 | const DeepCollectionEquality().equals(other.failure, failure))); 493 | } 494 | 495 | @override 496 | int get hashCode => 497 | runtimeType.hashCode ^ const DeepCollectionEquality().hash(failure); 498 | 499 | @override 500 | $ErrorCopyWith get copyWith => 501 | _$ErrorCopyWithImpl(this, _$identity); 502 | 503 | @override 504 | @optionalTypeArgs 505 | Result when({ 506 | @required Result initial(), 507 | @required Result loading(), 508 | @required Result success(List
articles), 509 | @required Result error(Failure failure), 510 | }) { 511 | assert(initial != null); 512 | assert(loading != null); 513 | assert(success != null); 514 | assert(error != null); 515 | return error(failure); 516 | } 517 | 518 | @override 519 | @optionalTypeArgs 520 | Result maybeWhen({ 521 | Result initial(), 522 | Result loading(), 523 | Result success(List
articles), 524 | Result error(Failure failure), 525 | @required Result orElse(), 526 | }) { 527 | assert(orElse != null); 528 | if (error != null) { 529 | return error(failure); 530 | } 531 | return orElse(); 532 | } 533 | 534 | @override 535 | @optionalTypeArgs 536 | Result map({ 537 | @required Result initial(Initial value), 538 | @required Result loading(Loading value), 539 | @required Result success(Success value), 540 | @required Result error(Error value), 541 | }) { 542 | assert(initial != null); 543 | assert(loading != null); 544 | assert(success != null); 545 | assert(error != null); 546 | return error(this); 547 | } 548 | 549 | @override 550 | @optionalTypeArgs 551 | Result maybeMap({ 552 | Result initial(Initial value), 553 | Result loading(Loading value), 554 | Result success(Success value), 555 | Result error(Error value), 556 | @required Result orElse(), 557 | }) { 558 | assert(orElse != null); 559 | if (error != null) { 560 | return error(this); 561 | } 562 | return orElse(); 563 | } 564 | } 565 | 566 | abstract class Error implements ArticlesState { 567 | const factory Error(Failure failure) = _$Error; 568 | 569 | Failure get failure; 570 | $ErrorCopyWith get copyWith; 571 | } 572 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1020; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | FRAMEWORK_SEARCH_PATHS = ( 293 | "$(inherited)", 294 | "$(PROJECT_DIR)/Flutter", 295 | ); 296 | INFOPLIST_FILE = Runner/Info.plist; 297 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 298 | LIBRARY_SEARCH_PATHS = ( 299 | "$(inherited)", 300 | "$(PROJECT_DIR)/Flutter", 301 | ); 302 | PRODUCT_BUNDLE_IDENTIFIER = com.omergamliel.getXHackerNewsAPi; 303 | PRODUCT_NAME = "$(TARGET_NAME)"; 304 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 305 | SWIFT_VERSION = 5.0; 306 | VERSIONING_SYSTEM = "apple-generic"; 307 | }; 308 | name = Profile; 309 | }; 310 | 97C147031CF9000F007C117D /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ALWAYS_SEARCH_USER_PATHS = NO; 314 | CLANG_ANALYZER_NONNULL = YES; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 316 | CLANG_CXX_LIBRARY = "libc++"; 317 | CLANG_ENABLE_MODULES = YES; 318 | CLANG_ENABLE_OBJC_ARC = YES; 319 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 320 | CLANG_WARN_BOOL_CONVERSION = YES; 321 | CLANG_WARN_COMMA = YES; 322 | CLANG_WARN_CONSTANT_CONVERSION = YES; 323 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_EMPTY_BODY = YES; 326 | CLANG_WARN_ENUM_CONVERSION = YES; 327 | CLANG_WARN_INFINITE_RECURSION = YES; 328 | CLANG_WARN_INT_CONVERSION = YES; 329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 333 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 334 | CLANG_WARN_STRICT_PROTOTYPES = YES; 335 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 336 | CLANG_WARN_UNREACHABLE_CODE = YES; 337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 338 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 339 | COPY_PHASE_STRIP = NO; 340 | DEBUG_INFORMATION_FORMAT = dwarf; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | ENABLE_TESTABILITY = YES; 343 | GCC_C_LANGUAGE_STANDARD = gnu99; 344 | GCC_DYNAMIC_NO_PIC = NO; 345 | GCC_NO_COMMON_BLOCKS = YES; 346 | GCC_OPTIMIZATION_LEVEL = 0; 347 | GCC_PREPROCESSOR_DEFINITIONS = ( 348 | "DEBUG=1", 349 | "$(inherited)", 350 | ); 351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 353 | GCC_WARN_UNDECLARED_SELECTOR = YES; 354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 355 | GCC_WARN_UNUSED_FUNCTION = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 358 | MTL_ENABLE_DEBUG_INFO = YES; 359 | ONLY_ACTIVE_ARCH = YES; 360 | SDKROOT = iphoneos; 361 | TARGETED_DEVICE_FAMILY = "1,2"; 362 | }; 363 | name = Debug; 364 | }; 365 | 97C147041CF9000F007C117D /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ALWAYS_SEARCH_USER_PATHS = NO; 369 | CLANG_ANALYZER_NONNULL = YES; 370 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 371 | CLANG_CXX_LIBRARY = "libc++"; 372 | CLANG_ENABLE_MODULES = YES; 373 | CLANG_ENABLE_OBJC_ARC = YES; 374 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 375 | CLANG_WARN_BOOL_CONVERSION = YES; 376 | CLANG_WARN_COMMA = YES; 377 | CLANG_WARN_CONSTANT_CONVERSION = YES; 378 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 379 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 380 | CLANG_WARN_EMPTY_BODY = YES; 381 | CLANG_WARN_ENUM_CONVERSION = YES; 382 | CLANG_WARN_INFINITE_RECURSION = YES; 383 | CLANG_WARN_INT_CONVERSION = YES; 384 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 386 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 387 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 388 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 389 | CLANG_WARN_STRICT_PROTOTYPES = YES; 390 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 394 | COPY_PHASE_STRIP = NO; 395 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 396 | ENABLE_NS_ASSERTIONS = NO; 397 | ENABLE_STRICT_OBJC_MSGSEND = YES; 398 | GCC_C_LANGUAGE_STANDARD = gnu99; 399 | GCC_NO_COMMON_BLOCKS = YES; 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 407 | MTL_ENABLE_DEBUG_INFO = NO; 408 | SDKROOT = iphoneos; 409 | SUPPORTED_PLATFORMS = iphoneos; 410 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | VALIDATE_PRODUCT = YES; 413 | }; 414 | name = Release; 415 | }; 416 | 97C147061CF9000F007C117D /* Debug */ = { 417 | isa = XCBuildConfiguration; 418 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | CLANG_ENABLE_MODULES = YES; 422 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 423 | ENABLE_BITCODE = NO; 424 | FRAMEWORK_SEARCH_PATHS = ( 425 | "$(inherited)", 426 | "$(PROJECT_DIR)/Flutter", 427 | ); 428 | INFOPLIST_FILE = Runner/Info.plist; 429 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 430 | LIBRARY_SEARCH_PATHS = ( 431 | "$(inherited)", 432 | "$(PROJECT_DIR)/Flutter", 433 | ); 434 | PRODUCT_BUNDLE_IDENTIFIER = com.omergamliel.getXHackerNewsAPi; 435 | PRODUCT_NAME = "$(TARGET_NAME)"; 436 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 437 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 438 | SWIFT_VERSION = 5.0; 439 | VERSIONING_SYSTEM = "apple-generic"; 440 | }; 441 | name = Debug; 442 | }; 443 | 97C147071CF9000F007C117D /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 446 | buildSettings = { 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | CLANG_ENABLE_MODULES = YES; 449 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 450 | ENABLE_BITCODE = NO; 451 | FRAMEWORK_SEARCH_PATHS = ( 452 | "$(inherited)", 453 | "$(PROJECT_DIR)/Flutter", 454 | ); 455 | INFOPLIST_FILE = Runner/Info.plist; 456 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 457 | LIBRARY_SEARCH_PATHS = ( 458 | "$(inherited)", 459 | "$(PROJECT_DIR)/Flutter", 460 | ); 461 | PRODUCT_BUNDLE_IDENTIFIER = com.omergamliel.getXHackerNewsAPi; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 464 | SWIFT_VERSION = 5.0; 465 | VERSIONING_SYSTEM = "apple-generic"; 466 | }; 467 | name = Release; 468 | }; 469 | /* End XCBuildConfiguration section */ 470 | 471 | /* Begin XCConfigurationList section */ 472 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | 97C147031CF9000F007C117D /* Debug */, 476 | 97C147041CF9000F007C117D /* Release */, 477 | 249021D3217E4FDB00AE95B9 /* Profile */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 483 | isa = XCConfigurationList; 484 | buildConfigurations = ( 485 | 97C147061CF9000F007C117D /* Debug */, 486 | 97C147071CF9000F007C117D /* Release */, 487 | 249021D4217E4FDB00AE95B9 /* Profile */, 488 | ); 489 | defaultConfigurationIsVisible = 0; 490 | defaultConfigurationName = Release; 491 | }; 492 | /* End XCConfigurationList section */ 493 | }; 494 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 495 | } --------------------------------------------------------------------------------