├── .fvmrc ├── lib ├── src │ ├── feature │ │ ├── weather │ │ │ ├── presentation │ │ │ │ ├── constants.dart │ │ │ │ ├── widgets │ │ │ │ │ ├── detail_block.dart │ │ │ │ │ ├── basic_weather_information.dart │ │ │ │ │ └── details_weather_information.dart │ │ │ │ ├── styles.dart │ │ │ │ └── screen │ │ │ │ │ └── weather_infromation_screen.dart │ │ │ ├── domain │ │ │ │ ├── base_repositories │ │ │ │ │ └── base_weather_repository.dart │ │ │ │ └── entity │ │ │ │ │ ├── weather_detail_entity.dart │ │ │ │ │ ├── weather_basic_entity.dart │ │ │ │ │ ├── weather_full_entity.dart │ │ │ │ │ ├── weather_detail_entity.freezed.dart │ │ │ │ │ ├── weather_basic_entity.freezed.dart │ │ │ │ │ └── weather_full_entity.freezed.dart │ │ │ ├── application │ │ │ │ ├── notifiers │ │ │ │ │ ├── weather_notifier.g.dart │ │ │ │ │ └── weather_notifier.dart │ │ │ │ ├── providers.dart │ │ │ │ └── providers.g.dart │ │ │ └── infrastructure │ │ │ │ ├── repository │ │ │ │ └── weather_repository.dart │ │ │ │ └── dto │ │ │ │ └── weather_model │ │ │ │ ├── weather_dto.dart │ │ │ │ ├── weather_dto.g.dart │ │ │ │ └── weather_dto.freezed.dart │ │ ├── location │ │ │ ├── domain │ │ │ │ └── base_repositories │ │ │ │ │ └── base_location_repository.dart │ │ │ ├── application │ │ │ │ ├── providers.dart │ │ │ │ ├── notifiers │ │ │ │ │ ├── location_notifier.g.dart │ │ │ │ │ └── location_notifier.dart │ │ │ │ └── providers.g.dart │ │ │ ├── infrastructure │ │ │ │ └── location_repository.dart │ │ │ └── presentation │ │ │ │ ├── widgets │ │ │ │ └── success_location_widget.dart │ │ │ │ └── screen │ │ │ │ └── initial_location_screen.dart │ │ └── common │ │ │ ├── states │ │ │ └── api_state.dart │ │ │ ├── providers.dart │ │ │ └── providers.g.dart │ ├── core │ │ ├── networking │ │ │ ├── url_config.dart │ │ │ ├── freezed_string_converter.dart │ │ │ ├── network_api_service.dart │ │ │ ├── interceptors │ │ │ │ ├── refresh_token_interceptor_helper.dart │ │ │ │ ├── cancel_interceptor.dart │ │ │ │ ├── api_interceptor.dart │ │ │ │ └── logging_interceptor.dart │ │ │ ├── network_misc.dart │ │ │ └── network_api_service.g.dart │ │ ├── failure │ │ │ ├── failure_mapper.dart │ │ │ ├── app_failure.dart │ │ │ ├── local_failure.dart │ │ │ ├── local_failure.freezed.dart │ │ │ ├── app_failure.freezed.dart │ │ │ ├── failure_handler.dart │ │ │ ├── network_failure.dart │ │ │ ├── network_failure.freezed.dart │ │ │ └── readme.md │ │ ├── helper │ │ │ ├── colored_logger.dart │ │ │ └── extensions.dart │ │ └── utils │ │ │ └── permission_manager.dart │ └── routes │ │ ├── router.dart │ │ └── router.gr.dart └── main.dart ├── ios ├── 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 ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore └── Podfile ├── .vscode └── settings.json ├── assets └── images │ ├── day.jpg │ ├── night.jpg │ ├── ClearDay.png │ └── ClearNight.png ├── readme_images ├── image.png ├── image-1.png ├── image-2.png ├── image-3.png ├── image-4.png └── figma_image.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── weather_app │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── .metadata ├── .gitignore ├── test └── widget_test.dart ├── .github └── workflows │ └── dart.yml ├── analysis_options.yaml ├── pubspec.yaml └── README.md /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.24.3" 3 | } -------------------------------------------------------------------------------- /lib/src/feature/weather/presentation/constants.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/versions/3.24.3" 3 | } -------------------------------------------------------------------------------- /assets/images/day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/assets/images/day.jpg -------------------------------------------------------------------------------- /assets/images/night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/assets/images/night.jpg -------------------------------------------------------------------------------- /readme_images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/readme_images/image.png -------------------------------------------------------------------------------- /assets/images/ClearDay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/assets/images/ClearDay.png -------------------------------------------------------------------------------- /readme_images/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/readme_images/image-1.png -------------------------------------------------------------------------------- /readme_images/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/readme_images/image-2.png -------------------------------------------------------------------------------- /readme_images/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/readme_images/image-3.png -------------------------------------------------------------------------------- /readme_images/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/readme_images/image-4.png -------------------------------------------------------------------------------- /assets/images/ClearNight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/assets/images/ClearNight.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /readme_images/figma_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/readme_images/figma_image.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meshkat-Shadik/WeatherApp/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/Meshkat-Shadik/WeatherApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/weather_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.weather_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/core/networking/url_config.dart: -------------------------------------------------------------------------------- 1 | const String imgBaseUrl = 'http://openweathermap.org/img/wn/'; 2 | const String apiKey = '126a1bfa19c59da5f3fcb88d289614c5'; 3 | const String appBaseUrl = 'https://api.openweathermap.org/data/2.5'; 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/src/feature/location/domain/base_repositories/base_location_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:geocoding/geocoding.dart'; 2 | import 'package:geolocator/geolocator.dart'; 3 | 4 | abstract class LocationRepository { 5 | Future getCoordinates(); 6 | Future getLocationName(double longitude, double latitude); 7 | } 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: d79295af24c3ed621c33713ecda14ad196fd9c31 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/base_repositories/base_weather_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | import 'package:weather_app/src/core/failure/app_failure.dart'; 3 | import 'package:weather_app/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart'; 4 | 5 | abstract class WeatherRepository { 6 | Future> getWeather(String cityName); 7 | } 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } -------------------------------------------------------------------------------- /lib/src/core/networking/freezed_string_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | class StringConverter implements JsonConverter { 4 | const StringConverter(); 5 | 6 | @override 7 | String fromJson(dynamic json) { 8 | return json.toString(); 9 | } 10 | 11 | @override 12 | dynamic toJson(String object) { 13 | return object; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/feature/location/application/providers.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | import 'package:weather_app/src/feature/location/domain/base_repositories/base_location_repository.dart'; 4 | import 'package:weather_app/src/feature/location/infrastructure/location_repository.dart'; 5 | 6 | part 'providers.g.dart'; 7 | 8 | @riverpod 9 | LocationRepository getLocationRepository(GetLocationRepositoryRef ref) { 10 | return LocationRepositoryImpl(); 11 | } 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/feature/common/states/api_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:weather_app/src/core/failure/app_failure.dart'; 3 | part 'api_state.freezed.dart'; 4 | 5 | @freezed 6 | sealed class ApiRequestState with _$ApiRequestState { 7 | const factory ApiRequestState.idle() = IDLE; 8 | 9 | const factory ApiRequestState.loading() = LOADING; 10 | 11 | const factory ApiRequestState.data({required T data}) = DATA; 12 | 13 | const factory ApiRequestState.failed({required AppFailure reason}) = FAILED; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/core/failure/failure_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:weather_app/src/core/failure/app_failure.dart'; 3 | import 'package:weather_app/src/core/failure/local_failure.dart'; 4 | import 'package:weather_app/src/core/failure/network_failure.dart'; 5 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 6 | 7 | class FailureMapper { 8 | static AppFailure getFailures(Exception error) { 9 | if (error is DioException) { 10 | ColoredLogger.Red.log("DioException: $error"); 11 | return NetworkFailure.getDioException(error); 12 | } else { 13 | return LocalFailure.fromException(error); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/core/networking/network_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:retrofit/retrofit.dart'; 2 | import 'package:dio/dio.dart'; 3 | 4 | import 'package:weather_app/src/core/networking/url_config.dart'; 5 | import 'package:weather_app/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart'; 6 | 7 | part 'network_api_service.g.dart'; 8 | 9 | @RestApi(baseUrl: appBaseUrl) 10 | abstract class NetworkApiService { 11 | factory NetworkApiService(Dio dio, {String baseUrl}) = _NetworkApiService; 12 | 13 | @GET("/weather") 14 | Future getWeather( 15 | @Query("q") String cityName, 16 | @Query("appid") String apiKey, 17 | @Query("units") String units, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /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/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /lib/src/routes/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:weather_app/src/feature/location/presentation/screen/initial_location_screen.dart'; 3 | import 'package:weather_app/src/feature/weather/presentation/screen/weather_infromation_screen.dart'; 4 | 5 | part 'router.gr.dart'; 6 | 7 | @AutoRouterConfig() 8 | class AppRouter extends RootStackRouter { 9 | @override 10 | RouteType get defaultRouteType => 11 | const RouteType.adaptive(); //.cupertino, .adaptive ..etc 12 | 13 | @override 14 | List get routes => [ 15 | AutoRoute(page: InitialLocationRoute.page, initial: true), 16 | AutoRoute(page: WeatherInformationRoute.page), 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/core/failure/app_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'app_failure.freezed.dart'; 4 | 5 | @Freezed(copyWith: false) 6 | abstract class AppFailure implements _$AppFailure, Exception { 7 | const AppFailure._(); 8 | 9 | const factory AppFailure({ 10 | required String message, 11 | required String name, 12 | int? code, 13 | String? uriPath, 14 | }) = _AppFailure; 15 | } 16 | 17 | extension AppFailureCopyWithX on AppFailure { 18 | AppFailure copyWith({ 19 | String? message, 20 | String? name, 21 | }) { 22 | return AppFailure( 23 | message: message ?? this.message, 24 | name: name ?? this.name, 25 | code: code, 26 | uriPath: uriPath, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/core/helper/colored_logger.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'dart:developer' as dev_log show log; 4 | 5 | enum ColoredLogger { 6 | Black("30", "⚫️"), 7 | Red("31", "🔴"), 8 | Green("32", "🟢"), 9 | Yellow("33", "🟡"), 10 | Blue("34", "🔵"), 11 | Magenta("35", "🟣"), 12 | White("37", "⚪️"); 13 | 14 | final String code; 15 | final String emoji; 16 | const ColoredLogger(this.code, this.emoji); 17 | 18 | static const maxWidth = 90; 19 | 20 | void log(dynamic text) => 21 | dev_log.log('\x1B[${code}m$emoji$text$emoji\x1B[0m'); 22 | String get emojiStart => '$emoji \x1B[${code}m'; 23 | String get emojiEnd => ' $emoji \x1B[0m'; 24 | String get normalStart => '\x1B[${code}m'; 25 | String get normalEnd => '\x1B[0m'; 26 | } 27 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:weather_app/src/feature/common/providers.dart'; 4 | 5 | void main() { 6 | runApp( 7 | ProviderScope( 8 | child: MyApp(), 9 | ), 10 | ); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Consumer(builder: (context, ref, _) { 17 | return MaterialApp.router( 18 | debugShowCheckedModeBanner: false, 19 | theme: ThemeData(primarySwatch: Colors.blue), 20 | routerDelegate: ref.watch(appRouterProvider).delegate(), 21 | routeInformationParser: 22 | ref.watch(appRouterProvider).defaultRouteParser(), 23 | ); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/core/helper/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | extension TimeEx on int { 6 | String get toFormattedTime => DateFormat('hh:mm a') 7 | .format(DateTime.fromMillisecondsSinceEpoch(this * 1000)) 8 | .toString(); 9 | } 10 | 11 | extension SizeX on BuildContext { 12 | double get height => MediaQuery.of(this).size.height; 13 | double get width => MediaQuery.of(this).size.width; 14 | } 15 | 16 | extension StringX on String { 17 | void toast(bool isSuccessful) { 18 | Fluttertoast.showToast( 19 | msg: this, 20 | gravity: ToastGravity.BOTTOM, 21 | timeInSecForIosWeb: 1, 22 | backgroundColor: 23 | isSuccessful ? Colors.teal.shade600 : Colors.red.shade300, 24 | textColor: Colors.white, 25 | fontSize: 13.0, 26 | toastLength: Toast.LENGTH_LONG, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/feature/location/infrastructure/location_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:geocoding/geocoding.dart'; 2 | import 'package:geolocator/geolocator.dart'; 3 | import 'package:weather_app/src/feature/location/domain/base_repositories/base_location_repository.dart'; 4 | 5 | class LocationRepositoryImpl implements LocationRepository { 6 | @override 7 | Future getCoordinates() async { 8 | Position position = await Geolocator.getCurrentPosition( 9 | locationSettings: AndroidSettings( 10 | accuracy: LocationAccuracy.best, 11 | distanceFilter: 10, 12 | timeLimit: Duration(seconds: 4)), 13 | ); 14 | return position; 15 | } 16 | 17 | @override 18 | Future getLocationName(double latitude, double longitude) async { 19 | List placemarks = 20 | await placemarkFromCoordinates(latitude, longitude); 21 | Placemark place = placemarks[0]; 22 | return place; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version "7.3.0" apply false 27 | id "org.jetbrains.kotlin.android" version "1.9.0" apply false 28 | } 29 | 30 | include ":app" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/application/states/credentials.dart 2 | 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Web related 37 | lib/generated_plugin_registrant.dart 38 | 39 | # Symbolication related 40 | app.*.symbols 41 | 42 | # Obfuscation related 43 | app.*.map.json 44 | 45 | # Android Studio will place build artifacts here 46 | /android/app/debug 47 | /android/app/profile 48 | /android/app/release 49 | 50 | # FVM Version Cache 51 | .fvm/ -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/src/feature/location/application/notifiers/location_notifier.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'location_notifier.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$locationNotifierHash() => r'edf403ca47e1ee39f143413d388fde8909d58709'; 10 | 11 | /// See also [LocationNotifier]. 12 | @ProviderFor(LocationNotifier) 13 | final locationNotifierProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | LocationNotifier.new, 16 | name: r'locationNotifierProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$locationNotifierHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$LocationNotifier = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /lib/src/feature/location/application/providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$getLocationRepositoryHash() => 10 | r'a2fe8f253b24c1c6cd8cc00825c0fdf2938a1073'; 11 | 12 | /// See also [getLocationRepository]. 13 | @ProviderFor(getLocationRepository) 14 | final getLocationRepositoryProvider = 15 | AutoDisposeProvider.internal( 16 | getLocationRepository, 17 | name: r'getLocationRepositoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$getLocationRepositoryHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef GetLocationRepositoryRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 28 | -------------------------------------------------------------------------------- /lib/src/feature/weather/application/notifiers/weather_notifier.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'weather_notifier.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$weatherNotifierHash() => r'8250bac3f54ed517c647bc7451d8824c9dd04560'; 10 | 11 | /// See also [WeatherNotifier]. 12 | @ProviderFor(WeatherNotifier) 13 | final weatherNotifierProvider = NotifierProvider>.internal( 15 | WeatherNotifier.new, 16 | name: r'weatherNotifierProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$weatherNotifierHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$WeatherNotifier = Notifier>; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /lib/src/feature/weather/application/providers.dart: -------------------------------------------------------------------------------- 1 | //independent sources 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | import 'package:weather_app/src/feature/common/providers.dart'; 4 | import 'package:weather_app/src/feature/location/application/notifiers/location_notifier.dart'; 5 | import 'package:weather_app/src/feature/weather/domain/base_repositories/base_weather_repository.dart'; 6 | import 'package:weather_app/src/feature/weather/infrastructure/repository/weather_repository.dart'; 7 | 8 | part 'providers.g.dart'; 9 | 10 | @riverpod 11 | WeatherRepository getWeatherRepository(GetWeatherRepositoryRef ref) { 12 | final apiService = ref.read(apiServiceProvider); 13 | return WeatherRepositoryImpl(apiService: apiService); 14 | } 15 | 16 | @riverpod 17 | class GetCityName extends _$GetCityName { 18 | @override 19 | String build() { 20 | final locationState = ref.watch(locationNotifierProvider); 21 | return locationState.maybeWhen( 22 | data: (cityName) => cityName, 23 | orElse: () => '', 24 | ); 25 | } 26 | 27 | void setCityName(String cityName) { 28 | state = cityName; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:weather_app/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Clone repository 15 | uses: actions/checkout@v4 16 | 17 | # Set up Java 17 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: "temurin" # This is an OpenJDK distribution 22 | java-version: "17" 23 | 24 | # Set up Flutter 25 | - name: Set up Flutter 26 | uses: subosito/flutter-action@v2 27 | with: 28 | channel: stable 29 | flutter-version: 3.24.2 30 | 31 | - run: flutter --version 32 | - run: flutter pub get 33 | 34 | # Build APK for debugging 35 | - run: flutter build apk --debug --split-per-abi -t lib/main.dart 36 | - run: flutter build appbundle 37 | 38 | # Push the release 39 | - name: Push to Releases 40 | uses: ncipollo/release-action@v1 41 | with: 42 | artifacts: "build/app/outputs/apk/debug/*" 43 | tag: v1.0.${{ github.run_number }} 44 | token: ${{ secrets.TOKEN }} 45 | -------------------------------------------------------------------------------- /lib/src/feature/weather/application/notifiers/weather_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 4 | import 'package:weather_app/src/feature/common/states/api_state.dart'; 5 | import 'package:weather_app/src/feature/weather/application/providers.dart'; 6 | import 'package:weather_app/src/feature/weather/domain/entity/weather_full_entity.dart'; 7 | 8 | part 'weather_notifier.g.dart'; 9 | 10 | @Riverpod(keepAlive: true) 11 | class WeatherNotifier extends _$WeatherNotifier { 12 | //build 13 | @override 14 | ApiRequestState build() => ApiRequestState.idle(); 15 | 16 | //getWeather 17 | Future getWeather(String cityName) async { 18 | state = ApiRequestState.loading(); 19 | ColoredLogger.Green.log('Getting weather for $cityName'); 20 | final repo = ref.read(getWeatherRepositoryProvider); 21 | var data = await repo.getWeather(cityName); 22 | ColoredLogger.Green.log('Got weather for $cityName'); 23 | 24 | state = switch (data) { 25 | Left(value: final l) => ApiRequestState.failed(reason: l), 26 | Right(value: final r) => ApiRequestState.data( 27 | data: WeatherFullEntity.fromDTO(r), 28 | ), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/feature/weather/infrastructure/repository/weather_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | import 'package:weather_app/src/core/failure/app_failure.dart'; 3 | import 'package:weather_app/src/core/failure/failure_mapper.dart'; 4 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 5 | import 'package:weather_app/src/core/networking/network_api_service.dart'; 6 | import 'package:weather_app/src/core/networking/url_config.dart'; 7 | import 'package:weather_app/src/feature/weather/domain/base_repositories/base_weather_repository.dart'; 8 | import 'package:weather_app/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart'; 9 | 10 | class WeatherRepositoryImpl implements WeatherRepository { 11 | final NetworkApiService _apiService; 12 | WeatherRepositoryImpl({required NetworkApiService apiService}) 13 | : _apiService = apiService; 14 | 15 | @override 16 | Future> getWeather( 17 | String cityName, 18 | ) async { 19 | try { 20 | final response = await _apiService.getWeather(cityName, apiKey, "metric"); 21 | ColoredLogger.Green.log("Response: $response"); 22 | return Right(response); 23 | } on Exception catch (e) { 24 | ColoredLogger.Red.log("Exception: $e"); 25 | return Left(FailureMapper.getFailures(e)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/feature/common/providers.dart: -------------------------------------------------------------------------------- 1 | //dio providers 2 | import 'package:dio/dio.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | import 'package:weather_app/src/core/networking/interceptors/api_interceptor.dart'; 5 | import 'package:weather_app/src/core/networking/interceptors/logging_interceptor.dart'; 6 | import 'package:weather_app/src/core/networking/network_api_service.dart'; 7 | import 'package:weather_app/src/core/networking/url_config.dart'; 8 | import 'package:weather_app/src/routes/router.dart'; 9 | 10 | part 'providers.g.dart'; 11 | 12 | @riverpod 13 | Dio dio(DioRef ref) { 14 | return Dio(BaseOptions( 15 | baseUrl: appBaseUrl, 16 | connectTimeout: const Duration(seconds: 10), 17 | receiveTimeout: const Duration(seconds: 10), 18 | sendTimeout: const Duration(seconds: 10), 19 | validateStatus: (status) => true, 20 | receiveDataWhenStatusError: true, 21 | )) 22 | ..interceptors.add( 23 | PrettyDioLogger( 24 | responseBody: true, 25 | ), 26 | ) 27 | ..interceptors.add(ApiInterceptor(ref)); 28 | } 29 | 30 | @riverpod 31 | NetworkApiService apiService(ApiServiceRef ref) { 32 | final dio = ref.read(dioProvider); 33 | return NetworkApiService(dio, baseUrl: appBaseUrl); 34 | } 35 | 36 | @riverpod 37 | AppRouter appRouter(AppRouterRef ref) { 38 | return AppRouter(); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/routes/router.gr.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // AutoRouterGenerator 5 | // ************************************************************************** 6 | 7 | // ignore_for_file: type=lint 8 | // coverage:ignore-file 9 | 10 | part of 'router.dart'; 11 | 12 | /// generated route for 13 | /// [InitialLocationScreen] 14 | class InitialLocationRoute extends PageRouteInfo { 15 | const InitialLocationRoute({List? children}) 16 | : super( 17 | InitialLocationRoute.name, 18 | initialChildren: children, 19 | ); 20 | 21 | static const String name = 'InitialLocationRoute'; 22 | 23 | static PageInfo page = PageInfo( 24 | name, 25 | builder: (data) { 26 | return const InitialLocationScreen(); 27 | }, 28 | ); 29 | } 30 | 31 | /// generated route for 32 | /// [WeatherInformationScreen] 33 | class WeatherInformationRoute extends PageRouteInfo { 34 | const WeatherInformationRoute({List? children}) 35 | : super( 36 | WeatherInformationRoute.name, 37 | initialChildren: children, 38 | ); 39 | 40 | static const String name = 'WeatherInformationRoute'; 41 | 42 | static PageInfo page = PageInfo( 43 | name, 44 | builder: (data) { 45 | return const WeatherInformationScreen(); 46 | }, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/src/feature/location/presentation/widgets/success_location_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:weather_app/src/feature/weather/application/providers.dart'; 5 | import 'package:weather_app/src/feature/weather/presentation/styles.dart'; 6 | import 'package:weather_app/src/routes/router.dart'; 7 | 8 | class BuildSucessLocation extends ConsumerWidget { 9 | final String cityName; 10 | 11 | const BuildSucessLocation({ 12 | super.key, 13 | required this.cityName, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | return Column( 19 | children: [ 20 | Text(cityName, style: bigTitleStyle.copyWith(fontSize: 25)), 21 | const SizedBox(height: 20), 22 | ElevatedButton( 23 | child: Text( 24 | 'Tap to see more!', 25 | style: TextStyle(color: Colors.white54), 26 | ), 27 | onPressed: () { 28 | //we are setting the city name in the provider and then pushing the route 29 | //later we will use this city name to fetch the weather data 30 | ref.read(getCityNameProvider.notifier).setCityName(cityName); 31 | WidgetsBinding.instance.addPostFrameCallback((_) { 32 | context.router.push(WeatherInformationRoute()); 33 | }); 34 | }, 35 | style: ElevatedButton.styleFrom( 36 | backgroundColor: Colors.white10, padding: EdgeInsets.all(15)), 37 | ), 38 | ], 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | 11 | analyzer: 12 | exclude: 13 | - "**/*.g.dart" 14 | - "**/*.gr.dart" 15 | - "**/*.config.dart" 16 | - "**/*.freezed.dart" 17 | 18 | linter: 19 | # The lint rules applied to this project can be customized in the 20 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 21 | # included above or to enable additional rules. A list of all available lints 22 | # and their documentation is published at 23 | # https://dart-lang.github.io/linter/lints/index.html. 24 | # 25 | # Instead of disabling a lint rule for the entire project in the 26 | # section below, it can also be suppressed for a single line of code 27 | # or a specific dart file by using the `// ignore: name_of_lint` and 28 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 29 | # producing the lint. 30 | rules: 31 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 32 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 33 | # Additional information about this file can be found at 34 | # https://dart.dev/guides/language/analysis-options 35 | -------------------------------------------------------------------------------- /lib/src/feature/weather/application/providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$getWeatherRepositoryHash() => 10 | r'1db0fbf80dc0d73a682663a21e45f3aad4db64f8'; 11 | 12 | /// See also [getWeatherRepository]. 13 | @ProviderFor(getWeatherRepository) 14 | final getWeatherRepositoryProvider = 15 | AutoDisposeProvider.internal( 16 | getWeatherRepository, 17 | name: r'getWeatherRepositoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$getWeatherRepositoryHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef GetWeatherRepositoryRef = AutoDisposeProviderRef; 26 | String _$getCityNameHash() => r'26a8657e4ea2a363fa612b14fafd8825d3584aa2'; 27 | 28 | /// See also [GetCityName]. 29 | @ProviderFor(GetCityName) 30 | final getCityNameProvider = 31 | AutoDisposeNotifierProvider.internal( 32 | GetCityName.new, 33 | name: r'getCityNameProvider', 34 | debugGetCreateSourceHash: 35 | const bool.fromEnvironment('dart.vm.product') ? null : _$getCityNameHash, 36 | dependencies: null, 37 | allTransitiveDependencies: null, 38 | ); 39 | 40 | typedef _$GetCityName = AutoDisposeNotifier; 41 | // ignore_for_file: type=lint 42 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 43 | -------------------------------------------------------------------------------- /lib/src/feature/weather/presentation/widgets/detail_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weather_app/src/feature/weather/presentation/styles.dart'; 3 | 4 | class DetailBlock extends StatelessWidget { 5 | const DetailBlock({ 6 | Key? key, 7 | required this.icon, 8 | required this.title, 9 | required this.value, 10 | required this.color, 11 | this.unit, 12 | }) : super(key: key); 13 | 14 | final Icon? icon; 15 | final String? title; 16 | final String? value; 17 | final Color? color; 18 | final String? unit; 19 | @override 20 | Widget build(BuildContext context) { 21 | return Container( 22 | padding: const EdgeInsets.all(8), 23 | child: Column( 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | children: [ 26 | icon ?? Container(), 27 | Text( 28 | title ?? "", 29 | style: weatherDetailsTextStyle.copyWith(fontSize: 18), 30 | ), 31 | Row( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | textBaseline: TextBaseline.ideographic, 34 | crossAxisAlignment: CrossAxisAlignment.baseline, 35 | children: [ 36 | Text( 37 | value ?? "", 38 | style: weatherDetailsTextStyle.copyWith( 39 | fontWeight: FontWeight.bold), 40 | ), 41 | SizedBox(width: 5), 42 | Text( 43 | unit ?? "", 44 | style: weatherDetailsTextStyle.copyWith(fontSize: 12), 45 | ), 46 | ], 47 | ), 48 | ], 49 | ), 50 | color: color, 51 | ); 52 | } 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 | -------------------------------------------------------------------------------- /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 | weather_app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/src/core/networking/interceptors/refresh_token_interceptor_helper.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: body_might_complete_normally_nullable 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | import 'package:weather_app/src/core/networking/network_misc.dart'; 7 | 8 | class RefreshTokenInterceptorHelper { 9 | static Future> retry( 10 | Dio dio, 11 | RequestOptions requestOptions, 12 | ) async { 13 | final options = Options( 14 | method: requestOptions.method, 15 | headers: requestOptions.headers, 16 | ); 17 | return dio.request( 18 | requestOptions.path, 19 | data: requestOptions.data, 20 | queryParameters: requestOptions.queryParameters, 21 | options: options, 22 | ); 23 | } 24 | 25 | static Future refreshTokenRequest({ 26 | required Dio tokenDio, 27 | required JSON data, 28 | required Ref ref, 29 | }) async { 30 | debugPrint('--> REFRESHING TOKEN'); 31 | try { 32 | //handle refresh token request 33 | } on DioException catch (_) { 34 | //if token refresh fails, clear dio and return null 35 | return null; 36 | } 37 | } 38 | 39 | //refresh sessionId request 40 | static Future refreshSessionIdRequest({ 41 | required Dio tokenDio, 42 | required String token, 43 | required Ref ref, 44 | }) async { 45 | debugPrint('--> REFRESHING SESSION ID'); 46 | try { 47 | //handle refresh token request 48 | } on DioException catch (_) { 49 | //if token refresh fails, clear dio and return null 50 | return null; 51 | } 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/feature/location/application/notifiers/location_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:geocoding/geocoding.dart'; 2 | import 'package:geolocator/geolocator.dart'; 3 | import 'package:permission_handler/permission_handler.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | import 'package:weather_app/src/core/failure/failure_mapper.dart'; 6 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 7 | import 'package:weather_app/src/core/utils/permission_manager.dart'; 8 | import 'package:weather_app/src/feature/common/states/api_state.dart'; 9 | import 'package:weather_app/src/feature/location/application/providers.dart'; 10 | 11 | part 'location_notifier.g.dart'; 12 | 13 | @riverpod 14 | class LocationNotifier extends _$LocationNotifier { 15 | ApiRequestState build() => ApiRequestState.idle(); 16 | 17 | Future getMyLocation() async { 18 | try { 19 | await PermissionManager.requestPermissions( 20 | [Permission.location], // list of permissions to request 21 | () async { 22 | // callback to be executed if all permissions are granted 23 | state = ApiRequestState.loading(); 24 | final repo = ref.read(getLocationRepositoryProvider); 25 | Position data = await repo.getCoordinates(); 26 | ColoredLogger.Green.log(data.toString()); 27 | Placemark place = 28 | await repo.getLocationName(data.latitude, data.longitude); 29 | String address = "${place.locality}, ${place.country}"; 30 | state = ApiRequestState.data(data: address); 31 | }, 32 | ); 33 | } on Exception catch (e) { 34 | state = ApiRequestState.failed(reason: FailureMapper.getFailures(e)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/core/networking/interceptors/cancel_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 3 | 4 | class CancelInterceptor extends Interceptor { 5 | final List _ignoredEndpoints; 6 | 7 | final _cancelTokens = {}; 8 | final _timestamps = {}; 9 | 10 | // Create a list of regular expressions from the _ignoredEndpoints list 11 | late final List _ignoredRegexps; 12 | 13 | CancelInterceptor.withIgnoredEndpoints(this._ignoredEndpoints) { 14 | // Initialize the _ignoredRegexps variable in the constructor body 15 | _ignoredRegexps = 16 | _ignoredEndpoints.map((endpoint) => RegExp(endpoint)).toList(); 17 | } 18 | 19 | @override 20 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 21 | final key = options.baseUrl + options.path; 22 | 23 | // Check if the endpoint matches any of the regular expressions in the _ignoredRegexps list 24 | if (!(_ignoredRegexps.any((regexp) => regexp.hasMatch(key)))) { 25 | // Cancel the previous request if it was made less than 500ms ago 26 | if (_timestamps.containsKey(key) && 27 | DateTime.now().difference(_timestamps[key]!) < 28 | const Duration(milliseconds: 500)) { 29 | _cancelTokens[key]?.cancel(); 30 | ColoredLogger.White.log('❕❕❌🚫Cancelled previous request to $key🚫❌❕❕'); 31 | } 32 | 33 | // Create a new CancelToken for the new request 34 | _cancelTokens[key] = CancelToken(); 35 | options.cancelToken = _cancelTokens[key]; 36 | 37 | // Update the timestamp for the new request 38 | _timestamps[key] = DateTime.now(); 39 | } 40 | 41 | super.onRequest(options, handler); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/entity/weather_detail_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:weather_app/src/core/helper/extensions.dart'; 3 | import 'package:weather_app/src/feature/weather/domain/entity/weather_full_entity.dart'; 4 | import 'package:weather_app/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart'; 5 | 6 | part 'weather_detail_entity.freezed.dart'; 7 | 8 | @freezed 9 | sealed class WeatherDetailEntity implements _$WeatherDetailEntity { 10 | const WeatherDetailEntity._(); 11 | 12 | const factory WeatherDetailEntity({ 13 | required String? windValue, 14 | required String? humidityValue, 15 | required String? gustValue, 16 | required String? pressureValue, 17 | required String? sunriseValue, 18 | required String? sunsetValue, 19 | }) = _WeatherDetailEntity; 20 | 21 | factory WeatherDetailEntity.fromDTO(WeatherDTO weatherData) { 22 | return WeatherDetailEntity( 23 | windValue: weatherData.wind?.speed.toString(), 24 | humidityValue: weatherData.main?.humidity.toString(), 25 | gustValue: weatherData.wind?.gust.toString(), 26 | pressureValue: weatherData.main?.pressure.toString(), 27 | sunriseValue: (weatherData.sys?.sunrise! ?? 0).toFormattedTime, 28 | sunsetValue: (weatherData.sys?.sunset! ?? 0).toFormattedTime, 29 | ); 30 | } 31 | 32 | factory WeatherDetailEntity.fromFullEntity(WeatherFullEntity weatherData) { 33 | return WeatherDetailEntity( 34 | windValue: weatherData.wind?.speed.toString(), 35 | humidityValue: weatherData.main?.humidity.toString(), 36 | gustValue: weatherData.wind?.gust.toString(), 37 | pressureValue: weatherData.main?.pressure.toString(), 38 | sunriseValue: weatherData.sys?.sunrise?.toFormattedTime, 39 | sunsetValue: weatherData.sys?.sunset?.toFormattedTime, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/feature/weather/presentation/widgets/basic_weather_information.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weather_app/src/feature/weather/domain/entity/weather_basic_entity.dart'; 3 | import 'package:weather_app/src/feature/weather/presentation/styles.dart'; 4 | 5 | class BasicWeatherInformation extends StatelessWidget { 6 | const BasicWeatherInformation({ 7 | Key? key, 8 | required this.data, 9 | }) : super(key: key); 10 | 11 | final WeatherBasicEntity data; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Column( 16 | children: [ 17 | const SizedBox(height: 10), 18 | CircleAvatar( 19 | radius: 40, 20 | backgroundColor: Colors.transparent, 21 | backgroundImage: 22 | NetworkImage(data.imgUrl ?? "http://via.placeholder.com/200x150"), 23 | ), 24 | Text( 25 | data.cityName.toString(), 26 | style: bigTitleStyle.copyWith(fontSize: 38), 27 | ), 28 | Row( 29 | mainAxisAlignment: MainAxisAlignment.center, 30 | children: [ 31 | Text( 32 | '${data.condition}, ', 33 | style: bigTitleStyle.copyWith(fontSize: 16), 34 | ), 35 | Text( 36 | data.dateTime ?? "", 37 | style: bigTitleStyle.copyWith(fontSize: 18), 38 | ) 39 | ], 40 | ), 41 | Text( 42 | data.description ?? "", 43 | style: bigTitleStyle.copyWith(fontSize: 16), 44 | ), 45 | Text( 46 | data.temp.toString() + "°", 47 | style: bigTitleStyle.copyWith(fontSize: 100), 48 | ), 49 | Text( 50 | "Feels Like : " + data.feelsLike.toString() + "°", 51 | style: bigTitleStyle.copyWith(fontSize: 16), 52 | ) 53 | // 54 | ], 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:weather_app/src/core/networking/freezed_string_converter.dart'; 3 | 4 | part 'weather_dto.freezed.dart'; 5 | part 'weather_dto.g.dart'; 6 | 7 | @freezed 8 | abstract class WeatherDTO with _$WeatherDTO { 9 | const factory WeatherDTO({ 10 | @required List? weather, 11 | @required Main? main, 12 | @required Wind? wind, 13 | @required int? dt, 14 | @required Sys? sys, 15 | @required String? name, 16 | @StringConverter() @required String? cod, 17 | }) = _WeatherDTO; 18 | 19 | factory WeatherDTO.fromJson(Map json) => 20 | _$WeatherDTOFromJson(json); 21 | } 22 | 23 | @freezed 24 | abstract class Main with _$Main { 25 | const factory Main({ 26 | @required double? temp, 27 | @required double? feelsLike, 28 | @required int? pressure, 29 | @required int? humidity, 30 | }) = _Main; 31 | 32 | factory Main.fromJson(Map json) => _$MainFromJson(json); 33 | } 34 | 35 | @freezed 36 | abstract class Sys with _$Sys { 37 | const factory Sys({ 38 | @required int? sunrise, 39 | @required int? sunset, 40 | }) = _Sys; 41 | 42 | factory Sys.fromJson(Map json) => _$SysFromJson(json); 43 | } 44 | 45 | @freezed 46 | abstract class Weather with _$Weather { 47 | const factory Weather({ 48 | @required String? main, 49 | @required String? description, 50 | @required String? icon, 51 | }) = _Weather; 52 | 53 | factory Weather.fromJson(Map json) => 54 | _$WeatherFromJson(json); 55 | } 56 | 57 | @freezed 58 | abstract class Wind with _$Wind { 59 | const factory Wind({ 60 | @required double? speed, 61 | @required double? gust, 62 | }) = _Wind; 63 | 64 | factory Wind.fromJson(Map json) => _$WindFromJson(json); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/core/utils/permission_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:device_info_plus/device_info_plus.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | 6 | class PermissionManager { 7 | PermissionManager._(); 8 | 9 | static Future _checkStoragePermission() async { 10 | PermissionStatus status; 11 | if (Platform.isAndroid) { 12 | final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); 13 | final AndroidDeviceInfo info = await deviceInfoPlugin.androidInfo; 14 | if ((info.version.sdkInt) >= 33) { 15 | status = PermissionStatus.granted; 16 | } else { 17 | status = await Permission.storage.request(); 18 | } 19 | } else { 20 | status = await Permission.storage.request(); 21 | } 22 | 23 | return status; 24 | } 25 | 26 | static Future requestPermissions( 27 | List permissions, Future Function() callback, 28 | {Future Function()? deniedCallback}) async { 29 | for (final permission in permissions) { 30 | var status = await permission.status; 31 | if (permission == Permission.storage) { 32 | status = await _checkStoragePermission(); 33 | } 34 | if (!status.isGranted) { 35 | status = await permission.request(); 36 | if (!status.isGranted) { 37 | if (status.isPermanentlyDenied) { 38 | openAppSettings(); 39 | } else { 40 | return await requestPermissions(permissions, callback, 41 | deniedCallback: deniedCallback); 42 | } 43 | if (deniedCallback != null) { 44 | return await deniedCallback(); 45 | } 46 | // Return early if permission is not granted. 47 | return null; 48 | } 49 | } 50 | } 51 | // Only call the callback once, after all permissions have been checked. 52 | return await callback(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/feature/common/providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$dioHash() => r'2a7d60fda0e2712c913d48da0c2790865a30cafd'; 10 | 11 | /// See also [dio]. 12 | @ProviderFor(dio) 13 | final dioProvider = AutoDisposeProvider.internal( 14 | dio, 15 | name: r'dioProvider', 16 | debugGetCreateSourceHash: 17 | const bool.fromEnvironment('dart.vm.product') ? null : _$dioHash, 18 | dependencies: null, 19 | allTransitiveDependencies: null, 20 | ); 21 | 22 | typedef DioRef = AutoDisposeProviderRef; 23 | String _$apiServiceHash() => r'6018482bf5951f4e2648305a4964157d26c00ab0'; 24 | 25 | /// See also [apiService]. 26 | @ProviderFor(apiService) 27 | final apiServiceProvider = AutoDisposeProvider.internal( 28 | apiService, 29 | name: r'apiServiceProvider', 30 | debugGetCreateSourceHash: 31 | const bool.fromEnvironment('dart.vm.product') ? null : _$apiServiceHash, 32 | dependencies: null, 33 | allTransitiveDependencies: null, 34 | ); 35 | 36 | typedef ApiServiceRef = AutoDisposeProviderRef; 37 | String _$appRouterHash() => r'd4aceb316283cf5b477f18f07d2741ad189dea8f'; 38 | 39 | /// See also [appRouter]. 40 | @ProviderFor(appRouter) 41 | final appRouterProvider = AutoDisposeProvider.internal( 42 | appRouter, 43 | name: r'appRouterProvider', 44 | debugGetCreateSourceHash: 45 | const bool.fromEnvironment('dart.vm.product') ? null : _$appRouterHash, 46 | dependencies: null, 47 | allTransitiveDependencies: null, 48 | ); 49 | 50 | typedef AppRouterRef = AutoDisposeProviderRef; 51 | // ignore_for_file: type=lint 52 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 53 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | 16 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 17 | if (flutterVersionCode == null) { 18 | flutterVersionCode = '1' 19 | } 20 | 21 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 22 | if (flutterVersionName == null) { 23 | flutterVersionName = '1.0' 24 | } 25 | 26 | 27 | android { 28 | namespace "com.example.weather_app" 29 | compileSdkVersion 34 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_17 33 | targetCompatibility JavaVersion.VERSION_17 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '17' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.example.weather_app" 48 | minSdkVersion 24 49 | targetSdkVersion 33 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | multiDexEnabled true 53 | } 54 | 55 | buildTypes { 56 | release { 57 | // TODO: Add your own signing config for the release build. 58 | // Signing with the debug keys for now, so `flutter run --release` works. 59 | signingConfig signingConfigs.debug 60 | } 61 | } 62 | } 63 | 64 | flutter { 65 | source '../..' 66 | } 67 | 68 | // dependencies { 69 | // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 70 | // implementation 'androidx.multidex:multidex:2.0.1' 71 | // } 72 | -------------------------------------------------------------------------------- /lib/src/core/networking/network_misc.dart: -------------------------------------------------------------------------------- 1 | typedef JSON = Map; 2 | 3 | enum StatusCode { 4 | redirectResponse(code: 302, name: 'Redirect Response'), 5 | badRequest(code: 400, name: 'Bad Request'), 6 | unauthorized(code: 401, name: 'Unauthorized'), 7 | forbidden(code: 403, name: 'Forbidden'), 8 | notFound(code: 404, name: 'Not Found'), 9 | requestTimeout(code: 408, name: 'Request Timeout'), 10 | conflict(code: 409, name: 'Conflict'), 11 | tokenExpired(code: 419, name: 'Token Expired'), 12 | unprocessableEntity(code: 422, name: 'Unprocessable Entity'), 13 | internalServerError(code: 500, name: 'Internal Server Error'), 14 | badGateWay(code: 502, name: 'Bad Gateway'), 15 | serviceUnavailable(code: 503, name: 'Service Unavailable'), 16 | gatewayTimeout(code: 504, name: 'Gateway Timeout'), 17 | unrecognized(code: 0, name: 'Unrecognized error'); 18 | 19 | final int code; 20 | final String name; 21 | 22 | const StatusCode({ 23 | required this.code, 24 | required this.name, 25 | }); 26 | } 27 | 28 | StatusCode? getStatusCode(int? code) { 29 | return switch (code) { 30 | 302 => StatusCode.redirectResponse, 31 | 400 => StatusCode.badRequest, 32 | 401 => StatusCode.unauthorized, 33 | 403 => StatusCode.forbidden, 34 | 404 => StatusCode.notFound, 35 | 409 => StatusCode.conflict, 36 | 419 => StatusCode.tokenExpired, 37 | 422 => StatusCode.unprocessableEntity, 38 | 500 => StatusCode.internalServerError, 39 | 502 => StatusCode.badGateWay, 40 | 503 => StatusCode.serviceUnavailable, 41 | 504 => StatusCode.gatewayTimeout, 42 | null => null, 43 | _ => StatusCode.unrecognized, 44 | }; 45 | } 46 | 47 | class NetworkMisc { 48 | const NetworkMisc._(); 49 | 50 | static const tokenField = 'requiresAuthToken'; 51 | 52 | static const needAuthToken = { 53 | 'Content-Type': 'application/json', 54 | tokenField: true 55 | }; 56 | } 57 | 58 | enum ErrorMessage { 59 | nonProducable, 60 | timeOut, 61 | badResponse, 62 | internalServerError, 63 | connectionError, 64 | unknownError, 65 | dataUnavailable, 66 | unrecognizedError, 67 | noInternetConnection, 68 | somethingWentWrong, 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/core/failure/local_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:weather_app/src/core/failure/app_failure.dart'; 3 | 4 | part 'local_failure.freezed.dart'; 5 | 6 | @freezed 7 | class LocalFailure with _$LocalFailure implements AppFailure { 8 | const factory LocalFailure({ 9 | required String name, 10 | required String message, 11 | String? uriPath, 12 | int? code, 13 | }) = _LocalFailure; 14 | 15 | factory LocalFailure.fromException(dynamic e) { 16 | String errorMessage = e.toString(); 17 | String errorName = e.runtimeType.toString(); 18 | if (e is Exception) { 19 | if (e is LocalFailure) { 20 | errorMessage = e.message; 21 | errorName = e.name; 22 | } else { 23 | errorMessage = (e.toString().split(':').lastOrNull ?? '').trim(); 24 | errorName = e.runtimeType.toString() == '_Exception' 25 | ? 'Exception' 26 | : e.runtimeType.toString(); 27 | } 28 | } else { 29 | if (e is AssertionError) { 30 | errorMessage = e.message.toString(); 31 | } else if (e is ArgumentError) { 32 | errorMessage = '${e.message}: ${e.invalidValue.toString()}'; 33 | } else if (e is RangeError) { 34 | errorMessage = '${e.message.toString()}: ${e.invalidValue.toString()}'; 35 | } else if (e is IndexError) { 36 | errorMessage = '${e.message.toString()}: ${e.invalidValue.toString()}'; 37 | } else if (e is NoSuchMethodError) { 38 | errorMessage = e.toString(); 39 | } else if (e is UnsupportedError) { 40 | errorMessage = e.message ?? 'Unsupported operation'; 41 | } else if (e is UnimplementedError) { 42 | errorMessage = e.message ?? 'Unimplemented'; 43 | } else if (e is StateError) { 44 | errorMessage = e.message; 45 | } else if (e is ConcurrentModificationError) { 46 | errorMessage = e.toString(); 47 | } else if (e is OutOfMemoryError) { 48 | errorMessage = 'Out of memory'; 49 | } else if (e is StackOverflowError) { 50 | errorMessage = 'Stack overflow'; 51 | } 52 | } 53 | 54 | return LocalFailure( 55 | name: errorName, 56 | message: errorMessage, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/feature/weather/presentation/styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | final detailsBgDecoration = BoxDecoration( 5 | color: Colors.transparent, 6 | image: DecorationImage( 7 | fit: BoxFit.fill, 8 | image: AssetImage( 9 | 'assets/images/ClearNight.png', 10 | ), 11 | ), 12 | ); 13 | 14 | final detailsBgDecorationWithGradient = BoxDecoration( 15 | color: Colors.white, 16 | gradient: LinearGradient( 17 | begin: FractionalOffset.topCenter, 18 | end: FractionalOffset.bottomCenter, 19 | colors: [ 20 | Colors.black54.withOpacity(0.0), 21 | Colors.black45, 22 | ], 23 | stops: [0.0, 0.5], 24 | ), 25 | ); 26 | 27 | final backButtonDecoration = BoxDecoration( 28 | color: Color(0xffe0e0e0), 29 | boxShadow: backBtnBoxShadow, 30 | borderRadius: BorderRadius.all( 31 | Radius.circular(10), 32 | ), 33 | ); 34 | 35 | const outlineInputBorder = OutlineInputBorder( 36 | borderSide: BorderSide( 37 | color: Colors.white, 38 | ), 39 | borderRadius: BorderRadius.all(Radius.circular(30.0))); 40 | 41 | const hintTextStyle = TextStyle( 42 | color: Colors.white54, 43 | fontWeight: FontWeight.w100, 44 | fontStyle: FontStyle.italic); 45 | 46 | const nightColor = Color(0xff262431); 47 | const dayColor = Color(0xffFDAE1C); 48 | const dayShadowColor = Color(0xff200003); 49 | const nightBackgroundColor = Color(0xff14141C); 50 | 51 | TextStyle bigTitleStyle = 52 | GoogleFonts.raleway(fontSize: 48, color: Colors.white); 53 | 54 | List backBtnBoxShadow = [ 55 | BoxShadow( 56 | color: Color(0xffc3c3c3).withOpacity(0.4), 57 | spreadRadius: 15, 58 | blurRadius: 30, 59 | offset: Offset(-20, 20), // changes position of shadow 60 | ), 61 | BoxShadow( 62 | color: nightBackgroundColor, 63 | spreadRadius: 15, 64 | blurRadius: 30, 65 | offset: Offset(20, -20), // changes position of shadow 66 | ) 67 | ]; 68 | 69 | TextStyle weatherDetailsTextStyle = 70 | GoogleFonts.raleway(fontSize: 24, color: Colors.black); 71 | 72 | const inputDecoration = InputDecoration( 73 | border: outlineInputBorder, 74 | enabledBorder: outlineInputBorder, 75 | focusedBorder: outlineInputBorder, 76 | hintText: "Enter the city name", 77 | hintStyle: hintTextStyle, 78 | ); 79 | -------------------------------------------------------------------------------- /lib/src/feature/weather/presentation/widgets/details_weather_information.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weather_app/src/feature/weather/domain/entity/weather_detail_entity.dart'; 3 | import 'package:weather_app/src/feature/weather/presentation/widgets/detail_block.dart'; 4 | 5 | class DetailsWeatherInformation extends StatelessWidget { 6 | const DetailsWeatherInformation({ 7 | required this.data, 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | final WeatherDetailEntity data; 12 | @override 13 | Widget build(BuildContext context) { 14 | return Expanded( 15 | child: GridView.count( 16 | physics: BouncingScrollPhysics(), 17 | primary: false, 18 | padding: const EdgeInsets.all(20), 19 | crossAxisSpacing: 10, 20 | mainAxisSpacing: 10, 21 | crossAxisCount: 2, 22 | children: [ 23 | DetailBlock( 24 | icon: Icon(Icons.air_outlined, size: 36), 25 | title: "Wind", 26 | value: data.windValue, 27 | unit: "m/s", 28 | color: Colors.amber[100], 29 | ), 30 | DetailBlock( 31 | icon: Icon(Icons.opacity, size: 36), 32 | title: "Humidity", 33 | value: '${data.humidityValue} %', 34 | color: Colors.amber[200], 35 | ), 36 | DetailBlock( 37 | icon: Icon(Icons.fast_forward, size: 36), 38 | title: "Gust", 39 | unit: "m/s", 40 | value: data.gustValue, 41 | color: Colors.amber[300], 42 | ), 43 | DetailBlock( 44 | icon: Icon(Icons.speed, size: 36), 45 | title: "Pressure", 46 | unit: "hPa", 47 | value: data.pressureValue, 48 | color: Colors.amber[400], 49 | ), 50 | DetailBlock( 51 | icon: Icon(Icons.brightness_7, size: 36), 52 | title: "Sunrise", 53 | value: data.sunriseValue, 54 | color: Colors.amber[500], 55 | ), 56 | DetailBlock( 57 | icon: Icon(Icons.brightness_3, size: 36), 58 | title: "Sunset", 59 | value: data.sunsetValue, 60 | color: Colors.amber[600], 61 | ), 62 | ], 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/entity/weather_basic_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:weather_app/src/core/helper/extensions.dart'; 3 | import 'package:weather_app/src/core/networking/url_config.dart'; 4 | import 'package:weather_app/src/feature/weather/domain/entity/weather_full_entity.dart'; 5 | import 'package:weather_app/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart'; 6 | 7 | part 'weather_basic_entity.freezed.dart'; 8 | 9 | // WHY THis entity? 10 | // This entity is used to pass data from one layer to another layer. more specifically from domain layer to presentation layer. and Entity should hold the data in the form of primitive data types. we Extract the data from the DTO and pass it to the Entity. and then we pass the Entity to the presentation layer. and then we extract the data from the Entity and pass it to the UI. and then we can use the data in the UI. 11 | 12 | @freezed 13 | sealed class WeatherBasicEntity implements _$WeatherBasicEntity { 14 | const factory WeatherBasicEntity({ 15 | required String? cityName, 16 | required String? temp, 17 | required String? condition, 18 | required String? dateTime, 19 | required String? imgUrl, 20 | required String? feelsLike, 21 | required String? description, 22 | }) = _WeatherBasicEntity; 23 | 24 | const WeatherBasicEntity._(); 25 | 26 | factory WeatherBasicEntity.fromDTO(WeatherDTO weatherData) { 27 | return WeatherBasicEntity( 28 | cityName: weatherData.name, 29 | temp: weatherData.main?.temp?.toStringAsFixed(1), 30 | condition: weatherData.weather?.first.main, 31 | dateTime: weatherData.dt!.toFormattedTime, 32 | imgUrl: '$imgBaseUrl${weatherData.weather?.first.icon}@2x.png', 33 | feelsLike: weatherData.main?.feelsLike?.toStringAsFixed(1), 34 | description: weatherData.weather?.first.description, 35 | ); 36 | } 37 | 38 | factory WeatherBasicEntity.fromFullEntity(WeatherFullEntity weatherData) { 39 | return WeatherBasicEntity( 40 | cityName: weatherData.name, 41 | temp: weatherData.main?.temp?.toStringAsFixed(1), 42 | condition: weatherData.weather?.first.main, 43 | dateTime: weatherData.dt?.toFormattedTime, 44 | imgUrl: '$imgBaseUrl${weatherData.weather?.first.icon}@2x.png', 45 | feelsLike: weatherData.main?.feelsLike?.toStringAsFixed(1), 46 | description: weatherData.weather?.first.description, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 17 | 21 | 22 | 26 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/src/core/networking/network_api_service.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'network_api_service.dart'; 4 | 5 | // ************************************************************************** 6 | // RetrofitGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element 10 | 11 | class _NetworkApiService implements NetworkApiService { 12 | _NetworkApiService( 13 | this._dio, { 14 | this.baseUrl, 15 | this.errorLogger, 16 | }) { 17 | baseUrl ??= 'https://api.openweathermap.org/data/2.5'; 18 | } 19 | 20 | final Dio _dio; 21 | 22 | String? baseUrl; 23 | 24 | final ParseErrorLogger? errorLogger; 25 | 26 | @override 27 | Future getWeather( 28 | String cityName, 29 | String apiKey, 30 | String units, 31 | ) async { 32 | final _extra = {}; 33 | final queryParameters = { 34 | r'q': cityName, 35 | r'appid': apiKey, 36 | r'units': units, 37 | }; 38 | final _headers = {}; 39 | const Map? _data = null; 40 | final _options = _setStreamType(Options( 41 | method: 'GET', 42 | headers: _headers, 43 | extra: _extra, 44 | ) 45 | .compose( 46 | _dio.options, 47 | '/weather', 48 | queryParameters: queryParameters, 49 | data: _data, 50 | ) 51 | .copyWith( 52 | baseUrl: _combineBaseUrls( 53 | _dio.options.baseUrl, 54 | baseUrl, 55 | ))); 56 | final _result = await _dio.fetch>(_options); 57 | late WeatherDTO _value; 58 | try { 59 | _value = WeatherDTO.fromJson(_result.data!); 60 | } on Object catch (e, s) { 61 | errorLogger?.logError(e, s, _options); 62 | rethrow; 63 | } 64 | return _value; 65 | } 66 | 67 | RequestOptions _setStreamType(RequestOptions requestOptions) { 68 | if (T != dynamic && 69 | !(requestOptions.responseType == ResponseType.bytes || 70 | requestOptions.responseType == ResponseType.stream)) { 71 | if (T == String) { 72 | requestOptions.responseType = ResponseType.plain; 73 | } else { 74 | requestOptions.responseType = ResponseType.json; 75 | } 76 | } 77 | return requestOptions; 78 | } 79 | 80 | String _combineBaseUrls( 81 | String dioBaseUrl, 82 | String? baseUrl, 83 | ) { 84 | if (baseUrl == null || baseUrl.trim().isEmpty) { 85 | return dioBaseUrl; 86 | } 87 | 88 | final url = Uri.parse(baseUrl); 89 | 90 | if (url.isAbsolute) { 91 | return url.toString(); 92 | } 93 | 94 | return Uri.parse(dioBaseUrl).resolveUri(url).toString(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/core/failure/local_failure.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'local_failure.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | /// @nodoc 18 | mixin _$LocalFailure { 19 | String get name => throw _privateConstructorUsedError; 20 | String get message => throw _privateConstructorUsedError; 21 | String? get uriPath => throw _privateConstructorUsedError; 22 | int? get code => throw _privateConstructorUsedError; 23 | } 24 | 25 | /// @nodoc 26 | 27 | class _$LocalFailureImpl implements _LocalFailure { 28 | const _$LocalFailureImpl( 29 | {required this.name, required this.message, this.uriPath, this.code}); 30 | 31 | @override 32 | final String name; 33 | @override 34 | final String message; 35 | @override 36 | final String? uriPath; 37 | @override 38 | final int? code; 39 | 40 | @override 41 | String toString() { 42 | return 'LocalFailure(name: $name, message: $message, uriPath: $uriPath, code: $code)'; 43 | } 44 | 45 | @override 46 | bool operator ==(Object other) { 47 | return identical(this, other) || 48 | (other.runtimeType == runtimeType && 49 | other is _$LocalFailureImpl && 50 | (identical(other.name, name) || other.name == name) && 51 | (identical(other.message, message) || other.message == message) && 52 | (identical(other.uriPath, uriPath) || other.uriPath == uriPath) && 53 | (identical(other.code, code) || other.code == code)); 54 | } 55 | 56 | @override 57 | int get hashCode => Object.hash(runtimeType, name, message, uriPath, code); 58 | } 59 | 60 | abstract class _LocalFailure implements LocalFailure { 61 | const factory _LocalFailure( 62 | {required final String name, 63 | required final String message, 64 | final String? uriPath, 65 | final int? code}) = _$LocalFailureImpl; 66 | 67 | @override 68 | String get name; 69 | @override 70 | String get message; 71 | @override 72 | String? get uriPath; 73 | @override 74 | int? get code; 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/core/failure/app_failure.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'app_failure.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | /// @nodoc 18 | mixin _$AppFailure { 19 | String get message => throw _privateConstructorUsedError; 20 | String get name => throw _privateConstructorUsedError; 21 | int? get code => throw _privateConstructorUsedError; 22 | String? get uriPath => throw _privateConstructorUsedError; 23 | } 24 | 25 | /// @nodoc 26 | 27 | class _$AppFailureImpl extends _AppFailure { 28 | const _$AppFailureImpl( 29 | {required this.message, required this.name, this.code, this.uriPath}) 30 | : super._(); 31 | 32 | @override 33 | final String message; 34 | @override 35 | final String name; 36 | @override 37 | final int? code; 38 | @override 39 | final String? uriPath; 40 | 41 | @override 42 | String toString() { 43 | return 'AppFailure(message: $message, name: $name, code: $code, uriPath: $uriPath)'; 44 | } 45 | 46 | @override 47 | bool operator ==(Object other) { 48 | return identical(this, other) || 49 | (other.runtimeType == runtimeType && 50 | other is _$AppFailureImpl && 51 | (identical(other.message, message) || other.message == message) && 52 | (identical(other.name, name) || other.name == name) && 53 | (identical(other.code, code) || other.code == code) && 54 | (identical(other.uriPath, uriPath) || other.uriPath == uriPath)); 55 | } 56 | 57 | @override 58 | int get hashCode => Object.hash(runtimeType, message, name, code, uriPath); 59 | } 60 | 61 | abstract class _AppFailure extends AppFailure { 62 | const factory _AppFailure( 63 | {required final String message, 64 | required final String name, 65 | final int? code, 66 | final String? uriPath}) = _$AppFailureImpl; 67 | const _AppFailure._() : super._(); 68 | 69 | @override 70 | String get message; 71 | @override 72 | String get name; 73 | @override 74 | int? get code; 75 | @override 76 | String? get uriPath; 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/entity/weather_full_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:weather_app/src/feature/weather/infrastructure/dto/weather_model/weather_dto.dart'; 3 | part 'weather_full_entity.freezed.dart'; 4 | 5 | //Why this ENTITY? 6 | //Because we want to separate the data layer from the domain layer. 7 | // We grab the dto but we don't want to pass the dto to the domain layer. 8 | // as the domain layer should not know about the dto. 9 | //as well as the presentation layer should not know about the dto. 10 | 11 | @freezed 12 | sealed class WeatherFullEntity implements _$WeatherFullEntity { 13 | const factory WeatherFullEntity({ 14 | @required List? weather, 15 | @required MainInsideEntity? main, 16 | @required WindInsideEntity? wind, 17 | @required int? dt, 18 | @required SysInsideEntity? sys, 19 | @required String? name, 20 | @required String? cod, 21 | }) = _WeatherFullEntity; 22 | 23 | const WeatherFullEntity._(); 24 | 25 | factory WeatherFullEntity.fromDTO(WeatherDTO dto) { 26 | return WeatherFullEntity( 27 | weather: dto.weather 28 | ?.map((e) => WeatherInsideEntity( 29 | main: e.main, 30 | description: e.description, 31 | icon: e.icon, 32 | )) 33 | .toList(), 34 | main: MainInsideEntity( 35 | temp: dto.main?.temp, 36 | feelsLike: dto.main?.feelsLike, 37 | pressure: dto.main?.pressure, 38 | humidity: dto.main?.humidity, 39 | ), 40 | wind: WindInsideEntity( 41 | speed: dto.wind?.speed, 42 | gust: dto.wind?.gust, 43 | ), 44 | dt: dto.dt, 45 | sys: SysInsideEntity( 46 | sunrise: dto.sys?.sunrise, 47 | sunset: dto.sys?.sunset, 48 | ), 49 | name: dto.name, 50 | cod: dto.cod, 51 | ); 52 | } 53 | } 54 | 55 | @freezed 56 | sealed class MainInsideEntity implements _$MainInsideEntity { 57 | const factory MainInsideEntity({ 58 | @required double? temp, 59 | @required double? feelsLike, 60 | @required int? pressure, 61 | @required int? humidity, 62 | }) = _MainInsideEntity; 63 | 64 | const MainInsideEntity._(); 65 | } 66 | 67 | @freezed 68 | sealed class SysInsideEntity implements _$SysInsideEntity { 69 | const factory SysInsideEntity({ 70 | @required int? sunrise, 71 | @required int? sunset, 72 | }) = _SysInsideEntity; 73 | const SysInsideEntity._(); 74 | } 75 | 76 | @freezed 77 | sealed class WeatherInsideEntity implements _$WeatherInsideEntity { 78 | const factory WeatherInsideEntity({ 79 | @required String? main, 80 | @required String? description, 81 | @required String? icon, 82 | }) = _WeatherInsideEntity; 83 | 84 | const WeatherInsideEntity._(); 85 | } 86 | 87 | @freezed 88 | sealed class WindInsideEntity implements _$WindInsideEntity { 89 | const factory WindInsideEntity({ 90 | @required double? speed, 91 | @required double? gust, 92 | }) = _WindInsideEntity; 93 | 94 | const WindInsideEntity._(); 95 | } 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/core/failure/failure_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' 2 | show 3 | AlertDialog, 4 | BuildContext, 5 | Column, 6 | MainAxisSize, 7 | Navigator, 8 | Text, 9 | TextAlign, 10 | TextButton, 11 | showDialog; 12 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 13 | import 'package:weather_app/src/core/failure/app_failure.dart'; 14 | import 'package:weather_app/src/core/failure/local_failure.dart'; 15 | import 'package:weather_app/src/core/failure/network_failure.dart'; 16 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 17 | import 'package:weather_app/src/core/helper/extensions.dart'; 18 | import 'package:weather_app/src/feature/common/states/api_state.dart'; 19 | 20 | class FailureHandler { 21 | static void defaultToastShowingHandler( 22 | AppFailure failure, WidgetRef ref, BuildContext context) { 23 | ColoredLogger.Red.log(failure.toString()); 24 | ColoredLogger.Red.log(failure.runtimeType); 25 | ('${failure.code}: ${failure.message}').toast(false); 26 | } 27 | 28 | static void listenForErrors( 29 | WidgetRef ref, 30 | ProviderBase> provider, 31 | BuildContext context, 32 | Future Function() onRetry, 33 | ) { 34 | ref.listen>(provider, (previous, current) { 35 | current.maybeWhen( 36 | orElse: () {}, 37 | failed: (AppFailure failure) { 38 | ColoredLogger.Red.log(failure.toString()); 39 | 40 | //? here we can check for the type of failure and show different dialogs 41 | final title = switch (failure) { 42 | NetworkFailure networkFailure => 43 | '${networkFailure.code} ${networkFailure.message}', 44 | LocalFailure localFailure => 45 | '${localFailure.name} ${localFailure.message}', 46 | _ => 'Unknown error' 47 | }; 48 | 49 | showDialog( 50 | context: context, 51 | builder: (context) { 52 | return AlertDialog.adaptive( 53 | title: Text( 54 | title, 55 | textAlign: TextAlign.center, 56 | ), 57 | content: Column( 58 | mainAxisSize: MainAxisSize.min, 59 | children: [ 60 | //? Here we can show different graphical representation of the error 61 | //? based on the [failure.code] 62 | 63 | Text(failure.name), 64 | ], 65 | ), 66 | actions: [ 67 | TextButton( 68 | onPressed: () { 69 | Navigator.of(context).pop(); 70 | }, 71 | child: Text('Close'), 72 | ), 73 | 74 | //retry button 75 | TextButton( 76 | onPressed: () async { 77 | await onRetry(); 78 | if (context.mounted) { 79 | Navigator.of(context).pop(); 80 | } 81 | }, 82 | child: const Text('Retry'), 83 | ), 84 | ], 85 | ); 86 | }, 87 | ); 88 | }); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: weather_app 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=3.5.0-259.0.dev <4.0.0" 22 | 23 | dependencies: 24 | auto_route: ^9.2.2 25 | cupertino_icons: ^1.0.8 26 | device_info_plus: ^11.1.1 27 | flutter: 28 | sdk: flutter 29 | flutter_hooks: ^0.20.5 30 | fluttertoast: ^8.2.8 31 | fpdart: ^2.0.0-dev.3 32 | freezed_annotation: ^2.4.4 33 | geocoding: ^3.0.0 34 | geolocator: ^13.0.2 35 | google_fonts: ^6.2.1 36 | hooks_riverpod: ^2.6.1 37 | intl: ^0.20.1 38 | permission_handler: ^11.3.1 39 | retrofit: ^4.4.1 40 | riverpod_annotation: ^2.6.1 41 | 42 | dev_dependencies: 43 | auto_route_generator: ^9.0.0 44 | build_runner: ^2.4.13 45 | flutter_test: 46 | sdk: flutter 47 | freezed: ^2.5.7 48 | json_serializable: ^6.9.0 49 | retrofit_generator: ^9.1.5 50 | riverpod_generator: ^2.6.3 51 | riverpod_lint: ^2.6.3 52 | # For information on the generic Dart part of this file, see the 53 | # following page: https://dart.dev/tools/pub/pubspec 54 | # The following section is specific to Flutter. 55 | flutter: 56 | # The following line ensures that the Material Icons font is 57 | # included with your application, so that you can use the icons in 58 | # the material Icons class. 59 | uses-material-design: true 60 | 61 | # To add assets to your application, add an assets section, like this: 62 | assets: 63 | - assets/images/ 64 | # An image asset can refer to one or more resolution-specific "variants", see 65 | # https://flutter.dev/assets-and-images/#resolution-aware. 66 | # For details regarding adding assets from package dependencies, see 67 | # https://flutter.dev/assets-and-images/#from-packages 68 | # To add custom fonts to your application, add a fonts section here, 69 | # in this "flutter" section. Each entry in this list should have a 70 | # "family" key with the font family name, and a "fonts" key with a 71 | # list giving the asset and other descriptors for the font. For 72 | # example: 73 | # fonts: 74 | # - family: Schyler 75 | # fonts: 76 | # - asset: fonts/Schyler-Regular.ttf 77 | # - asset: fonts/Schyler-Italic.ttf 78 | # style: italic 79 | # - family: Trajan Pro 80 | # fonts: 81 | # - asset: fonts/TrajanPro.ttf 82 | # - asset: fonts/TrajanPro_Bold.ttf 83 | # weight: 700 84 | # 85 | # For details regarding fonts from package dependencies, 86 | # see https://flutter.dev/custom-fonts/#from-packages 87 | -------------------------------------------------------------------------------- /lib/src/core/failure/network_failure.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: depend_on_referenced_packages, no_leading_underscores_for_local_identifiers 2 | import 'package:dio/dio.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:weather_app/src/core/failure/app_failure.dart'; 5 | import 'package:weather_app/src/core/networking/network_misc.dart'; 6 | 7 | part 'network_failure.freezed.dart'; 8 | 9 | @Freezed(copyWith: true) 10 | class NetworkFailure with _$NetworkFailure implements AppFailure { 11 | const factory NetworkFailure({ 12 | required String name, 13 | required String message, 14 | required String uriPath, 15 | required int code, 16 | }) = _NetworkFailure; 17 | 18 | static NetworkFailure getDioException(Exception error) { 19 | if (error is DioException) { 20 | final code = error.response?.statusCode; 21 | final path = error.requestOptions.path; 22 | final message = _handleDioErrorMessage(error); 23 | final name = _getErrorName(error); 24 | 25 | return NetworkFailure( 26 | name: name, 27 | uriPath: path, 28 | message: message, 29 | code: code ?? 0, 30 | ); 31 | } 32 | //we can't be here because we checked before calling this method 33 | throw Exception(ErrorMessage.nonProducable.name); 34 | } 35 | 36 | // Helper method to handle different types of Dio exceptions 37 | static String _handleDioErrorMessage(DioException error) { 38 | switch (error.type) { 39 | case DioExceptionType.connectionTimeout: 40 | case DioExceptionType.sendTimeout: 41 | case DioExceptionType.receiveTimeout: 42 | return ErrorMessage.timeOut.name; 43 | case DioExceptionType.badResponse: 44 | return error.message ?? ErrorMessage.badResponse.name; 45 | case DioExceptionType.cancel: 46 | break; 47 | case DioExceptionType.unknown: 48 | return _getErrorResponseBodyFromServer(error); 49 | case DioExceptionType.badCertificate: 50 | return ErrorMessage.internalServerError.name; 51 | case DioExceptionType.connectionError: 52 | return error.message ?? ErrorMessage.connectionError.name; 53 | default: 54 | return ErrorMessage.unknownError.name; 55 | } 56 | return ErrorMessage.unknownError.name; 57 | } 58 | 59 | static String _getErrorResponseBodyFromServer(DioException error) { 60 | assert(error.type == DioExceptionType.unknown); 61 | if (error.response?.data is String) { 62 | return error.response?.data ?? 'Redirected to login page'; 63 | } 64 | // Here Map is the type of the response body 65 | // You can use a proper Model class to parse the response body 66 | final responseBody = error.response?.data as Map?; 67 | String? msg = responseBody?['message'] ?? error.message; 68 | if (msg == null || msg.isEmpty || msg == 'null') { 69 | msg = ErrorMessage.dataUnavailable.name; 70 | } 71 | return msg; 72 | } 73 | 74 | static String _getErrorName(DioException error) { 75 | String _name = ErrorMessage.unrecognizedError.name; 76 | if (error.type == DioExceptionType.connectionError || 77 | error.type == DioExceptionType.sendTimeout || 78 | error.type == DioExceptionType.receiveTimeout || 79 | error.type == DioExceptionType.connectionTimeout) { 80 | _name = ErrorMessage.noInternetConnection.name; 81 | } 82 | 83 | final code = error.response?.statusCode; 84 | final status = getStatusCode(code); 85 | return status?.name ?? _name; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/feature/weather/infrastructure/dto/weather_model/weather_dto.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'weather_dto.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$WeatherDTOImpl _$$WeatherDTOImplFromJson(Map json) => 10 | _$WeatherDTOImpl( 11 | weather: (json['weather'] as List?) 12 | ?.map((e) => Weather.fromJson(e as Map)) 13 | .toList(), 14 | main: json['main'] == null 15 | ? null 16 | : Main.fromJson(json['main'] as Map), 17 | wind: json['wind'] == null 18 | ? null 19 | : Wind.fromJson(json['wind'] as Map), 20 | dt: (json['dt'] as num?)?.toInt(), 21 | sys: json['sys'] == null 22 | ? null 23 | : Sys.fromJson(json['sys'] as Map), 24 | name: json['name'] as String?, 25 | cod: const StringConverter().fromJson(json['cod']), 26 | ); 27 | 28 | Map _$$WeatherDTOImplToJson(_$WeatherDTOImpl instance) => 29 | { 30 | 'weather': instance.weather?.map((e) => e.toJson()).toList(), 31 | 'main': instance.main?.toJson(), 32 | 'wind': instance.wind?.toJson(), 33 | 'dt': instance.dt, 34 | 'sys': instance.sys?.toJson(), 35 | 'name': instance.name, 36 | 'cod': _$JsonConverterToJson( 37 | instance.cod, const StringConverter().toJson), 38 | }; 39 | 40 | Json? _$JsonConverterToJson( 41 | Value? value, 42 | Json? Function(Value value) toJson, 43 | ) => 44 | value == null ? null : toJson(value); 45 | 46 | _$MainImpl _$$MainImplFromJson(Map json) => _$MainImpl( 47 | temp: (json['temp'] as num?)?.toDouble(), 48 | feelsLike: (json['feels_like'] as num?)?.toDouble(), 49 | pressure: (json['pressure'] as num?)?.toInt(), 50 | humidity: (json['humidity'] as num?)?.toInt(), 51 | ); 52 | 53 | Map _$$MainImplToJson(_$MainImpl instance) => 54 | { 55 | 'temp': instance.temp, 56 | 'feels_like': instance.feelsLike, 57 | 'pressure': instance.pressure, 58 | 'humidity': instance.humidity, 59 | }; 60 | 61 | _$SysImpl _$$SysImplFromJson(Map json) => _$SysImpl( 62 | sunrise: (json['sunrise'] as num?)?.toInt(), 63 | sunset: (json['sunset'] as num?)?.toInt(), 64 | ); 65 | 66 | Map _$$SysImplToJson(_$SysImpl instance) => { 67 | 'sunrise': instance.sunrise, 68 | 'sunset': instance.sunset, 69 | }; 70 | 71 | _$WeatherImpl _$$WeatherImplFromJson(Map json) => 72 | _$WeatherImpl( 73 | main: json['main'] as String?, 74 | description: json['description'] as String?, 75 | icon: json['icon'] as String?, 76 | ); 77 | 78 | Map _$$WeatherImplToJson(_$WeatherImpl instance) => 79 | { 80 | 'main': instance.main, 81 | 'description': instance.description, 82 | 'icon': instance.icon, 83 | }; 84 | 85 | _$WindImpl _$$WindImplFromJson(Map json) => _$WindImpl( 86 | speed: (json['speed'] as num?)?.toDouble(), 87 | gust: (json['gust'] as num?)?.toDouble(), 88 | ); 89 | 90 | Map _$$WindImplToJson(_$WindImpl instance) => 91 | { 92 | 'speed': instance.speed, 93 | 'gust': instance.gust, 94 | }; 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/entity/weather_detail_entity.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'weather_detail_entity.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | /// @nodoc 18 | mixin _$WeatherDetailEntity { 19 | String? get windValue => throw _privateConstructorUsedError; 20 | String? get humidityValue => throw _privateConstructorUsedError; 21 | String? get gustValue => throw _privateConstructorUsedError; 22 | String? get pressureValue => throw _privateConstructorUsedError; 23 | String? get sunriseValue => throw _privateConstructorUsedError; 24 | String? get sunsetValue => throw _privateConstructorUsedError; 25 | } 26 | 27 | /// @nodoc 28 | 29 | class _$WeatherDetailEntityImpl extends _WeatherDetailEntity { 30 | const _$WeatherDetailEntityImpl( 31 | {required this.windValue, 32 | required this.humidityValue, 33 | required this.gustValue, 34 | required this.pressureValue, 35 | required this.sunriseValue, 36 | required this.sunsetValue}) 37 | : super._(); 38 | 39 | @override 40 | final String? windValue; 41 | @override 42 | final String? humidityValue; 43 | @override 44 | final String? gustValue; 45 | @override 46 | final String? pressureValue; 47 | @override 48 | final String? sunriseValue; 49 | @override 50 | final String? sunsetValue; 51 | 52 | @override 53 | String toString() { 54 | return 'WeatherDetailEntity(windValue: $windValue, humidityValue: $humidityValue, gustValue: $gustValue, pressureValue: $pressureValue, sunriseValue: $sunriseValue, sunsetValue: $sunsetValue)'; 55 | } 56 | 57 | @override 58 | bool operator ==(Object other) { 59 | return identical(this, other) || 60 | (other.runtimeType == runtimeType && 61 | other is _$WeatherDetailEntityImpl && 62 | (identical(other.windValue, windValue) || 63 | other.windValue == windValue) && 64 | (identical(other.humidityValue, humidityValue) || 65 | other.humidityValue == humidityValue) && 66 | (identical(other.gustValue, gustValue) || 67 | other.gustValue == gustValue) && 68 | (identical(other.pressureValue, pressureValue) || 69 | other.pressureValue == pressureValue) && 70 | (identical(other.sunriseValue, sunriseValue) || 71 | other.sunriseValue == sunriseValue) && 72 | (identical(other.sunsetValue, sunsetValue) || 73 | other.sunsetValue == sunsetValue)); 74 | } 75 | 76 | @override 77 | int get hashCode => Object.hash(runtimeType, windValue, humidityValue, 78 | gustValue, pressureValue, sunriseValue, sunsetValue); 79 | } 80 | 81 | abstract class _WeatherDetailEntity extends WeatherDetailEntity { 82 | const factory _WeatherDetailEntity( 83 | {required final String? windValue, 84 | required final String? humidityValue, 85 | required final String? gustValue, 86 | required final String? pressureValue, 87 | required final String? sunriseValue, 88 | required final String? sunsetValue}) = _$WeatherDetailEntityImpl; 89 | const _WeatherDetailEntity._() : super._(); 90 | 91 | @override 92 | String? get windValue; 93 | @override 94 | String? get humidityValue; 95 | @override 96 | String? get gustValue; 97 | @override 98 | String? get pressureValue; 99 | @override 100 | String? get sunriseValue; 101 | @override 102 | String? get sunsetValue; 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/entity/weather_basic_entity.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'weather_basic_entity.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | /// @nodoc 18 | mixin _$WeatherBasicEntity { 19 | String? get cityName => throw _privateConstructorUsedError; 20 | String? get temp => throw _privateConstructorUsedError; 21 | String? get condition => throw _privateConstructorUsedError; 22 | String? get dateTime => throw _privateConstructorUsedError; 23 | String? get imgUrl => throw _privateConstructorUsedError; 24 | String? get feelsLike => throw _privateConstructorUsedError; 25 | String? get description => throw _privateConstructorUsedError; 26 | } 27 | 28 | /// @nodoc 29 | 30 | class _$WeatherBasicEntityImpl extends _WeatherBasicEntity { 31 | const _$WeatherBasicEntityImpl( 32 | {required this.cityName, 33 | required this.temp, 34 | required this.condition, 35 | required this.dateTime, 36 | required this.imgUrl, 37 | required this.feelsLike, 38 | required this.description}) 39 | : super._(); 40 | 41 | @override 42 | final String? cityName; 43 | @override 44 | final String? temp; 45 | @override 46 | final String? condition; 47 | @override 48 | final String? dateTime; 49 | @override 50 | final String? imgUrl; 51 | @override 52 | final String? feelsLike; 53 | @override 54 | final String? description; 55 | 56 | @override 57 | String toString() { 58 | return 'WeatherBasicEntity(cityName: $cityName, temp: $temp, condition: $condition, dateTime: $dateTime, imgUrl: $imgUrl, feelsLike: $feelsLike, description: $description)'; 59 | } 60 | 61 | @override 62 | bool operator ==(Object other) { 63 | return identical(this, other) || 64 | (other.runtimeType == runtimeType && 65 | other is _$WeatherBasicEntityImpl && 66 | (identical(other.cityName, cityName) || 67 | other.cityName == cityName) && 68 | (identical(other.temp, temp) || other.temp == temp) && 69 | (identical(other.condition, condition) || 70 | other.condition == condition) && 71 | (identical(other.dateTime, dateTime) || 72 | other.dateTime == dateTime) && 73 | (identical(other.imgUrl, imgUrl) || other.imgUrl == imgUrl) && 74 | (identical(other.feelsLike, feelsLike) || 75 | other.feelsLike == feelsLike) && 76 | (identical(other.description, description) || 77 | other.description == description)); 78 | } 79 | 80 | @override 81 | int get hashCode => Object.hash(runtimeType, cityName, temp, condition, 82 | dateTime, imgUrl, feelsLike, description); 83 | } 84 | 85 | abstract class _WeatherBasicEntity extends WeatherBasicEntity { 86 | const factory _WeatherBasicEntity( 87 | {required final String? cityName, 88 | required final String? temp, 89 | required final String? condition, 90 | required final String? dateTime, 91 | required final String? imgUrl, 92 | required final String? feelsLike, 93 | required final String? description}) = _$WeatherBasicEntityImpl; 94 | const _WeatherBasicEntity._() : super._(); 95 | 96 | @override 97 | String? get cityName; 98 | @override 99 | String? get temp; 100 | @override 101 | String? get condition; 102 | @override 103 | String? get dateTime; 104 | @override 105 | String? get imgUrl; 106 | @override 107 | String? get feelsLike; 108 | @override 109 | String? get description; 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/core/networking/interceptors/api_interceptor.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: no_leading_underscores_for_local_identifiers, unused_field, unused_local_variable 2 | import 'package:dio/dio.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 5 | 6 | class ApiInterceptor extends Interceptor { 7 | late final Ref _ref; 8 | ApiInterceptor(this._ref) : super(); 9 | @override 10 | void onRequest( 11 | RequestOptions options, 12 | RequestInterceptorHandler handler, 13 | ) async { 14 | // final token = _ref.read(sharedPrefProvider).getAuthToken; 15 | 16 | ColoredLogger.Yellow.log('🎯LOG URL PATH: ${options.headers}'); 17 | ColoredLogger.Yellow.log('🔰Content-Type: ${options.contentType}'); 18 | ColoredLogger.Yellow.log('🔰Request method: ${options.toCustomString()}'); 19 | // ColoredLogger.Yellow.log('🔰Token: $token'); 20 | 21 | //add header 22 | // options.headers.addAll({ 23 | // "System-key": Config.systemKey, 24 | // }); 25 | 26 | //add token 27 | // if (options.headers.containsKey('requiresAuthToken')) { 28 | // if (options.headers['requiresAuthToken'] == true) { 29 | // ColoredLogger.White.log('🔰Token added to the header $token'); 30 | // options.headers.addAll( 31 | // { 32 | // 'Authorization': 'Bearer $token', 33 | // }, 34 | // ); 35 | // } 36 | // options.headers.remove('requiresAuthToken'); 37 | // } 38 | 39 | return handler.next(options); 40 | } 41 | 42 | @override 43 | void onResponse( 44 | Response response, 45 | ResponseInterceptorHandler handler, 46 | ) async { 47 | ColoredLogger.Green.log('🎯LOG URL PATH: ${response.requestOptions.uri}'); 48 | ColoredLogger.Green.log('🔟Response status code: ${response.statusCode}'); 49 | ColoredLogger.Magenta.log('🍿Response data: ${response.data}'); 50 | 51 | if (response.statusCode != null && 52 | (response.statusCode! >= 400 && response.statusCode! <= 599)) { 53 | return handler.reject( 54 | DioException( 55 | requestOptions: response.requestOptions, 56 | response: response, 57 | message: response.data['detail'].toString(), 58 | ), 59 | true, 60 | ); 61 | } 62 | 63 | //sometimes the response is not in the expected format 64 | //like, it may return 200 but the response is showing error 65 | //so, we need to handle this case 66 | 67 | //ex: {"code": 400, "message": "Bad Request", "success": false} with status code 200 68 | 69 | //we can check the response data and status code, for validating the response 70 | //we can use dart's Pattern matching to check the response data 71 | 72 | if (response.statusCode == 200 && response.data is Map) { 73 | final data = response.data as Map; 74 | 75 | //pattern matching 76 | final (bool? sucess, String? message, int? code) = ( 77 | data['success'] as bool?, 78 | data['message'] as String?, 79 | data['code'] as int?, 80 | ); 81 | 82 | final isSuccess = sucess; 83 | 84 | if (isSuccess == false) { 85 | return handler.reject( 86 | DioException( 87 | requestOptions: response.requestOptions, 88 | response: Response( 89 | requestOptions: response.requestOptions, 90 | statusCode: (code is int) ? code : 503, 91 | ), 92 | message: message ?? 'Unknown error', 93 | ), 94 | true, 95 | ); 96 | } else { 97 | //here the response is successful either the success is true or null 98 | //null means the respnose format is not as ours so we suppose it as success 99 | //so, we can return the response as it is 100 | return handler.next(response); 101 | } 102 | } else { 103 | return handler.next(response); 104 | } 105 | } 106 | 107 | @override 108 | void onError( 109 | DioException err, 110 | ErrorInterceptorHandler handler, 111 | ) async { 112 | // in case of no internet connection 113 | if (err.type == DioExceptionType.connectionError || 114 | err.type == DioExceptionType.sendTimeout || 115 | err.type == DioExceptionType.receiveTimeout || 116 | err.type == DioExceptionType.connectionTimeout) { 117 | return handler.reject( 118 | DioException( 119 | type: DioExceptionType.connectionError, 120 | requestOptions: err.requestOptions, 121 | response: err.response, 122 | message: 'No internet connection', 123 | ), 124 | ); 125 | } else { 126 | ColoredLogger.Red.log('🔴Error: ${err.message}'); 127 | return handler.reject( 128 | DioException( 129 | requestOptions: err.requestOptions, 130 | response: err.response, 131 | message: err.error.toString(), 132 | ), 133 | ); 134 | } 135 | } 136 | } 137 | 138 | //custom extension for showing the origin, host 139 | extension RequestOptionsToStringX on RequestOptions { 140 | String toCustomString() { 141 | return 'method: $method, path: $path, origin: ${headers["Origin"]}, host: ${headers["Host"]}'; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/src/feature/location/presentation/screen/initial_location_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:google_fonts/google_fonts.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | import 'package:weather_app/src/core/helper/extensions.dart'; 7 | import 'package:weather_app/src/feature/location/application/notifiers/location_notifier.dart'; 8 | import 'package:weather_app/src/feature/location/presentation/widgets/success_location_widget.dart'; 9 | import 'package:weather_app/src/feature/weather/application/providers.dart'; 10 | import 'package:weather_app/src/feature/weather/presentation/styles.dart'; 11 | import 'package:weather_app/src/routes/router.dart'; 12 | 13 | @RoutePage() 14 | class InitialLocationScreen extends HookConsumerWidget { 15 | const InitialLocationScreen({Key? key}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | //calling the location provider at init 20 | //so that we can get the current location 21 | useEffect( 22 | () { 23 | //widgetBinding 24 | WidgetsBinding.instance.addPostFrameCallback((_) async { 25 | await ref.read(locationNotifierProvider.notifier).getMyLocation(); 26 | }); 27 | return null; 28 | }, 29 | [], 30 | ); 31 | 32 | //we are listening to the location provider 33 | //so that we can get the current location 34 | //it will call as soon as the app is opened 35 | //as we are using the location provider in the initial page 36 | final locationState = ref.watch(locationNotifierProvider); 37 | final cityNameController = useTextEditingController(); 38 | 39 | return Stack( 40 | children: [ 41 | Positioned( 42 | child: Container( 43 | width: double.infinity, 44 | height: context.height / 2, 45 | child: Image.asset( 46 | 'assets/images/day.jpg', 47 | fit: BoxFit.cover, 48 | ), 49 | ), 50 | ), 51 | Positioned( 52 | bottom: 0, 53 | right: 0, 54 | child: Container( 55 | height: context.height / 2, 56 | width: context.width, 57 | color: dayShadowColor, 58 | ), 59 | ), 60 | Scaffold( 61 | resizeToAvoidBottomInset: false, 62 | backgroundColor: Colors.black54, 63 | body: RefreshIndicator( 64 | onRefresh: () async { 65 | //this will refresh the location api 66 | ref.invalidate(locationNotifierProvider); 67 | }, 68 | child: ListView( 69 | physics: const BouncingScrollPhysics( 70 | parent: AlwaysScrollableScrollPhysics(), 71 | ), 72 | children: [ 73 | Padding( 74 | padding: const EdgeInsets.all(18.0), 75 | child: Column( 76 | crossAxisAlignment: CrossAxisAlignment.start, 77 | children: [ 78 | SizedBox(height: context.height * 0.10), 79 | Text( 80 | "Hello there!", 81 | style: GoogleFonts.raleway( 82 | fontSize: 32, color: Colors.white), 83 | ), 84 | const SizedBox(height: 10), 85 | Text( 86 | "Check the weather by the city", 87 | style: GoogleFonts.raleway( 88 | fontSize: 16, color: Colors.white), 89 | ), 90 | const SizedBox(height: 20), 91 | TextField( 92 | controller: cityNameController, 93 | style: TextStyle(color: Colors.white), 94 | decoration: inputDecoration.copyWith( 95 | suffixIcon: IconButton( 96 | onPressed: () { 97 | if (cityNameController.text.isEmpty) return; 98 | ref 99 | .watch(getCityNameProvider.notifier) 100 | .setCityName( 101 | cityNameController.text, 102 | ); 103 | WidgetsBinding.instance.addPostFrameCallback((_) { 104 | context.router.push( 105 | WeatherInformationRoute(), 106 | ); 107 | }); 108 | }, 109 | icon: Icon( 110 | Icons.search, 111 | color: Colors.white, 112 | ), 113 | ), 114 | ), 115 | ), 116 | SizedBox(height: context.height * 0.2), 117 | Container( 118 | width: double.infinity, 119 | child: Column( 120 | mainAxisSize: MainAxisSize.max, 121 | mainAxisAlignment: MainAxisAlignment.center, 122 | children: [ 123 | Text( 124 | "You are in ", 125 | style: GoogleFonts.raleway( 126 | fontSize: 16, color: Colors.white), 127 | ), 128 | locationState.maybeWhen( 129 | loading: () => CircularProgressIndicator(), 130 | data: (cityName) { 131 | return BuildSucessLocation(cityName: cityName); 132 | }, 133 | failed: (e) => Text( 134 | e.toString(), 135 | style: TextStyle(color: Colors.white), 136 | ), 137 | orElse: () => CircularProgressIndicator(), 138 | ), 139 | ], 140 | ), 141 | ), 142 | ], 143 | ), 144 | ), 145 | ], 146 | ), 147 | ), 148 | ) 149 | ], 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/src/feature/weather/presentation/screen/weather_infromation_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | import 'package:weather_app/src/core/failure/failure_handler.dart'; 7 | import 'package:weather_app/src/core/helper/extensions.dart'; 8 | import 'package:weather_app/src/feature/weather/application/notifiers/weather_notifier.dart'; 9 | import 'package:weather_app/src/feature/weather/application/providers.dart'; 10 | import 'package:weather_app/src/feature/weather/domain/entity/weather_basic_entity.dart'; 11 | import 'package:weather_app/src/feature/weather/domain/entity/weather_detail_entity.dart'; 12 | import 'package:weather_app/src/feature/weather/presentation/styles.dart'; 13 | import 'package:weather_app/src/feature/weather/presentation/widgets/basic_weather_information.dart'; 14 | import 'package:weather_app/src/feature/weather/presentation/widgets/details_weather_information.dart'; 15 | 16 | @RoutePage() 17 | class WeatherInformationScreen extends HookConsumerWidget { 18 | const WeatherInformationScreen({Key? key}) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | //we are reading the city name from the provider 23 | final cityName = ref.watch(getCityNameProvider); 24 | 25 | //calling the location provider at init 26 | //so that we can get the current location 27 | useEffect( 28 | () { 29 | //widgetBinding 30 | WidgetsBinding.instance.addPostFrameCallback((_) async { 31 | await ref.read(weatherNotifierProvider.notifier).getWeather(cityName); 32 | }); 33 | return null; 34 | }, 35 | [], 36 | ); 37 | 38 | final weatherState = ref.watch(weatherNotifierProvider); 39 | final isSuccessful = weatherState.maybeWhen( 40 | data: (data) => true, 41 | orElse: () => false, 42 | ); 43 | 44 | FailureHandler.listenForErrors( 45 | ref, 46 | weatherNotifierProvider, 47 | context, 48 | () async { 49 | await ref.read(weatherNotifierProvider.notifier).getWeather(cityName); 50 | }, 51 | ); 52 | return SafeArea( 53 | child: Scaffold( 54 | backgroundColor: Colors.black, 55 | body: RefreshIndicator( 56 | onRefresh: () async { 57 | return await ref 58 | .refresh(weatherNotifierProvider.notifier) 59 | .getWeather(cityName); 60 | }, 61 | child: SingleChildScrollView( 62 | child: Stack( 63 | children: [ 64 | Container( 65 | height: context.height / 2, 66 | decoration: detailsBgDecoration, 67 | ), 68 | Container( 69 | height: context.height, 70 | decoration: detailsBgDecorationWithGradient, 71 | ), 72 | Positioned( 73 | top: 10, 74 | left: 10, 75 | child: Container( 76 | height: 40, 77 | width: 40, 78 | padding: EdgeInsets.only(left: 4), 79 | decoration: backButtonDecoration, 80 | child: IconButton( 81 | onPressed: () { 82 | WidgetsBinding.instance 83 | .addPostFrameCallback((timeStamp) { 84 | Navigator.pop(context); 85 | }); 86 | }, 87 | icon: Icon(Icons.arrow_back_ios), 88 | ), 89 | ), 90 | ), 91 | Positioned( 92 | child: weatherState.maybeWhen( 93 | loading: () => Center( 94 | child: CircularProgressIndicator(), 95 | ), 96 | data: (data) => Container( 97 | width: context.width, 98 | height: context.height / 2.0, 99 | child: BasicWeatherInformation( 100 | key: GlobalObjectKey( 101 | cityName.toString(), 102 | ), 103 | data: WeatherBasicEntity.fromFullEntity(data), 104 | ), 105 | ), 106 | failed: (e) { 107 | return Container( 108 | margin: EdgeInsets.only(top: 140), 109 | alignment: Alignment.center, 110 | child: Text( 111 | e.message, 112 | style: bigTitleStyle, 113 | ), 114 | ); 115 | }, 116 | orElse: () => CircularProgressIndicator(), 117 | ), 118 | ), 119 | Positioned( 120 | top: context.height / 2, 121 | child: Container( 122 | height: context.height / 2, 123 | width: context.width, 124 | padding: const EdgeInsets.only(left: 45, top: 20), 125 | decoration: BoxDecoration( 126 | color: Color(0xff14141C), 127 | ), 128 | child: isSuccessful 129 | ? Column( 130 | children: [ 131 | Row( 132 | mainAxisAlignment: MainAxisAlignment.start, 133 | crossAxisAlignment: CrossAxisAlignment.start, 134 | children: [ 135 | CircleAvatar( 136 | radius: 10, 137 | backgroundColor: Colors.white, 138 | ), 139 | SizedBox(width: 10), 140 | Text( 141 | "Weather Details", 142 | style: bigTitleStyle.copyWith(fontSize: 16), 143 | ) 144 | ], 145 | ), 146 | SizedBox(height: 10), 147 | weatherState.maybeWhen( 148 | loading: () => Center( 149 | child: CircularProgressIndicator(), 150 | ), 151 | data: (weatherData) { 152 | final weatherDetail = 153 | WeatherDetailEntity.fromFullEntity( 154 | weatherData, 155 | ); 156 | return DetailsWeatherInformation( 157 | data: weatherDetail); 158 | }, 159 | failed: (e) => Text('${e.code} : ${e.message}'), 160 | orElse: () => Container(), 161 | ), 162 | ], 163 | ) 164 | : const SizedBox(), 165 | ), 166 | ), 167 | ], 168 | ), 169 | ), 170 | ), 171 | ), 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/src/core/failure/network_failure.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'network_failure.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | /// @nodoc 18 | mixin _$NetworkFailure { 19 | String get name => throw _privateConstructorUsedError; 20 | String get message => throw _privateConstructorUsedError; 21 | String get uriPath => throw _privateConstructorUsedError; 22 | int get code => throw _privateConstructorUsedError; 23 | 24 | /// Create a copy of NetworkFailure 25 | /// with the given fields replaced by the non-null parameter values. 26 | @JsonKey(includeFromJson: false, includeToJson: false) 27 | $NetworkFailureCopyWith get copyWith => 28 | throw _privateConstructorUsedError; 29 | } 30 | 31 | /// @nodoc 32 | abstract class $NetworkFailureCopyWith<$Res> { 33 | factory $NetworkFailureCopyWith( 34 | NetworkFailure value, $Res Function(NetworkFailure) then) = 35 | _$NetworkFailureCopyWithImpl<$Res, NetworkFailure>; 36 | @useResult 37 | $Res call({String name, String message, String uriPath, int code}); 38 | } 39 | 40 | /// @nodoc 41 | class _$NetworkFailureCopyWithImpl<$Res, $Val extends NetworkFailure> 42 | implements $NetworkFailureCopyWith<$Res> { 43 | _$NetworkFailureCopyWithImpl(this._value, this._then); 44 | 45 | // ignore: unused_field 46 | final $Val _value; 47 | // ignore: unused_field 48 | final $Res Function($Val) _then; 49 | 50 | /// Create a copy of NetworkFailure 51 | /// with the given fields replaced by the non-null parameter values. 52 | @pragma('vm:prefer-inline') 53 | @override 54 | $Res call({ 55 | Object? name = null, 56 | Object? message = null, 57 | Object? uriPath = null, 58 | Object? code = null, 59 | }) { 60 | return _then(_value.copyWith( 61 | name: null == name 62 | ? _value.name 63 | : name // ignore: cast_nullable_to_non_nullable 64 | as String, 65 | message: null == message 66 | ? _value.message 67 | : message // ignore: cast_nullable_to_non_nullable 68 | as String, 69 | uriPath: null == uriPath 70 | ? _value.uriPath 71 | : uriPath // ignore: cast_nullable_to_non_nullable 72 | as String, 73 | code: null == code 74 | ? _value.code 75 | : code // ignore: cast_nullable_to_non_nullable 76 | as int, 77 | ) as $Val); 78 | } 79 | } 80 | 81 | /// @nodoc 82 | abstract class _$$NetworkFailureImplCopyWith<$Res> 83 | implements $NetworkFailureCopyWith<$Res> { 84 | factory _$$NetworkFailureImplCopyWith(_$NetworkFailureImpl value, 85 | $Res Function(_$NetworkFailureImpl) then) = 86 | __$$NetworkFailureImplCopyWithImpl<$Res>; 87 | @override 88 | @useResult 89 | $Res call({String name, String message, String uriPath, int code}); 90 | } 91 | 92 | /// @nodoc 93 | class __$$NetworkFailureImplCopyWithImpl<$Res> 94 | extends _$NetworkFailureCopyWithImpl<$Res, _$NetworkFailureImpl> 95 | implements _$$NetworkFailureImplCopyWith<$Res> { 96 | __$$NetworkFailureImplCopyWithImpl( 97 | _$NetworkFailureImpl _value, $Res Function(_$NetworkFailureImpl) _then) 98 | : super(_value, _then); 99 | 100 | /// Create a copy of NetworkFailure 101 | /// with the given fields replaced by the non-null parameter values. 102 | @pragma('vm:prefer-inline') 103 | @override 104 | $Res call({ 105 | Object? name = null, 106 | Object? message = null, 107 | Object? uriPath = null, 108 | Object? code = null, 109 | }) { 110 | return _then(_$NetworkFailureImpl( 111 | name: null == name 112 | ? _value.name 113 | : name // ignore: cast_nullable_to_non_nullable 114 | as String, 115 | message: null == message 116 | ? _value.message 117 | : message // ignore: cast_nullable_to_non_nullable 118 | as String, 119 | uriPath: null == uriPath 120 | ? _value.uriPath 121 | : uriPath // ignore: cast_nullable_to_non_nullable 122 | as String, 123 | code: null == code 124 | ? _value.code 125 | : code // ignore: cast_nullable_to_non_nullable 126 | as int, 127 | )); 128 | } 129 | } 130 | 131 | /// @nodoc 132 | 133 | class _$NetworkFailureImpl implements _NetworkFailure { 134 | const _$NetworkFailureImpl( 135 | {required this.name, 136 | required this.message, 137 | required this.uriPath, 138 | required this.code}); 139 | 140 | @override 141 | final String name; 142 | @override 143 | final String message; 144 | @override 145 | final String uriPath; 146 | @override 147 | final int code; 148 | 149 | @override 150 | String toString() { 151 | return 'NetworkFailure(name: $name, message: $message, uriPath: $uriPath, code: $code)'; 152 | } 153 | 154 | @override 155 | bool operator ==(Object other) { 156 | return identical(this, other) || 157 | (other.runtimeType == runtimeType && 158 | other is _$NetworkFailureImpl && 159 | (identical(other.name, name) || other.name == name) && 160 | (identical(other.message, message) || other.message == message) && 161 | (identical(other.uriPath, uriPath) || other.uriPath == uriPath) && 162 | (identical(other.code, code) || other.code == code)); 163 | } 164 | 165 | @override 166 | int get hashCode => Object.hash(runtimeType, name, message, uriPath, code); 167 | 168 | /// Create a copy of NetworkFailure 169 | /// with the given fields replaced by the non-null parameter values. 170 | @JsonKey(includeFromJson: false, includeToJson: false) 171 | @override 172 | @pragma('vm:prefer-inline') 173 | _$$NetworkFailureImplCopyWith<_$NetworkFailureImpl> get copyWith => 174 | __$$NetworkFailureImplCopyWithImpl<_$NetworkFailureImpl>( 175 | this, _$identity); 176 | } 177 | 178 | abstract class _NetworkFailure implements NetworkFailure { 179 | const factory _NetworkFailure( 180 | {required final String name, 181 | required final String message, 182 | required final String uriPath, 183 | required final int code}) = _$NetworkFailureImpl; 184 | 185 | @override 186 | String get name; 187 | @override 188 | String get message; 189 | @override 190 | String get uriPath; 191 | @override 192 | int get code; 193 | 194 | /// Create a copy of NetworkFailure 195 | /// with the given fields replaced by the non-null parameter values. 196 | @override 197 | @JsonKey(includeFromJson: false, includeToJson: false) 198 | _$$NetworkFailureImplCopyWith<_$NetworkFailureImpl> get copyWith => 199 | throw _privateConstructorUsedError; 200 | } 201 | -------------------------------------------------------------------------------- /lib/src/core/failure/readme.md: -------------------------------------------------------------------------------- 1 | # Failure Handling in Your Flutter App 2 | In this app, error or failure handling is centralized using the **AppFailure** class, which is extended by specific failure types like **NetworkFailure** and **LocalFailure**. This approach allows you to represent different kinds of errors consistently across your app, providing a flexible, reusable, and maintainable system for dealing with failures. 3 | 4 | **We'll explore:** 5 | 6 | How AppFailure is implemented via NetworkFailure and LocalFailure. 7 | Why this approach is beneficial. 8 | What principles (like SOLID) are maintained by this approach. 9 | 10 | 11 | ## 1. How AppFailure is Implemented via NetworkFailure and LocalFailure 12 | This app's Failure Handling system starts with an abstract class called **AppFailure**, which acts as a base class for all types of errors this app might encounter. 13 | 14 | ### AppFailure Class (Base Class) 15 | ```dart 16 | abstract class AppFailure { 17 | final String message; 18 | 19 | AppFailure(this.message); 20 | 21 | @override 22 | String toString() => message; 23 | } 24 | ``` 25 | 26 | **Purpose:** AppFailure is an abstract class that represents a generic error in the app. It contains a single property, message, which holds the error message. By using an abstract class, you establish a contract that all specific failure types (e.g., NetworkFailure, LocalFailure) must adhere to, ensuring consistency. 27 | 28 | ### NetworkFailure Class 29 | ```dart 30 | class NetworkFailure implements AppFailure { 31 | final int? statusCode; 32 | 33 | NetworkFailure(String message, {this.statusCode}) : super(message); 34 | } 35 | ``` 36 | **Purpose:** NetworkFailure extends AppFailure and adds specific details about network-related errors. It includes an optional statusCode field to capture HTTP response codes (e.g., 404, 500) when an error occurs during an API call. 37 | 38 | **Use Case:** When a network request fails, such as a timeout or an invalid response from the server, NetworkFailure is used to represent that error. 39 | 40 | ### LocalFailure Class 41 | ```dart 42 | class LocalFailure implements AppFailure { 43 | LocalFailure(String message) : super(message); 44 | } 45 | ``` 46 | **Purpose:**LocalFailure represents errors that occur locally in the app, such as database issues, file read/write errors, or any other failures unrelated to network communication. 47 | 48 | **Use Case:** If there’s a problem with storing data on the device or a failure in local processing, you can use LocalFailure to capture and handle it accordingly. 49 | 50 | ### Example of Mapping Exceptions to Failures 51 | In your repository, when you catch an exception, you map it to a failure type based on the nature of the error. Here's an example: 52 | 53 | ```dart 54 | try { 55 | final response = await apiService.fetchData(); 56 | return Right(response); 57 | } catch (e) { 58 | if (e is DioError) { 59 | // Network error case 60 | return Left(FailureMapper.getFailures(e)); 61 | } else { 62 | // Local error case 63 | return Left(LocalFailure("An unexpected error occurred locally.")); 64 | } 65 | } 66 | ``` 67 | The FailureMapper utility converts different exceptions (e.g., DioError) into their appropriate failure types (e.g., NetworkFailure). 68 | 69 | ## 2. Why This Approach is Beneficial 70 | This failure-handling strategy provides several key benefits: 71 | 72 | - **A. Consistent and Centralized Error Handling** 73 | 74 | By using a centralized system with AppFailure, NetworkFailure, and LocalFailure, you ensure that errors are treated uniformly across the app. Instead of handling different error types in every repository or notifier, you can handle errors in a unified way, simplifying your codebase. 75 | 76 | For example, regardless of whether the error came from a network call or a local database failure, your UI can handle it in a consistent manner: 77 | 78 | ```dart 79 | state.fold( 80 | (failure) => showErrorDialog(failure.message), // Unified error handling 81 | (data) => showDataOnUI(data), 82 | ); 83 | ``` 84 | 85 | - **B. Easier Error Mapping and Debugging** 86 | 87 | Because each failure type is clearly defined (network errors vs. local errors), it becomes easier to debug issues. For example, NetworkFailure can contain additional details like status codes or specific messages from the server, making it easier to pinpoint the cause of the error. 88 | 89 | In your logs, you might see: 90 | 91 | ```go 92 | Network error: Status code 404 - Resource not found 93 | ``` 94 | versus: 95 | 96 | ```lua 97 | Local error: Database read failure 98 | ``` 99 | - **C. Extensibility** 100 | 101 | If you need to add more specific failure types in the future (e.g., *AuthenticationFailure*, *TimeoutFailure*), this architecture is easily extendable. Simply create a new class that extends AppFailure. 102 | 103 | ```dart 104 | class AuthenticationFailure extends AppFailure { 105 | AuthenticationFailure(String message) : super(message); 106 | } 107 | ``` 108 | This makes your app more flexible as it grows in complexity. 109 | 110 | - **D. Reusability>** 111 | 112 | With this structure, failure handling can be reused across different parts of your app. Any time you encounter a network or local error, you can use the same set of failure types without rewriting error handling logic for every feature. 113 | 114 | - **E. Separation of Concerns** 115 | 116 | Repositories focus solely on fetching data. 117 | Failure Handling is delegated to specific classes that represent different types of errors, maintaining clear boundaries between data fetching and error handling. 118 | 119 | ## 3. Principles Maintained by This Approach 120 | This failure-handling system adheres to several core principles of software design, especially those from the SOLID principles: 121 | 122 | - **A. Single Responsibility Principle (SRP)** 123 | Each class has one responsibility: 124 | 125 | AppFailure is responsible for representing a failure. 126 | NetworkFailure is responsible for representing network-related failures. 127 | LocalFailure is responsible for local failures. 128 | This makes your code more readable, maintainable, and easier to debug since each class has a focused purpose. 129 | 130 | - **B. Open-Closed Principle (OCP)** 131 | The system is open for extension but closed for modification. If you need to introduce a new failure type in the future (e.g., DatabaseFailure, PermissionFailure), you can extend AppFailure without changing the existing classes: 132 | 133 | dart 134 | Copy code 135 | class PermissionFailure extends AppFailure { 136 | PermissionFailure(String message) : super(message); 137 | } 138 | This prevents you from modifying existing, tested code while still allowing the system to grow. 139 | 140 | - **C. Liskov Substitution Principle (LSP)** 141 | Derived classes (NetworkFailure, LocalFailure, etc.) can be substituted for their base class (AppFailure). This ensures that any place expecting an AppFailure can handle all types of failures interchangeably. 142 | 143 | For example: 144 | 145 | ```dart 146 | void handleFailure(AppFailure failure) { 147 | print(failure.message); // Works regardless of the specific failure type. 148 | } 149 | ``` 150 | - **D. Dependency Inversion Principle (DIP)** 151 | High-level modules (like notifiers) depend on abstractions (AppFailure) rather than on low-level implementations (specific failure types). This allows the high-level business logic to remain decoupled from how failures are handled. 152 | 153 | - **E. Encapsulation and Abstraction** 154 | The details of network or local failures are encapsulated within their respective classes, hiding complexity from the rest of the app. The rest of the app simply deals with AppFailure, unaware of whether it's a network or local error, making the error handling mechanism abstract and easy to manage. 155 | 156 | ## Conclusion: Why This Approach is Ideal for Your App 157 | The failure-handling system you're using with AppFailure as a base class for NetworkFailure, LocalFailure, and potentially other failure types in the future is an excellent design choice. It ensures that: 158 | 159 | - Errors are consistently handled across the app, reducing redundancy and making the code more maintainable. 160 | - Debugging is easier due to specific failure types that provide detailed error information. 161 | - The system is flexible and can grow as the app becomes more complex. 162 | SOLID principles like Single Responsibility, Open-Closed, and Liskov Substitution are maintained, ensuring a clean, scalable architecture. 163 | - This robust failure-handling system allows your app to gracefully manage different types of errors, providing a better user experience and making the codebase easier to understand and maintain for developers at all levels. 164 | 165 | -------------------------------------------------------------------------------- /lib/src/feature/weather/domain/entity/weather_full_entity.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'weather_full_entity.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | /// @nodoc 18 | mixin _$WeatherFullEntity { 19 | List? get weather => throw _privateConstructorUsedError; 20 | MainInsideEntity? get main => throw _privateConstructorUsedError; 21 | WindInsideEntity? get wind => throw _privateConstructorUsedError; 22 | int? get dt => throw _privateConstructorUsedError; 23 | SysInsideEntity? get sys => throw _privateConstructorUsedError; 24 | String? get name => throw _privateConstructorUsedError; 25 | String? get cod => throw _privateConstructorUsedError; 26 | } 27 | 28 | /// @nodoc 29 | 30 | class _$WeatherFullEntityImpl extends _WeatherFullEntity { 31 | const _$WeatherFullEntityImpl( 32 | {final List? weather, 33 | this.main, 34 | this.wind, 35 | this.dt, 36 | this.sys, 37 | this.name, 38 | this.cod}) 39 | : _weather = weather, 40 | super._(); 41 | 42 | final List? _weather; 43 | @override 44 | List? get weather { 45 | final value = _weather; 46 | if (value == null) return null; 47 | if (_weather is EqualUnmodifiableListView) return _weather; 48 | // ignore: implicit_dynamic_type 49 | return EqualUnmodifiableListView(value); 50 | } 51 | 52 | @override 53 | final MainInsideEntity? main; 54 | @override 55 | final WindInsideEntity? wind; 56 | @override 57 | final int? dt; 58 | @override 59 | final SysInsideEntity? sys; 60 | @override 61 | final String? name; 62 | @override 63 | final String? cod; 64 | 65 | @override 66 | String toString() { 67 | return 'WeatherFullEntity(weather: $weather, main: $main, wind: $wind, dt: $dt, sys: $sys, name: $name, cod: $cod)'; 68 | } 69 | 70 | @override 71 | bool operator ==(Object other) { 72 | return identical(this, other) || 73 | (other.runtimeType == runtimeType && 74 | other is _$WeatherFullEntityImpl && 75 | const DeepCollectionEquality().equals(other._weather, _weather) && 76 | (identical(other.main, main) || other.main == main) && 77 | (identical(other.wind, wind) || other.wind == wind) && 78 | (identical(other.dt, dt) || other.dt == dt) && 79 | (identical(other.sys, sys) || other.sys == sys) && 80 | (identical(other.name, name) || other.name == name) && 81 | (identical(other.cod, cod) || other.cod == cod)); 82 | } 83 | 84 | @override 85 | int get hashCode => Object.hash( 86 | runtimeType, 87 | const DeepCollectionEquality().hash(_weather), 88 | main, 89 | wind, 90 | dt, 91 | sys, 92 | name, 93 | cod); 94 | } 95 | 96 | abstract class _WeatherFullEntity extends WeatherFullEntity { 97 | const factory _WeatherFullEntity( 98 | {final List? weather, 99 | final MainInsideEntity? main, 100 | final WindInsideEntity? wind, 101 | final int? dt, 102 | final SysInsideEntity? sys, 103 | final String? name, 104 | final String? cod}) = _$WeatherFullEntityImpl; 105 | const _WeatherFullEntity._() : super._(); 106 | 107 | @override 108 | List? get weather; 109 | @override 110 | MainInsideEntity? get main; 111 | @override 112 | WindInsideEntity? get wind; 113 | @override 114 | int? get dt; 115 | @override 116 | SysInsideEntity? get sys; 117 | @override 118 | String? get name; 119 | @override 120 | String? get cod; 121 | } 122 | 123 | /// @nodoc 124 | mixin _$MainInsideEntity { 125 | double? get temp => throw _privateConstructorUsedError; 126 | double? get feelsLike => throw _privateConstructorUsedError; 127 | int? get pressure => throw _privateConstructorUsedError; 128 | int? get humidity => throw _privateConstructorUsedError; 129 | } 130 | 131 | /// @nodoc 132 | 133 | class _$MainInsideEntityImpl extends _MainInsideEntity { 134 | const _$MainInsideEntityImpl( 135 | {this.temp, this.feelsLike, this.pressure, this.humidity}) 136 | : super._(); 137 | 138 | @override 139 | final double? temp; 140 | @override 141 | final double? feelsLike; 142 | @override 143 | final int? pressure; 144 | @override 145 | final int? humidity; 146 | 147 | @override 148 | String toString() { 149 | return 'MainInsideEntity(temp: $temp, feelsLike: $feelsLike, pressure: $pressure, humidity: $humidity)'; 150 | } 151 | 152 | @override 153 | bool operator ==(Object other) { 154 | return identical(this, other) || 155 | (other.runtimeType == runtimeType && 156 | other is _$MainInsideEntityImpl && 157 | (identical(other.temp, temp) || other.temp == temp) && 158 | (identical(other.feelsLike, feelsLike) || 159 | other.feelsLike == feelsLike) && 160 | (identical(other.pressure, pressure) || 161 | other.pressure == pressure) && 162 | (identical(other.humidity, humidity) || 163 | other.humidity == humidity)); 164 | } 165 | 166 | @override 167 | int get hashCode => 168 | Object.hash(runtimeType, temp, feelsLike, pressure, humidity); 169 | } 170 | 171 | abstract class _MainInsideEntity extends MainInsideEntity { 172 | const factory _MainInsideEntity( 173 | {final double? temp, 174 | final double? feelsLike, 175 | final int? pressure, 176 | final int? humidity}) = _$MainInsideEntityImpl; 177 | const _MainInsideEntity._() : super._(); 178 | 179 | @override 180 | double? get temp; 181 | @override 182 | double? get feelsLike; 183 | @override 184 | int? get pressure; 185 | @override 186 | int? get humidity; 187 | } 188 | 189 | /// @nodoc 190 | mixin _$SysInsideEntity { 191 | int? get sunrise => throw _privateConstructorUsedError; 192 | int? get sunset => throw _privateConstructorUsedError; 193 | } 194 | 195 | /// @nodoc 196 | 197 | class _$SysInsideEntityImpl extends _SysInsideEntity { 198 | const _$SysInsideEntityImpl({this.sunrise, this.sunset}) : super._(); 199 | 200 | @override 201 | final int? sunrise; 202 | @override 203 | final int? sunset; 204 | 205 | @override 206 | String toString() { 207 | return 'SysInsideEntity(sunrise: $sunrise, sunset: $sunset)'; 208 | } 209 | 210 | @override 211 | bool operator ==(Object other) { 212 | return identical(this, other) || 213 | (other.runtimeType == runtimeType && 214 | other is _$SysInsideEntityImpl && 215 | (identical(other.sunrise, sunrise) || other.sunrise == sunrise) && 216 | (identical(other.sunset, sunset) || other.sunset == sunset)); 217 | } 218 | 219 | @override 220 | int get hashCode => Object.hash(runtimeType, sunrise, sunset); 221 | } 222 | 223 | abstract class _SysInsideEntity extends SysInsideEntity { 224 | const factory _SysInsideEntity({final int? sunrise, final int? sunset}) = 225 | _$SysInsideEntityImpl; 226 | const _SysInsideEntity._() : super._(); 227 | 228 | @override 229 | int? get sunrise; 230 | @override 231 | int? get sunset; 232 | } 233 | 234 | /// @nodoc 235 | mixin _$WeatherInsideEntity { 236 | String? get main => throw _privateConstructorUsedError; 237 | String? get description => throw _privateConstructorUsedError; 238 | String? get icon => throw _privateConstructorUsedError; 239 | } 240 | 241 | /// @nodoc 242 | 243 | class _$WeatherInsideEntityImpl extends _WeatherInsideEntity { 244 | const _$WeatherInsideEntityImpl({this.main, this.description, this.icon}) 245 | : super._(); 246 | 247 | @override 248 | final String? main; 249 | @override 250 | final String? description; 251 | @override 252 | final String? icon; 253 | 254 | @override 255 | String toString() { 256 | return 'WeatherInsideEntity(main: $main, description: $description, icon: $icon)'; 257 | } 258 | 259 | @override 260 | bool operator ==(Object other) { 261 | return identical(this, other) || 262 | (other.runtimeType == runtimeType && 263 | other is _$WeatherInsideEntityImpl && 264 | (identical(other.main, main) || other.main == main) && 265 | (identical(other.description, description) || 266 | other.description == description) && 267 | (identical(other.icon, icon) || other.icon == icon)); 268 | } 269 | 270 | @override 271 | int get hashCode => Object.hash(runtimeType, main, description, icon); 272 | } 273 | 274 | abstract class _WeatherInsideEntity extends WeatherInsideEntity { 275 | const factory _WeatherInsideEntity( 276 | {final String? main, 277 | final String? description, 278 | final String? icon}) = _$WeatherInsideEntityImpl; 279 | const _WeatherInsideEntity._() : super._(); 280 | 281 | @override 282 | String? get main; 283 | @override 284 | String? get description; 285 | @override 286 | String? get icon; 287 | } 288 | 289 | /// @nodoc 290 | mixin _$WindInsideEntity { 291 | double? get speed => throw _privateConstructorUsedError; 292 | double? get gust => throw _privateConstructorUsedError; 293 | } 294 | 295 | /// @nodoc 296 | 297 | class _$WindInsideEntityImpl extends _WindInsideEntity { 298 | const _$WindInsideEntityImpl({this.speed, this.gust}) : super._(); 299 | 300 | @override 301 | final double? speed; 302 | @override 303 | final double? gust; 304 | 305 | @override 306 | String toString() { 307 | return 'WindInsideEntity(speed: $speed, gust: $gust)'; 308 | } 309 | 310 | @override 311 | bool operator ==(Object other) { 312 | return identical(this, other) || 313 | (other.runtimeType == runtimeType && 314 | other is _$WindInsideEntityImpl && 315 | (identical(other.speed, speed) || other.speed == speed) && 316 | (identical(other.gust, gust) || other.gust == gust)); 317 | } 318 | 319 | @override 320 | int get hashCode => Object.hash(runtimeType, speed, gust); 321 | } 322 | 323 | abstract class _WindInsideEntity extends WindInsideEntity { 324 | const factory _WindInsideEntity({final double? speed, final double? gust}) = 325 | _$WindInsideEntityImpl; 326 | const _WindInsideEntity._() : super._(); 327 | 328 | @override 329 | double? get speed; 330 | @override 331 | double? get gust; 332 | } 333 | -------------------------------------------------------------------------------- /lib/src/core/networking/interceptors/logging_interceptor.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'dart:math' as math; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:dio/dio.dart'; 7 | import 'package:weather_app/src/core/helper/colored_logger.dart'; 8 | 9 | enum Type { 10 | white, 11 | red, 12 | green, 13 | blue, 14 | magenta, 15 | } 16 | 17 | class PrettyDioLogger extends Interceptor { 18 | /// Print request [Options] 19 | final bool request; 20 | 21 | /// Print request header [Options.headers] 22 | final bool requestHeader; 23 | 24 | /// Print request data [Options.data] 25 | final bool requestBody; 26 | 27 | /// Print [Response.data] 28 | final bool responseBody; 29 | 30 | /// Print [Response.headers] 31 | final bool responseHeader; 32 | 33 | /// Print error message 34 | final bool error; 35 | 36 | /// InitialTab count to logPrint json response 37 | static const int kInitialTab = 1; 38 | 39 | /// 1 tab length 40 | static const String tabStep = ' '; 41 | 42 | /// Print compact json response 43 | final bool compact; 44 | 45 | /// Width size per logPrint 46 | final int maxWidth; 47 | 48 | /// Size in which the Uint8List will be splitted 49 | static const int chunkSize = 20; 50 | 51 | /// Log printer; defaults logPrint log to console. 52 | /// In flutter, you'd better use debugPrint. 53 | /// you can also write log in a file. 54 | final void Function(Object object) logPrint; 55 | 56 | PrettyDioLogger({ 57 | this.request = true, 58 | this.requestHeader = false, 59 | this.requestBody = false, 60 | this.responseHeader = false, 61 | this.responseBody = true, 62 | this.error = true, 63 | this.maxWidth = 90, 64 | this.compact = true, 65 | this.logPrint = print, 66 | }); 67 | 68 | @override 69 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 70 | if (request) { 71 | _printRequestHeader(options); 72 | } 73 | if (requestHeader) { 74 | _printMapAsTable(options.queryParameters, 75 | header: 'Query Parameters', type: Type.blue); 76 | final requestHeaders = {}; 77 | requestHeaders.addAll(options.headers); 78 | requestHeaders['contentType'] = options.contentType?.toString(); 79 | requestHeaders['responseType'] = options.responseType.toString(); 80 | requestHeaders['followRedirects'] = options.followRedirects; 81 | requestHeaders['connectTimeout'] = options.connectTimeout?.toString(); 82 | requestHeaders['receiveTimeout'] = options.receiveTimeout?.toString(); 83 | _printMapAsTable(requestHeaders, header: 'Headers', type: Type.blue); 84 | _printMapAsTable(options.extra, header: 'Extras', type: Type.blue); 85 | } 86 | if (requestBody && options.method != 'GET') { 87 | final dynamic data = options.data; 88 | if (data != null) { 89 | if (data is Map) { 90 | _printMapAsTable(options.data as Map?, 91 | header: 'Body', type: Type.magenta); 92 | } 93 | if (data is FormData) { 94 | final formDataMap = {} 95 | ..addEntries(data.fields) 96 | ..addEntries(data.files); 97 | _printMapAsTable(formDataMap, 98 | header: 'Form data | ${data.boundary}', type: Type.magenta); 99 | } else { 100 | _printBlock(data.toString(), Type.magenta); 101 | } 102 | } 103 | } 104 | super.onRequest(options, handler); 105 | } 106 | 107 | @override 108 | void onError(DioException err, ErrorInterceptorHandler handler) { 109 | String s = ColoredLogger.Red.emojiStart; 110 | String e = ColoredLogger.Red.emojiEnd; 111 | 112 | if (error) { 113 | if (err.type == DioExceptionType.badResponse) { 114 | final uri = err.response?.requestOptions.uri; 115 | _printBoxed( 116 | header: 117 | '$s DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage} $e', 118 | text: uri.toString(), 119 | type: Type.red, 120 | ); 121 | if (err.response != null && err.response?.data != null) { 122 | logPrint(' ╔ ${err.type.toString()}'); 123 | _printResponse( 124 | err.response!, 125 | Type.red, 126 | ); 127 | } 128 | _printLine( 129 | Type.red, 130 | ' ╚', 131 | ); 132 | logPrint(''); 133 | } else { 134 | _printBoxed( 135 | header: '$s DioError ║ ${err.type} $e', 136 | text: err.message, 137 | type: Type.red, 138 | ); 139 | } 140 | } 141 | super.onError(err, handler); 142 | } 143 | 144 | @override 145 | void onResponse(Response response, ResponseInterceptorHandler handler) { 146 | _printResponseHeader(response); 147 | if (responseHeader) { 148 | final responseHeaders = {}; 149 | response.headers 150 | .forEach((k, list) => responseHeaders[k] = list.toString()); 151 | _printMapAsTable(responseHeaders, header: 'Headers', type: Type.blue); 152 | } 153 | 154 | if (responseBody) { 155 | String s = ColoredLogger.Green.normalStart; 156 | String e = ColoredLogger.Green.normalEnd; 157 | logPrint('$s ╔ Body $e'); 158 | logPrint('$s ║ $e'); 159 | _printResponse(response, Type.green); 160 | logPrint('$s ║ $e'); 161 | _printLine(Type.green, ' ╚'); 162 | } 163 | super.onResponse(response, handler); 164 | } 165 | 166 | void _printBoxed({String? header, String? text, required Type type}) { 167 | final (s, e) = getStartEndColor(type); 168 | 169 | logPrint(''); 170 | logPrint('$s ╔╣ $header $e'); 171 | logPrint('$s ║ $text $e'); 172 | _printLine(type, ' ╚'); 173 | } 174 | 175 | void _printResponse(Response response, Type type) { 176 | final (s, e) = getStartEndColor(type); 177 | if (response.data != null) { 178 | if (response.data is Map) { 179 | _printPrettyMap( 180 | response.data as Map, 181 | type: type, 182 | ); 183 | } else if (response.data is Uint8List) { 184 | logPrint('$s ║$e${_indent()}['); 185 | _printUint8List(response.data as Uint8List); 186 | logPrint('$s ║$e${_indent()}]'); 187 | } else if (response.data is List) { 188 | logPrint('$s ║$e${_indent()}['); 189 | _printList(response.data as List); 190 | logPrint('$s ║$e${_indent()}]'); 191 | } else { 192 | _printBlock(response.data.toString(), type); 193 | } 194 | } 195 | } 196 | 197 | void _printResponseHeader(Response response) { 198 | final uri = response.requestOptions.uri; 199 | final method = response.requestOptions.method; 200 | _printBoxed( 201 | header: 202 | '${ColoredLogger.Green.emojiStart} Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage} ${ColoredLogger.Green.emojiEnd}', 203 | text: uri.toString(), 204 | type: Type.green, 205 | ); 206 | } 207 | 208 | void _printRequestHeader(RequestOptions options) { 209 | final uri = options.uri; 210 | final method = options.method; 211 | _printBoxed( 212 | header: 213 | '${ColoredLogger.White.emojiStart} Request ║ $method ${ColoredLogger.White.emojiEnd}', 214 | text: 215 | '${ColoredLogger.White.emojiStart} ${uri.toString()} ${ColoredLogger.White.emojiEnd}', 216 | type: Type.white, 217 | ); 218 | } 219 | 220 | void _printLine(Type type, [String pre = '', String suf = '╝']) { 221 | final (s, e) = getStartEndColor(type); 222 | logPrint('$s$pre${'═' * maxWidth}$suf$e'); 223 | } 224 | 225 | void _printKV(String? key, Object? v) { 226 | final pre = ' ╟ $key: '; 227 | final msg = v.toString(); 228 | 229 | if (pre.length + msg.length > maxWidth) { 230 | logPrint(pre); 231 | _printBlock(msg, Type.white); 232 | } else { 233 | logPrint('$pre$msg'); 234 | } 235 | } 236 | 237 | void _printBlock(String msg, Type type) { 238 | final (s, e) = getStartEndColor(type); 239 | final lines = (msg.length / maxWidth).ceil(); 240 | for (var i = 0; i < lines; ++i) { 241 | logPrint((i >= 0 ? '$s ║ $e' : '') + 242 | msg.substring(i * maxWidth, 243 | math.min(i * maxWidth + maxWidth, msg.length))); 244 | } 245 | } 246 | 247 | String _indent([int tabCount = kInitialTab]) => tabStep * tabCount; 248 | 249 | void _printPrettyMap( 250 | Map data, { 251 | int initialTab = kInitialTab, 252 | bool isListItem = false, 253 | bool isLast = false, 254 | required Type type, 255 | }) { 256 | var tabs = initialTab; 257 | final isRoot = tabs == kInitialTab; 258 | final initialIndent = _indent(tabs); 259 | tabs++; 260 | 261 | final (s, e) = getStartEndColor(type); 262 | 263 | if (isRoot || isListItem) logPrint('$s ║$e$initialIndent{'); 264 | 265 | data.keys.toList().asMap().forEach((index, dynamic key) { 266 | final isLast = index == data.length - 1; 267 | dynamic value = data[key]; 268 | if (value is String) { 269 | value = '"${value.toString().replaceAll(RegExp(r'([\r\n])+'), " ")}"'; 270 | } 271 | if (value is Map) { 272 | if (compact && _canFlattenMap(value)) { 273 | logPrint('$s ║$e${_indent(tabs)} $key: $value${!isLast ? ',' : ''}'); 274 | } else { 275 | logPrint('$s ║$e${_indent(tabs)} $key: {'); 276 | _printPrettyMap( 277 | value, 278 | initialTab: tabs, 279 | type: type, 280 | ); 281 | } 282 | } else if (value is List) { 283 | if (compact && _canFlattenList(value)) { 284 | logPrint('$s ║$e${_indent(tabs)} $key: ${value.toString()}'); 285 | } else { 286 | logPrint('$s ║$e${_indent(tabs)} $key: ['); 287 | _printList(value, tabs: tabs); 288 | logPrint('$s ║$e${_indent(tabs)} ]${isLast ? '' : ','}'); 289 | } 290 | } else { 291 | final msg = value.toString().replaceAll('\n', ''); 292 | final indent = _indent(tabs); 293 | final linWidth = maxWidth - indent.length; 294 | if (msg.length + indent.length > linWidth) { 295 | final lines = (msg.length / linWidth).ceil(); 296 | for (var i = 0; i < lines; ++i) { 297 | logPrint( 298 | '$s ║$e${_indent(tabs)} ${msg.substring(i * linWidth, math.min(i * linWidth + linWidth, msg.length))}'); 299 | } 300 | } else { 301 | logPrint('$s ║$e${_indent(tabs)} $key: $msg${!isLast ? ',' : ''}'); 302 | } 303 | } 304 | }); 305 | 306 | logPrint('$s ║$initialIndent} $e ${isListItem && !isLast ? ',' : ''}'); 307 | } 308 | 309 | void _printList(List list, {int tabs = kInitialTab}) { 310 | list.asMap().forEach((i, dynamic ee) { 311 | final isLast = i == list.length - 1; 312 | if (ee is Map) { 313 | if (compact && _canFlattenMap(ee)) { 314 | logPrint(' ║${_indent(tabs)} $ee${!isLast ? ',' : ''}'); 315 | } else { 316 | _printPrettyMap( 317 | ee, 318 | initialTab: tabs + 1, 319 | isListItem: true, 320 | isLast: isLast, 321 | type: Type.green, 322 | ); 323 | } 324 | } else { 325 | logPrint(' ║${_indent(tabs + 2)}${isLast ? '' : ','}'); 326 | } 327 | }); 328 | } 329 | 330 | void _printUint8List(Uint8List list, {int tabs = kInitialTab}) { 331 | var chunks = []; 332 | for (var i = 0; i < list.length; i += chunkSize) { 333 | chunks.add( 334 | list.sublist( 335 | i, i + chunkSize > list.length ? list.length : i + chunkSize), 336 | ); 337 | } 338 | for (var element in chunks) { 339 | logPrint(' ║${_indent(tabs)} ${element.join(", ")}'); 340 | } 341 | } 342 | 343 | bool _canFlattenMap(Map map) { 344 | return map.values 345 | .where((dynamic val) => val is Map || val is List) 346 | .isEmpty && 347 | map.toString().length < maxWidth; 348 | } 349 | 350 | bool _canFlattenList(List list) { 351 | return list.length < 10 && list.toString().length < maxWidth; 352 | } 353 | 354 | void _printMapAsTable(Map? map, {String? header, required Type type}) { 355 | if (map == null || map.isEmpty) return; 356 | logPrint(' ╔ $header '); 357 | map.forEach( 358 | (dynamic key, dynamic value) => _printKV(key.toString(), value)); 359 | _printLine(type, ' ╚'); 360 | } 361 | 362 | (String, String) getStartEndColor(Type type) { 363 | String s = switch (type) { 364 | Type.green => ColoredLogger.Green.normalStart, 365 | Type.red => ColoredLogger.Red.normalStart, 366 | Type.white => ColoredLogger.White.normalStart, 367 | Type.blue => ColoredLogger.Blue.normalStart, 368 | Type.magenta => ColoredLogger.Magenta.normalStart, 369 | }; 370 | String e = switch (type) { 371 | Type.green => ColoredLogger.Green.normalEnd, 372 | Type.red => ColoredLogger.Red.normalEnd, 373 | Type.white => ColoredLogger.White.normalEnd, 374 | Type.blue => ColoredLogger.Blue.normalEnd, 375 | Type.magenta => ColoredLogger.Magenta.normalEnd, 376 | }; 377 | return (s, e); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Study of Network Flow 2 | 3 | Figma design of flow chart - https://www.figma.com/board/oyPZGn7pFEZciS5zQR3bzv/Untitled?node-id=0-1&t=hCvKgekUWtBBRIiK-1 4 | 5 |  6 | 7 | 8 | 9 | ### **Overall Network Data Flow in Your Flutter App** 10 | 11 | 1. **UI Layer Initiates a Request**: 12 | - The user interacts with the app, triggering an action that requires data from the network (e.g., fetching weather information for a city). 13 | - This action is captured in the **Presentation Layer**, specifically in the UI widgets or screens. 14 | 15 | 2. **State Notifier Handles the Request**: 16 | - The UI calls a method in the **Notifier** (e.g., `getWeather` in `WeatherNotifier`). 17 | - The notifier is part of the **Application Layer** and is responsible for managing state and business logic. 18 | - It updates the state to a loading state to reflect that a network request is in progress. 19 | 20 | 3. **Notifier Interacts with the Repository**: 21 | - The notifier calls the appropriate method in the **Repository** (e.g., `getWeather` in `WeatherRepositoryImpl`). 22 | - This call goes through the **Domain Layer**, which defines the abstract `WeatherRepository` interface. The notifier depends on this abstraction, not on the concrete implementation. 23 | 24 | 4. **Repository Makes Network Call via NetworkApiService**: 25 | - The repository uses the **NetworkApiService** (Retrofit interface) to make the actual network call. 26 | - It constructs the request with necessary parameters (city name, API key, units). 27 | 28 | 5. **Dio and Interceptors Process the Request**: 29 | - The **Dio** HTTP client sends the request. 30 | - The **ApiInterceptor** intercepts the request to: 31 | - Log request details (URL, headers, method). 32 | - Add or modify headers if needed (e.g., authentication tokens). 33 | - When the response comes back, the interceptor: 34 | - Logs response details. 35 | - Checks for HTTP error codes (e.g., 4xx or 5xx) and converts them into exceptions using `handler.reject`. 36 | 37 | 6. **Handling Responses and Errors in Repository**: 38 | - The repository receives the response or catches an exception. 39 | - In the `try` block: 40 | - If the response is successful, it logs the response and returns a `Right(response)` wrapped in an `Either` type, indicating success. 41 | - In the `catch` block: 42 | - If an exception occurs, it uses `FailureMapper.getFailures(e)` to convert the exception into a well-defined `AppFailure`. 43 | - Returns a `Left(failure)` indicating an error. 44 | 45 | 7. **Notifier Updates State Based on Repository Result**: 46 | - The notifier receives the `Either` result from the repository. 47 | - It uses pattern matching (`fold`) to handle both cases: 48 | - On **Success** (`Right`): 49 | - Converts the `WeatherDTO` into a domain entity (`WeatherFullEntity`) using a factory constructor or mapper. 50 | - Updates the state to `ApiRequestState.data(data: weatherEntity)`. 51 | - On **Failure** (`Left`): 52 | - Updates the state to `ApiRequestState.failed(reason: failure)`. 53 | - Logs the failure for debugging purposes. 54 | 55 | 8. **UI Reacts to State Changes**: 56 | - The UI listens to changes in the notifier's state using Riverpod's `ref.watch`. 57 | - Depending on the state: 58 | - **Loading**: Shows a loading indicator. 59 | - **Data**: Displays the weather information to the user. 60 | - **Failed**: Shows an error message, possibly with options to retry. 61 | 62 | 9. **Error Handling and User Feedback**: 63 | - When a failure occurs, the UI can use the `FailureHandler` to display error dialogs or messages. 64 | - The error messages are user-friendly and can be specific based on the type of failure (network error, server error, etc.). 65 | 66 | ### **Understanding Each Layer's Responsibility** 67 | 68 | - **Presentation Layer**: 69 | - Handles UI components and user interactions. 70 | - Should be free of business logic and data-fetching code. 71 | - Reacts to state changes provided by the notifier. 72 | 73 | - **Application Layer (Notifiers and Providers)**: 74 | - Manages state and contains business logic related to state changes. 75 | - Interacts with the domain layer to perform actions like fetching data. 76 | - Updates the state based on the result of these actions. 77 | 78 | - **Domain Layer (Repositories and Entities)**: 79 | - Defines the core business logic and models (entities). 80 | - Contains abstract repositories that define what operations are available. 81 | - Entities are pure Dart objects representing the data in your app. 82 | 83 | - **Infrastructure Layer (Data Access and DTOs)**: 84 | - Implements the repositories defined in the domain layer. 85 | - Handles data fetching from external sources (APIs, databases). 86 | - Converts data transfer objects (DTOs) into domain entities. 87 | 88 | - **Core Layer**: 89 | - Contains shared utilities, such as error handling, logging, and networking setup. 90 | - Provides consistent mechanisms for dealing with failures and logging throughout the app. 91 | 92 | ### **Applying SOLID Principles** 93 | 94 | - **Single Responsibility Principle (SRP)**: 95 | - Each class and module has one responsibility. 96 | - For example, `WeatherRepositoryImpl` is only responsible for fetching weather data. 97 | 98 | - **Open-Closed Principle (OCP)**: 99 | - Classes are open for extension but closed for modification. 100 | - New features can be added without changing existing code, reducing the risk of introducing bugs. 101 | 102 | - **Liskov Substitution Principle (LSP)**: 103 | - Subclasses or implementations should be substitutable for their base types. 104 | - `WeatherRepositoryImpl` can be used anywhere `WeatherRepository` is expected. 105 | 106 | - **Interface Segregation Principle (ISP)**: 107 | - Clients should not be forced to depend on interfaces they do not use. 108 | - By defining specific interfaces for repositories, notifiers depend only on what they need. 109 | 110 | - **Dependency Inversion Principle (DIP)**: 111 | - High-level modules should not depend on low-level modules; both should depend on abstractions. 112 | - The notifier depends on the abstract `WeatherRepository` interface, not the concrete implementation. 113 | 114 | ### **Benefits of This Approach** 115 | 116 | - **Maintainability**: 117 | - Clear separation of concerns makes the code easier to understand and maintain. 118 | - Changes in one layer have minimal impact on others. 119 | 120 | - **Testability**: 121 | - Each component can be tested in isolation. 122 | - Mock implementations can be provided for repositories during testing. 123 | 124 | - **Scalability**: 125 | - The app can grow in features without becoming unmanageable. 126 | - New features can follow the same structural patterns. 127 | 128 | - **Reusability**: 129 | - Common utilities and patterns in the core layer can be reused across different features. 130 | 131 | ### **Real-Life Example: Fetching Weather Data** 132 | 133 | Let's walk through a real-life example using your app: 134 | 135 | 1. **User Action**: 136 | - The user enters a city name and taps a button to fetch the weather. 137 | 138 | 2. **UI Calls Notifier**: 139 | - The UI calls `getWeather(cityName)` on the `WeatherNotifier`. 140 | 141 | 3. **Notifier Updates State to Loading**: 142 | - The `WeatherNotifier` sets the state to `ApiRequestState.loading()` to show a loading indicator. 143 | 144 | 4. **Notifier Requests Data from Repository**: 145 | - The notifier calls `getWeather(cityName)` on the `WeatherRepositoryImpl`. 146 | 147 | 5. **Repository Fetches Data from API**: 148 | - The repository uses `NetworkApiService` to make an API call via Retrofit and Dio. 149 | 150 | 6. **ApiInterceptor Processes Request**: 151 | - The `ApiInterceptor` logs the request and adds any necessary headers. 152 | 153 | 7. **Response Handling**: 154 | - On receiving the response: 155 | - If successful, the data is returned to the repository. 156 | - If there's an error (e.g., 404 Not Found), the interceptor rejects the request, and an exception is thrown. 157 | 158 | 8. **Repository Handles Exceptions**: 159 | - The repository catches exceptions and uses `FailureMapper` to convert them into `AppFailure` instances. 160 | 161 | 9. **Notifier Updates State Based on Result**: 162 | - If data is received, the notifier updates the state to `ApiRequestState.data()` with the weather entity. 163 | - If an error occurs, the notifier updates the state to `ApiRequestState.failed()` with the failure reason. 164 | 165 | 10. **UI Reacts to State Change**: 166 | - The UI observes the notifier's state and updates accordingly: 167 | - Displays weather information on success. 168 | - Shows an error message on failure. 169 | 170 | ### **Conclusion** 171 | 172 | Your app's architecture effectively separates concerns, handles errors gracefully, and ensures that network data flows smoothly from the API to the UI. By adhering to clean architecture principles and SOLID design patterns, you've created a scalable and maintainable app structure that can be easily understood and extended by developers at any level. 173 | 174 | --- 175 | 176 | **Next Steps or Further Assistance** 177 | 178 | - **Documentation**: Consider adding comments and documentation throughout your code to help new developers understand each component's role. 179 | - **Testing**: Implement unit tests for your notifiers, repositories, and other critical components to ensure reliability. 180 | - **Error Messages**: Enhance user-facing error messages to be more descriptive and helpful. 181 | 182 | If you have any specific questions or need clarification on any part of this flow or architecture, feel free to ask! 183 | 184 | 185 | 186 | 187 | 188 | 189 | # Architecture 190 | 191 | | Images | 192 | |--------------------------------| 193 | |  *Figure 1: Architecture Overview* | 194 | |  *Figure 2: Router* | 195 | |  *Figure 3: single feature structure* | 196 | |  *Figure 4: Core structure* | 197 | |  *Figure 5: Full app feature folder structure* | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | # UI 210 | 211 | 212 | Landing Page 213 | Landing Page Tap to see more 214 | Search by city 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | Search Result 1 223 | Search Result 2 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | Search with wrong cityname 233 | Error Search Result 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | # Problems I've faced while completing this app 261 | 262 | 263 | ## 1. Hiding credentials 264 | 265 | > Soln: 266 | > git add . , then check the path of the tracked file you don't want to push. copy paste to .gitignore 267 | 268 | ## 2. Platform Exception 269 | 270 | > Soln: 271 | > Restart the device 272 | 273 | ## 3. StateNotifier multiple state problem 274 | 275 | > Solved: 276 | > Use Different function that returs different state. 277 | [Riverpod Video Tutorial](https://www.youtube.com/playlist?list=PL1WkZqhlAdC-GNyxQbfn8Db9pR6bRcQuw) 278 | 279 | ## 4. More 280 | 281 | - AsyncValue with FutureProvider 282 | - https://stackoverflow.com/questions/67582335/how-to-get-the-old-state-before-updating-the-state-in-state-notifier-riverpod 283 | - my git queries - https://github.com/rrousselGit/river_pod/issues/212 284 | - await on asyncValue - https://stackoverflow.com/questions/66411312/riverpod-how-to-await-using-futureprovider-on-asyncvalue-not-in-widget/66955043#66955043 285 | - cached refresh indicator 286 | https://github.com/rrousselGit/river_pod/issues/461 287 | - Unhandled Exception: setState() or markNeedsBuild() called during build 288 | https://github.com/rrousselGit/river_pod/issues/177 289 | 290 | ## Know about riverpod (written tutorial) 291 | 292 | https://codewithandrea.com/videos/flutter-state-management-riverpod/ 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /lib/src/feature/weather/infrastructure/dto/weather_model/weather_dto.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'weather_dto.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 16 | 17 | WeatherDTO _$WeatherDTOFromJson(Map json) { 18 | return _WeatherDTO.fromJson(json); 19 | } 20 | 21 | /// @nodoc 22 | mixin _$WeatherDTO { 23 | List? get weather => throw _privateConstructorUsedError; 24 | Main? get main => throw _privateConstructorUsedError; 25 | Wind? get wind => throw _privateConstructorUsedError; 26 | int? get dt => throw _privateConstructorUsedError; 27 | Sys? get sys => throw _privateConstructorUsedError; 28 | String? get name => throw _privateConstructorUsedError; 29 | @StringConverter() 30 | String? get cod => throw _privateConstructorUsedError; 31 | 32 | /// Serializes this WeatherDTO to a JSON map. 33 | Map toJson() => throw _privateConstructorUsedError; 34 | } 35 | 36 | /// @nodoc 37 | @JsonSerializable() 38 | class _$WeatherDTOImpl implements _WeatherDTO { 39 | const _$WeatherDTOImpl( 40 | {final List? weather, 41 | this.main, 42 | this.wind, 43 | this.dt, 44 | this.sys, 45 | this.name, 46 | @StringConverter() this.cod}) 47 | : _weather = weather; 48 | 49 | factory _$WeatherDTOImpl.fromJson(Map json) => 50 | _$$WeatherDTOImplFromJson(json); 51 | 52 | final List? _weather; 53 | @override 54 | List? get weather { 55 | final value = _weather; 56 | if (value == null) return null; 57 | if (_weather is EqualUnmodifiableListView) return _weather; 58 | // ignore: implicit_dynamic_type 59 | return EqualUnmodifiableListView(value); 60 | } 61 | 62 | @override 63 | final Main? main; 64 | @override 65 | final Wind? wind; 66 | @override 67 | final int? dt; 68 | @override 69 | final Sys? sys; 70 | @override 71 | final String? name; 72 | @override 73 | @StringConverter() 74 | final String? cod; 75 | 76 | @override 77 | String toString() { 78 | return 'WeatherDTO(weather: $weather, main: $main, wind: $wind, dt: $dt, sys: $sys, name: $name, cod: $cod)'; 79 | } 80 | 81 | @override 82 | bool operator ==(Object other) { 83 | return identical(this, other) || 84 | (other.runtimeType == runtimeType && 85 | other is _$WeatherDTOImpl && 86 | const DeepCollectionEquality().equals(other._weather, _weather) && 87 | (identical(other.main, main) || other.main == main) && 88 | (identical(other.wind, wind) || other.wind == wind) && 89 | (identical(other.dt, dt) || other.dt == dt) && 90 | (identical(other.sys, sys) || other.sys == sys) && 91 | (identical(other.name, name) || other.name == name) && 92 | (identical(other.cod, cod) || other.cod == cod)); 93 | } 94 | 95 | @JsonKey(includeFromJson: false, includeToJson: false) 96 | @override 97 | int get hashCode => Object.hash( 98 | runtimeType, 99 | const DeepCollectionEquality().hash(_weather), 100 | main, 101 | wind, 102 | dt, 103 | sys, 104 | name, 105 | cod); 106 | 107 | @override 108 | Map toJson() { 109 | return _$$WeatherDTOImplToJson( 110 | this, 111 | ); 112 | } 113 | } 114 | 115 | abstract class _WeatherDTO implements WeatherDTO { 116 | const factory _WeatherDTO( 117 | {final List? weather, 118 | final Main? main, 119 | final Wind? wind, 120 | final int? dt, 121 | final Sys? sys, 122 | final String? name, 123 | @StringConverter() final String? cod}) = _$WeatherDTOImpl; 124 | 125 | factory _WeatherDTO.fromJson(Map json) = 126 | _$WeatherDTOImpl.fromJson; 127 | 128 | @override 129 | List? get weather; 130 | @override 131 | Main? get main; 132 | @override 133 | Wind? get wind; 134 | @override 135 | int? get dt; 136 | @override 137 | Sys? get sys; 138 | @override 139 | String? get name; 140 | @override 141 | @StringConverter() 142 | String? get cod; 143 | } 144 | 145 | Main _$MainFromJson(Map json) { 146 | return _Main.fromJson(json); 147 | } 148 | 149 | /// @nodoc 150 | mixin _$Main { 151 | double? get temp => throw _privateConstructorUsedError; 152 | double? get feelsLike => throw _privateConstructorUsedError; 153 | int? get pressure => throw _privateConstructorUsedError; 154 | int? get humidity => throw _privateConstructorUsedError; 155 | 156 | /// Serializes this Main to a JSON map. 157 | Map toJson() => throw _privateConstructorUsedError; 158 | } 159 | 160 | /// @nodoc 161 | @JsonSerializable() 162 | class _$MainImpl implements _Main { 163 | const _$MainImpl({this.temp, this.feelsLike, this.pressure, this.humidity}); 164 | 165 | factory _$MainImpl.fromJson(Map json) => 166 | _$$MainImplFromJson(json); 167 | 168 | @override 169 | final double? temp; 170 | @override 171 | final double? feelsLike; 172 | @override 173 | final int? pressure; 174 | @override 175 | final int? humidity; 176 | 177 | @override 178 | String toString() { 179 | return 'Main(temp: $temp, feelsLike: $feelsLike, pressure: $pressure, humidity: $humidity)'; 180 | } 181 | 182 | @override 183 | bool operator ==(Object other) { 184 | return identical(this, other) || 185 | (other.runtimeType == runtimeType && 186 | other is _$MainImpl && 187 | (identical(other.temp, temp) || other.temp == temp) && 188 | (identical(other.feelsLike, feelsLike) || 189 | other.feelsLike == feelsLike) && 190 | (identical(other.pressure, pressure) || 191 | other.pressure == pressure) && 192 | (identical(other.humidity, humidity) || 193 | other.humidity == humidity)); 194 | } 195 | 196 | @JsonKey(includeFromJson: false, includeToJson: false) 197 | @override 198 | int get hashCode => 199 | Object.hash(runtimeType, temp, feelsLike, pressure, humidity); 200 | 201 | @override 202 | Map toJson() { 203 | return _$$MainImplToJson( 204 | this, 205 | ); 206 | } 207 | } 208 | 209 | abstract class _Main implements Main { 210 | const factory _Main( 211 | {final double? temp, 212 | final double? feelsLike, 213 | final int? pressure, 214 | final int? humidity}) = _$MainImpl; 215 | 216 | factory _Main.fromJson(Map json) = _$MainImpl.fromJson; 217 | 218 | @override 219 | double? get temp; 220 | @override 221 | double? get feelsLike; 222 | @override 223 | int? get pressure; 224 | @override 225 | int? get humidity; 226 | } 227 | 228 | Sys _$SysFromJson(Map json) { 229 | return _Sys.fromJson(json); 230 | } 231 | 232 | /// @nodoc 233 | mixin _$Sys { 234 | int? get sunrise => throw _privateConstructorUsedError; 235 | int? get sunset => throw _privateConstructorUsedError; 236 | 237 | /// Serializes this Sys to a JSON map. 238 | Map toJson() => throw _privateConstructorUsedError; 239 | } 240 | 241 | /// @nodoc 242 | @JsonSerializable() 243 | class _$SysImpl implements _Sys { 244 | const _$SysImpl({this.sunrise, this.sunset}); 245 | 246 | factory _$SysImpl.fromJson(Map json) => 247 | _$$SysImplFromJson(json); 248 | 249 | @override 250 | final int? sunrise; 251 | @override 252 | final int? sunset; 253 | 254 | @override 255 | String toString() { 256 | return 'Sys(sunrise: $sunrise, sunset: $sunset)'; 257 | } 258 | 259 | @override 260 | bool operator ==(Object other) { 261 | return identical(this, other) || 262 | (other.runtimeType == runtimeType && 263 | other is _$SysImpl && 264 | (identical(other.sunrise, sunrise) || other.sunrise == sunrise) && 265 | (identical(other.sunset, sunset) || other.sunset == sunset)); 266 | } 267 | 268 | @JsonKey(includeFromJson: false, includeToJson: false) 269 | @override 270 | int get hashCode => Object.hash(runtimeType, sunrise, sunset); 271 | 272 | @override 273 | Map toJson() { 274 | return _$$SysImplToJson( 275 | this, 276 | ); 277 | } 278 | } 279 | 280 | abstract class _Sys implements Sys { 281 | const factory _Sys({final int? sunrise, final int? sunset}) = _$SysImpl; 282 | 283 | factory _Sys.fromJson(Map json) = _$SysImpl.fromJson; 284 | 285 | @override 286 | int? get sunrise; 287 | @override 288 | int? get sunset; 289 | } 290 | 291 | Weather _$WeatherFromJson(Map json) { 292 | return _Weather.fromJson(json); 293 | } 294 | 295 | /// @nodoc 296 | mixin _$Weather { 297 | String? get main => throw _privateConstructorUsedError; 298 | String? get description => throw _privateConstructorUsedError; 299 | String? get icon => throw _privateConstructorUsedError; 300 | 301 | /// Serializes this Weather to a JSON map. 302 | Map toJson() => throw _privateConstructorUsedError; 303 | } 304 | 305 | /// @nodoc 306 | @JsonSerializable() 307 | class _$WeatherImpl implements _Weather { 308 | const _$WeatherImpl({this.main, this.description, this.icon}); 309 | 310 | factory _$WeatherImpl.fromJson(Map json) => 311 | _$$WeatherImplFromJson(json); 312 | 313 | @override 314 | final String? main; 315 | @override 316 | final String? description; 317 | @override 318 | final String? icon; 319 | 320 | @override 321 | String toString() { 322 | return 'Weather(main: $main, description: $description, icon: $icon)'; 323 | } 324 | 325 | @override 326 | bool operator ==(Object other) { 327 | return identical(this, other) || 328 | (other.runtimeType == runtimeType && 329 | other is _$WeatherImpl && 330 | (identical(other.main, main) || other.main == main) && 331 | (identical(other.description, description) || 332 | other.description == description) && 333 | (identical(other.icon, icon) || other.icon == icon)); 334 | } 335 | 336 | @JsonKey(includeFromJson: false, includeToJson: false) 337 | @override 338 | int get hashCode => Object.hash(runtimeType, main, description, icon); 339 | 340 | @override 341 | Map toJson() { 342 | return _$$WeatherImplToJson( 343 | this, 344 | ); 345 | } 346 | } 347 | 348 | abstract class _Weather implements Weather { 349 | const factory _Weather( 350 | {final String? main, 351 | final String? description, 352 | final String? icon}) = _$WeatherImpl; 353 | 354 | factory _Weather.fromJson(Map json) = _$WeatherImpl.fromJson; 355 | 356 | @override 357 | String? get main; 358 | @override 359 | String? get description; 360 | @override 361 | String? get icon; 362 | } 363 | 364 | Wind _$WindFromJson(Map json) { 365 | return _Wind.fromJson(json); 366 | } 367 | 368 | /// @nodoc 369 | mixin _$Wind { 370 | double? get speed => throw _privateConstructorUsedError; 371 | double? get gust => throw _privateConstructorUsedError; 372 | 373 | /// Serializes this Wind to a JSON map. 374 | Map toJson() => throw _privateConstructorUsedError; 375 | } 376 | 377 | /// @nodoc 378 | @JsonSerializable() 379 | class _$WindImpl implements _Wind { 380 | const _$WindImpl({this.speed, this.gust}); 381 | 382 | factory _$WindImpl.fromJson(Map json) => 383 | _$$WindImplFromJson(json); 384 | 385 | @override 386 | final double? speed; 387 | @override 388 | final double? gust; 389 | 390 | @override 391 | String toString() { 392 | return 'Wind(speed: $speed, gust: $gust)'; 393 | } 394 | 395 | @override 396 | bool operator ==(Object other) { 397 | return identical(this, other) || 398 | (other.runtimeType == runtimeType && 399 | other is _$WindImpl && 400 | (identical(other.speed, speed) || other.speed == speed) && 401 | (identical(other.gust, gust) || other.gust == gust)); 402 | } 403 | 404 | @JsonKey(includeFromJson: false, includeToJson: false) 405 | @override 406 | int get hashCode => Object.hash(runtimeType, speed, gust); 407 | 408 | @override 409 | Map toJson() { 410 | return _$$WindImplToJson( 411 | this, 412 | ); 413 | } 414 | } 415 | 416 | abstract class _Wind implements Wind { 417 | const factory _Wind({final double? speed, final double? gust}) = _$WindImpl; 418 | 419 | factory _Wind.fromJson(Map json) = _$WindImpl.fromJson; 420 | 421 | @override 422 | double? get speed; 423 | @override 424 | double? get gust; 425 | } 426 | --------------------------------------------------------------------------------