diff --git a/.gitignore b/.gitignore
index 0ea4711..6b8a6e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,7 @@ gen-external-apklibs
/out/
# User-specific configurations
+.idea/androidTestResultsUserPreferences.xml
.idea/caches/
.idea/libraries/
.idea/shelf/
@@ -94,5 +95,3 @@ gen-external-apklibs
/fastlane/report.xml
# Google play files
/google-play-key.json
-
-/.idea/androidTestResultsUserPreferences.xml
diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
deleted file mode 100644
index 66e1e63..0000000
--- a/.idea/androidTestResultsUserPreferences.xml
+++ /dev/null
@@ -1,126 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..c7706ea
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,218 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.6)
+ rexml
+ addressable (2.8.5)
+ public_suffix (>= 2.0.2, < 6.0)
+ artifactory (3.0.15)
+ atomos (0.1.3)
+ aws-eventstream (1.2.0)
+ aws-partitions (1.798.0)
+ aws-sdk-core (3.180.1)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.651.0)
+ aws-sigv4 (~> 1.5)
+ jmespath (~> 1, >= 1.6.1)
+ aws-sdk-kms (1.71.0)
+ aws-sdk-core (~> 3, >= 3.177.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.132.0)
+ aws-sdk-core (~> 3, >= 3.179.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.6)
+ aws-sigv4 (1.6.0)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ declarative (0.0.20)
+ digest-crc (0.6.5)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.5.20190701)
+ unf (>= 0.0.5, < 1.0.0)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.100.0)
+ faraday (1.10.3)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
+ http-cookie (~> 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.0)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
+ faraday-net_http (1.0.1)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.0)
+ faraday (~> 1.0)
+ fastimage (2.2.7)
+ fastlane (2.214.0)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ bundler (>= 1.12.0, < 3.0.0)
+ colored
+ commander (~> 4.6)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ naturally (~> 2.2)
+ optparse (~> 0.1.1)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.3)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (>= 1.4.5, < 2.0.0)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.3.0)
+ xcpretty-travis-formatter (>= 0.0.3)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.46.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-core (0.11.1)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ webrick
+ google-apis-iamcredentials_v1 (0.17.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.13.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-storage_v1 (0.19.0)
+ google-apis-core (>= 0.9.0, < 2.a)
+ google-cloud-core (1.6.0)
+ google-cloud-env (~> 1.0)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.3.1)
+ google-cloud-storage (1.44.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.19.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.7.0)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ memoist (~> 0.16)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.5)
+ domain_name (~> 0.5)
+ httpclient (2.8.3)
+ jmespath (1.6.2)
+ json (2.6.3)
+ jwt (2.7.1)
+ memoist (0.16.2)
+ mini_magick (4.12.0)
+ mini_mime (1.1.2)
+ multi_json (1.15.0)
+ multipart-post (2.3.0)
+ nanaimo (0.3.0)
+ naturally (2.2.1)
+ optparse (0.1.1)
+ os (1.1.4)
+ plist (3.7.0)
+ public_suffix (5.0.3)
+ rake (13.0.6)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.2.6)
+ rouge (2.0.7)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.3.2)
+ security (0.1.3)
+ signet (0.17.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ terminal-notifier (2.0.0)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.1)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.8.2)
+ unicode-display_width (1.8.0)
+ webrick (1.8.1)
+ word_wrap (1.0.0)
+ xcodeproj (1.22.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.3.0)
+ rexml (~> 3.2.4)
+ xcpretty (0.3.0)
+ rouge (~> 2.0.7)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ x86_64-linux
+
+DEPENDENCIES
+ fastlane
+
+BUNDLED WITH
+ 2.3.5
diff --git a/app/src/main/res/raw/invalid_response.json b/app/src/androidTest/assets/invalid_api_key_response.json
similarity index 100%
rename from app/src/main/res/raw/invalid_response.json
rename to app/src/androidTest/assets/invalid_api_key_response.json
diff --git a/app/src/androidTest/assets/valid_response_imperial.json b/app/src/androidTest/assets/valid_response_imperial.json
new file mode 100644
index 0000000..96cd02d
--- /dev/null
+++ b/app/src/androidTest/assets/valid_response_imperial.json
@@ -0,0 +1,345 @@
+{
+ "lat": 51.51,
+ "lon": -0.13,
+ "timezone": "Europe/London",
+ "timezone_offset": 3600,
+ "current": {
+ "dt": 1691537401,
+ "sunrise": 1691555756,
+ "sunset": 1691609779,
+ "temp": 58.57,
+ "feels_like": 58.5,
+ "pressure": 1012,
+ "humidity": 93,
+ "dew_point": 56.55,
+ "uvi": 0,
+ "clouds": 100,
+ "visibility": 10000,
+ "wind_speed": 5.75,
+ "wind_deg": 280,
+ "weather": [
+ {
+ "id": 804,
+ "main": "Clouds",
+ "description": "overcast clouds",
+ "icon": "04n"
+ }
+ ]
+ },
+ "daily": [
+ {
+ "dt": 1691582400,
+ "sunrise": 1691555756,
+ "sunset": 1691609779,
+ "moonrise": 1691620920,
+ "moonset": 1691592600,
+ "moon_phase": 0.78,
+ "temp": {
+ "day": 71.08,
+ "min": 54.81,
+ "max": 75.24,
+ "night": 65.26,
+ "eve": 74.3,
+ "morn": 55.85
+ },
+ "feels_like": {
+ "day": 70.14,
+ "night": 65.3,
+ "eve": 73.92,
+ "morn": 55.04
+ },
+ "pressure": 1018,
+ "humidity": 48,
+ "dew_point": 50.27,
+ "wind_speed": 5.03,
+ "wind_deg": 204,
+ "wind_gust": 11.03,
+ "weather": [
+ {
+ "id": 802,
+ "main": "Clouds",
+ "description": "scattered clouds",
+ "icon": "03d"
+ }
+ ],
+ "clouds": 36,
+ "pop": 0,
+ "uvi": 6.76
+ },
+ {
+ "dt": 1691668800,
+ "sunrise": 1691642251,
+ "sunset": 1691696069,
+ "moonrise": 0,
+ "moonset": 1691683560,
+ "moon_phase": 0.82,
+ "temp": {
+ "day": 76.14,
+ "min": 61.27,
+ "max": 79.39,
+ "night": 68.88,
+ "eve": 76.66,
+ "morn": 62.64
+ },
+ "feels_like": {
+ "day": 75.94,
+ "night": 68.99,
+ "eve": 76.6,
+ "morn": 62.31
+ },
+ "pressure": 1019,
+ "humidity": 53,
+ "dew_point": 58.01,
+ "wind_speed": 8.57,
+ "wind_deg": 184,
+ "wind_gust": 15.05,
+ "weather": [
+ {
+ "id": 803,
+ "main": "Clouds",
+ "description": "broken clouds",
+ "icon": "04d"
+ }
+ ],
+ "clouds": 68,
+ "pop": 0,
+ "uvi": 6.43
+ },
+ {
+ "dt": 1691755200,
+ "sunrise": 1691728746,
+ "sunset": 1691782357,
+ "moonrise": 1691709180,
+ "moonset": 1691773920,
+ "moon_phase": 0.85,
+ "temp": {
+ "day": 75.67,
+ "min": 63.64,
+ "max": 80.06,
+ "night": 68.27,
+ "eve": 74.07,
+ "morn": 65.26
+ },
+ "feels_like": {
+ "day": 75.9,
+ "night": 68.27,
+ "eve": 74.05,
+ "morn": 65.21
+ },
+ "pressure": 1018,
+ "humidity": 63,
+ "dew_point": 62.37,
+ "wind_speed": 11.59,
+ "wind_deg": 224,
+ "wind_gust": 19.64,
+ "weather": [
+ {
+ "id": 500,
+ "main": "Rain",
+ "description": "light rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 100,
+ "pop": 0.32,
+ "rain": 0.12,
+ "uvi": 5.08
+ },
+ {
+ "dt": 1691841600,
+ "sunrise": 1691815241,
+ "sunset": 1691868645,
+ "moonrise": 1691798160,
+ "moonset": 1691863560,
+ "moon_phase": 0.88,
+ "temp": {
+ "day": 74.5,
+ "min": 63.07,
+ "max": 74.5,
+ "night": 63.86,
+ "eve": 70.03,
+ "morn": 63.07
+ },
+ "feels_like": {
+ "day": 73.53,
+ "night": 63.57,
+ "eve": 69.28,
+ "morn": 62.89
+ },
+ "pressure": 1014,
+ "humidity": 40,
+ "dew_point": 48.34,
+ "wind_speed": 14.63,
+ "wind_deg": 225,
+ "wind_gust": 22.73,
+ "weather": [
+ {
+ "id": 803,
+ "main": "Clouds",
+ "description": "broken clouds",
+ "icon": "04d"
+ }
+ ],
+ "clouds": 67,
+ "pop": 0.07,
+ "uvi": 5.66
+ },
+ {
+ "dt": 1691928000,
+ "sunrise": 1691901736,
+ "sunset": 1691954930,
+ "moonrise": 1691887860,
+ "moonset": 1691952420,
+ "moon_phase": 0.91,
+ "temp": {
+ "day": 71.26,
+ "min": 61.68,
+ "max": 71.26,
+ "night": 63.75,
+ "eve": 68.59,
+ "morn": 61.68
+ },
+ "feels_like": {
+ "day": 70.52,
+ "night": 63.39,
+ "eve": 67.93,
+ "morn": 61.21
+ },
+ "pressure": 1012,
+ "humidity": 52,
+ "dew_point": 52.92,
+ "wind_speed": 12.08,
+ "wind_deg": 226,
+ "wind_gust": 20.87,
+ "weather": [
+ {
+ "id": 500,
+ "main": "Rain",
+ "description": "light rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 99,
+ "pop": 0.22,
+ "rain": 0.16,
+ "uvi": 3.71
+ },
+ {
+ "dt": 1692014400,
+ "sunrise": 1691988232,
+ "sunset": 1692041215,
+ "moonrise": 1691978220,
+ "moonset": 1692040560,
+ "moon_phase": 0.94,
+ "temp": {
+ "day": 73.13,
+ "min": 60.35,
+ "max": 73.92,
+ "night": 63.91,
+ "eve": 69.6,
+ "morn": 60.82
+ },
+ "feels_like": {
+ "day": 72.12,
+ "night": 63.54,
+ "eve": 68.9,
+ "morn": 60.64
+ },
+ "pressure": 1013,
+ "humidity": 42,
+ "dew_point": 48.63,
+ "wind_speed": 12.35,
+ "wind_deg": 237,
+ "wind_gust": 16.15,
+ "weather": [
+ {
+ "id": 801,
+ "main": "Clouds",
+ "description": "few clouds",
+ "icon": "02d"
+ }
+ ],
+ "clouds": 16,
+ "pop": 0.1,
+ "uvi": 4
+ },
+ {
+ "dt": 1692100800,
+ "sunrise": 1692074727,
+ "sunset": 1692127498,
+ "moonrise": 1692068820,
+ "moonset": 1692128220,
+ "moon_phase": 0.97,
+ "temp": {
+ "day": 71.24,
+ "min": 58.37,
+ "max": 71.24,
+ "night": 66.47,
+ "eve": 69.06,
+ "morn": 58.37
+ },
+ "feels_like": {
+ "day": 70.23,
+ "night": 65.5,
+ "eve": 68.16,
+ "morn": 57.61
+ },
+ "pressure": 1018,
+ "humidity": 46,
+ "dew_point": 49.62,
+ "wind_speed": 7.38,
+ "wind_deg": 261,
+ "wind_gust": 13.49,
+ "weather": [
+ {
+ "id": 801,
+ "main": "Clouds",
+ "description": "few clouds",
+ "icon": "02d"
+ }
+ ],
+ "clouds": 23,
+ "pop": 0,
+ "uvi": 4
+ },
+ {
+ "dt": 1692187200,
+ "sunrise": 1692161223,
+ "sunset": 1692213780,
+ "moonrise": 1692159660,
+ "moonset": 1692215580,
+ "moon_phase": 0,
+ "temp": {
+ "day": 74.35,
+ "min": 60.22,
+ "max": 75.9,
+ "night": 66.33,
+ "eve": 71.44,
+ "morn": 60.69
+ },
+ "feels_like": {
+ "day": 73.42,
+ "night": 65.71,
+ "eve": 70.68,
+ "morn": 59.7
+ },
+ "pressure": 1018,
+ "humidity": 41,
+ "dew_point": 49.51,
+ "wind_speed": 7.92,
+ "wind_deg": 111,
+ "wind_gust": 14.92,
+ "weather": [
+ {
+ "id": 804,
+ "main": "Clouds",
+ "description": "overcast clouds",
+ "icon": "04d"
+ }
+ ],
+ "clouds": 91,
+ "pop": 0,
+ "uvi": 4
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/src/main/res/raw/valid_response.json b/app/src/androidTest/assets/valid_response_metric.json
similarity index 100%
rename from app/src/main/res/raw/valid_response.json
rename to app/src/androidTest/assets/valid_response_metric.json
diff --git a/app/src/androidTest/assets/valid_response_metric_sydney.json b/app/src/androidTest/assets/valid_response_metric_sydney.json
new file mode 100644
index 0000000..cd7dd3b
--- /dev/null
+++ b/app/src/androidTest/assets/valid_response_metric_sydney.json
@@ -0,0 +1,350 @@
+{
+ "lat": -33.89,
+ "lon": -151.12,
+ "timezone": "Etc/GMT+10",
+ "timezone_offset": -36000,
+ "current": {
+ "dt": 1691771435,
+ "sunrise": 1691772463,
+ "sunset": 1691811108,
+ "temp": 12.48,
+ "feels_like": 11.25,
+ "pressure": 1025,
+ "humidity": 56,
+ "dew_point": 3.95,
+ "uvi": 0,
+ "clouds": 100,
+ "visibility": 10000,
+ "wind_speed": 2.3,
+ "wind_deg": 66,
+ "wind_gust": 2.68,
+ "weather": [
+ {
+ "id": 804,
+ "main": "Clouds",
+ "description": "overcast clouds",
+ "icon": "04n"
+ }
+ ]
+ },
+ "daily": [
+ {
+ "dt": 1691791200,
+ "sunrise": 1691772463,
+ "sunset": 1691811108,
+ "moonrise": 1691761680,
+ "moonset": 1691796060,
+ "moon_phase": 0.86,
+ "temp": {
+ "day": 12.94,
+ "min": 12.26,
+ "max": 13.59,
+ "night": 13.59,
+ "eve": 12.46,
+ "morn": 12.46
+ },
+ "feels_like": {
+ "day": 11.91,
+ "night": 13.38,
+ "eve": 11.8,
+ "morn": 11.2
+ },
+ "pressure": 1022,
+ "humidity": 62,
+ "dew_point": 5.89,
+ "wind_speed": 17.33,
+ "wind_deg": 36,
+ "wind_gust": 21.77,
+ "weather": [
+ {
+ "id": 501,
+ "main": "Rain",
+ "description": "moderate rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 100,
+ "pop": 1,
+ "rain": 5.93,
+ "uvi": 2.13
+ },
+ {
+ "dt": 1691877600,
+ "sunrise": 1691858802,
+ "sunset": 1691897551,
+ "moonrise": 1691851260,
+ "moonset": 1691885640,
+ "moon_phase": 0.89,
+ "temp": {
+ "day": 15.41,
+ "min": 14.02,
+ "max": 16.03,
+ "night": 15.48,
+ "eve": 15.27,
+ "morn": 15.67
+ },
+ "feels_like": {
+ "day": 15.23,
+ "night": 15.25,
+ "eve": 14.92,
+ "morn": 15.77
+ },
+ "pressure": 1008,
+ "humidity": 85,
+ "dew_point": 12.76,
+ "wind_speed": 16.89,
+ "wind_deg": 26,
+ "wind_gust": 22.91,
+ "weather": [
+ {
+ "id": 501,
+ "main": "Rain",
+ "description": "moderate rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 7,
+ "pop": 1,
+ "rain": 11.8,
+ "uvi": 4.27
+ },
+ {
+ "dt": 1691964000,
+ "sunrise": 1691945139,
+ "sunset": 1691983994,
+ "moonrise": 1691940540,
+ "moonset": 1691975520,
+ "moon_phase": 0.92,
+ "temp": {
+ "day": 14.49,
+ "min": 13.32,
+ "max": 15.51,
+ "night": 13.32,
+ "eve": 13.92,
+ "morn": 15.02
+ },
+ "feels_like": {
+ "day": 14.11,
+ "night": 12.67,
+ "eve": 13.56,
+ "morn": 14.59
+ },
+ "pressure": 1005,
+ "humidity": 81,
+ "dew_point": 11.26,
+ "wind_speed": 16.29,
+ "wind_deg": 281,
+ "wind_gust": 21.74,
+ "weather": [
+ {
+ "id": 500,
+ "main": "Rain",
+ "description": "light rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 84,
+ "pop": 0.63,
+ "rain": 1.42,
+ "uvi": 3.5
+ },
+ {
+ "dt": 1692050400,
+ "sunrise": 1692031476,
+ "sunset": 1692070437,
+ "moonrise": 1692029400,
+ "moonset": 1692065400,
+ "moon_phase": 0.95,
+ "temp": {
+ "day": 12.81,
+ "min": 12.63,
+ "max": 13.09,
+ "night": 12.63,
+ "eve": 12.74,
+ "morn": 12.84
+ },
+ "feels_like": {
+ "day": 11.71,
+ "night": 11.36,
+ "eve": 11.51,
+ "morn": 11.9
+ },
+ "pressure": 1019,
+ "humidity": 60,
+ "dew_point": 5.36,
+ "wind_speed": 15.72,
+ "wind_deg": 218,
+ "wind_gust": 18.13,
+ "weather": [
+ {
+ "id": 500,
+ "main": "Rain",
+ "description": "light rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 74,
+ "pop": 0.47,
+ "rain": 0.46,
+ "uvi": 2.37
+ },
+ {
+ "dt": 1692136800,
+ "sunrise": 1692117812,
+ "sunset": 1692156879,
+ "moonrise": 1692117900,
+ "moonset": 1692155340,
+ "moon_phase": 0,
+ "temp": {
+ "day": 12.21,
+ "min": 12.21,
+ "max": 13.31,
+ "night": 13.3,
+ "eve": 13.14,
+ "morn": 12.29
+ },
+ "feels_like": {
+ "day": 11,
+ "night": 12.28,
+ "eve": 11.97,
+ "morn": 10.98
+ },
+ "pressure": 1023,
+ "humidity": 58,
+ "dew_point": 4.12,
+ "wind_speed": 6.03,
+ "wind_deg": 241,
+ "wind_gust": 7.73,
+ "weather": [
+ {
+ "id": 803,
+ "main": "Clouds",
+ "description": "broken clouds",
+ "icon": "04d"
+ }
+ ],
+ "clouds": 80,
+ "pop": 0,
+ "uvi": 3
+ },
+ {
+ "dt": 1692223200,
+ "sunrise": 1692204146,
+ "sunset": 1692243322,
+ "moonrise": 1692206100,
+ "moonset": 1692245160,
+ "moon_phase": 0.02,
+ "temp": {
+ "day": 12.69,
+ "min": 12.33,
+ "max": 13.65,
+ "night": 12.97,
+ "eve": 13.1,
+ "morn": 13.58
+ },
+ "feels_like": {
+ "day": 12.05,
+ "night": 11.84,
+ "eve": 12.14,
+ "morn": 12.93
+ },
+ "pressure": 1024,
+ "humidity": 78,
+ "dew_point": 8.88,
+ "wind_speed": 7.54,
+ "wind_deg": 130,
+ "wind_gust": 7.83,
+ "weather": [
+ {
+ "id": 501,
+ "main": "Rain",
+ "description": "moderate rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 86,
+ "pop": 0.8,
+ "rain": 10.37,
+ "uvi": 3
+ },
+ {
+ "dt": 1692309600,
+ "sunrise": 1692290480,
+ "sunset": 1692329764,
+ "moonrise": 1692294120,
+ "moonset": 1692334980,
+ "moon_phase": 0.05,
+ "temp": {
+ "day": 12.81,
+ "min": 12.44,
+ "max": 13.25,
+ "night": 13.25,
+ "eve": 13.05,
+ "morn": 12.79
+ },
+ "feels_like": {
+ "day": 11.58,
+ "night": 12.28,
+ "eve": 11.93,
+ "morn": 11.61
+ },
+ "pressure": 1030,
+ "humidity": 55,
+ "dew_point": 3.97,
+ "wind_speed": 7.57,
+ "wind_deg": 145,
+ "wind_gust": 7.26,
+ "weather": [
+ {
+ "id": 800,
+ "main": "Clear",
+ "description": "clear sky",
+ "icon": "01d"
+ }
+ ],
+ "clouds": 5,
+ "pop": 0,
+ "uvi": 3
+ },
+ {
+ "dt": 1692396000,
+ "sunrise": 1692376812,
+ "sunset": 1692416207,
+ "moonrise": 1692382020,
+ "moonset": 1692424680,
+ "moon_phase": 0.08,
+ "temp": {
+ "day": 13.99,
+ "min": 13.52,
+ "max": 14.8,
+ "night": 14.8,
+ "eve": 14.4,
+ "morn": 13.52
+ },
+ "feels_like": {
+ "day": 13.14,
+ "night": 14.29,
+ "eve": 13.62,
+ "morn": 12.73
+ },
+ "pressure": 1027,
+ "humidity": 65,
+ "dew_point": 7.62,
+ "wind_speed": 12.53,
+ "wind_deg": 47,
+ "wind_gust": 14.21,
+ "weather": [
+ {
+ "id": 500,
+ "main": "Rain",
+ "description": "light rain",
+ "icon": "10d"
+ }
+ ],
+ "clouds": 99,
+ "pop": 0.31,
+ "rain": 0.31,
+ "uvi": 3
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/src/androidTest/assets/wrong_location_response.json b/app/src/androidTest/assets/wrong_location_response.json
new file mode 100644
index 0000000..dcb8e52
--- /dev/null
+++ b/app/src/androidTest/assets/wrong_location_response.json
@@ -0,0 +1,4 @@
+{
+ "cod": "400",
+ "message": "wrong latitude"
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt
index b525a59..af29076 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt
@@ -5,20 +5,30 @@ package com.appttude.h_mal.atlas_weather
import android.Manifest
import android.app.Activity
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.view.View
+import android.view.WindowManager
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
+import androidx.test.espresso.Root
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.RootMatchers.withDecorView
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
+import com.appttude.h_mal.atlas_weather.helpers.BaseCustomMatcher
+import com.appttude.h_mal.atlas_weather.helpers.BaseViewAction
import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
import com.appttude.h_mal.atlas_weather.utils.Stubs
import kotlinx.coroutines.runBlocking
+import org.hamcrest.Description
import org.hamcrest.Matcher
+import org.hamcrest.Matchers
+import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -34,6 +44,7 @@ open class BaseTest(
lateinit var scenario: ActivityScenario
private lateinit var testApp: TestAppClass
private lateinit var testActivity: Activity
+ private lateinit var decorView: View
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@@ -58,19 +69,24 @@ open class BaseTest(
scenario = ActivityScenario.launch(startIntent)
scenario.onActivity {
+ decorView = it.window.decorView
testActivity = it
}
afterLaunch()
}
- fun stubEndpoint(url: String, stub: Stubs) {
- testApp.stubUrl(url, stub.id)
+ fun stubEndpoint(url: String, stub: Stubs, code: Int = 200) {
+ testApp.stubUrl(url, stub.id, code)
}
fun unstubEndpoint(url: String) {
testApp.removeUrlStub(url)
}
+ fun stubLocation(location: String, lat: Double = 0.00, long: Double = 0.00) {
+ testApp.stubLocation(location, lat, long)
+ }
+
fun getActivity() = testActivity
@After
@@ -91,4 +107,13 @@ open class BaseTest(
}
})
}
+
+ @Suppress("DEPRECATION")
+ fun checkToastMessage(message: String) {
+ Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView)))
+ .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ waitFor(3500)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt
index fc5f264..1118703 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt
@@ -3,6 +3,8 @@ package com.appttude.h_mal.atlas_weather.application
import androidx.room.Room
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
+import androidx.test.platform.app.InstrumentationRegistry
+import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
@@ -18,6 +20,9 @@ class TestAppClass : BaseAppClass() {
private val idlingResources = CountingIdlingResource("Data_loader")
private val mockingNetworkInterceptor = MockingNetworkInterceptor(idlingResources)
+ lateinit var database: AppDatabase
+ lateinit var locationProvider: MockLocationProvider
+
override fun onCreate() {
super.onCreate()
IdlingRegistry.getInstance().register(idlingResources)
@@ -32,23 +37,31 @@ class TestAppClass : BaseAppClass() {
) as WeatherApi
}
- override fun createLocationModule() = MockLocationProvider()
-
- override fun createRoomDatabase(): AppDatabase {
- return Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
- .addTypeConverter(Converter(this))
- .build()
+ override fun createLocationModule(): LocationProvider {
+ locationProvider = MockLocationProvider()
+ return locationProvider
}
- fun stubUrl(url: String, rawPath: String) {
- val id = resources.getIdentifier(rawPath, "raw", packageName)
- val iStream = resources.openRawResource(id)
+ override fun createRoomDatabase(): AppDatabase {
+ database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
+ .addTypeConverter(Converter(this))
+ .build()
+ return database
+ }
+
+ fun stubUrl(url: String, rawPath: String, code: Int = 200) {
+ val iStream =
+ InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath.json")
val data = iStream.bufferedReader().use(BufferedReader::readText)
- mockingNetworkInterceptor.addUrlStub(url = url, data = data)
+ mockingNetworkInterceptor.addUrlStub(url = url, data = data, code = code)
}
fun removeUrlStub(url: String) {
mockingNetworkInterceptor.removeUrlStub(url = url)
}
+ fun stubLocation(location: String, lat: Double = 0.00, long: Double = 0.00) {
+ locationProvider.addLocationToList(location, lat, long)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt
index 6115ef1..ea101a8 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt
@@ -3,17 +3,25 @@ package com.appttude.h_mal.atlas_weather.data.location
import com.appttude.h_mal.atlas_weather.model.types.LocationType
class MockLocationProvider : LocationProvider {
- private val latLong = Pair(0.00, 0.00)
+ private var feedMap: MutableMap> = mutableMapOf()
+
+ private var latLong = Pair(0.00, 0.00)
override suspend fun getCurrentLatLong() = latLong
- override fun getLatLongFromLocationName(location: String) = latLong
+ override fun getLatLongFromLocationName(location: String): Pair {
+ return feedMap[location] ?: Pair(0.00, 0.00)
+ }
override suspend fun getLocationNameFromLatLong(
lat: Double,
long: Double,
type: LocationType
): String {
- return "Mock Location"
+ return feedMap.filterValues { it.first == lat && it.second == long }.keys.firstOrNull() ?: "Mock Location"
}
+ fun addLocationToList(name: String, lat: Double, long: Double) {
+ val latLong = Pair(lat, long)
+ feedMap.put(name, latLong)
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt
index a1b8638..94397c4 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt
@@ -11,23 +11,21 @@ class MockingNetworkInterceptor(
private val idlingResource: CountingIdlingResource
) : Interceptor {
- private var feedMap: MutableMap = mutableMapOf()
- private var urlCallTracker: MutableMap = mutableMapOf()
+ private var feedMap: MutableMap> = mutableMapOf()
override fun intercept(chain: Interceptor.Chain): Response {
idlingResource.increment()
val original = chain.request()
val originalHttpUrl = original.url.toString().split("?")[0]
- urlCallTracker.computeIfPresent(originalHttpUrl) { _, j ->
- j + 1
- }
+ feedMap[originalHttpUrl]?.let { responsePair ->
+ val code = responsePair.second
+ val jsonBody = responsePair.first
- feedMap[originalHttpUrl]?.let { jsonPath ->
- val body = jsonPath.toResponseBody("application/json".toMediaType())
+ val body = jsonBody.toResponseBody("application/json".toMediaType())
val chainResponseBuilder = Response.Builder()
- .code(200)
+ .code(code)
.protocol(Protocol.HTTP_1_1)
.request(original)
.message("OK")
@@ -40,7 +38,7 @@ class MockingNetworkInterceptor(
return chain.proceed(original)
}
- fun addUrlStub(url: String, data: String) = feedMap.put(url, data)
+ fun addUrlStub(url: String, data: String, code: Int = 200) = feedMap.put(url, Pair(data, code))
fun removeUrlStub(url: String) = feedMap.remove(url)
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseCustomMatcher.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseCustomMatcher.kt
new file mode 100644
index 0000000..afe9dc4
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseCustomMatcher.kt
@@ -0,0 +1,12 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import org.hamcrest.Description
+import org.hamcrest.TypeSafeMatcher
+
+open class BaseCustomMatcher : TypeSafeMatcher() {
+ override fun describeTo(description: Description?) = describe(description)
+ override fun matchesSafely(item: T): Boolean = match(item)
+
+ open fun describe(description: Description?) { }
+ open fun match(actual: T): Boolean { return false }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt
new file mode 100644
index 0000000..5fe68d9
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt
@@ -0,0 +1,117 @@
+package com.appttude.h_mal.atlas_weather.testSuite
+
+import android.os.Build
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.util.UUIDUtil
+import androidx.test.platform.app.InstrumentationRegistry
+import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
+import com.appttude.h_mal.atlas_weather.data.room.Converter
+import com.appttude.h_mal.atlas_weather.data.room.WeatherDao
+import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
+import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
+import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
+import com.appttude.h_mal.atlas_weather.test.BuildConfig
+import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.Assert.*
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.mock
+import java.util.UUID
+
+
+class RoomDatabaseTests {
+ @get:Rule
+ @Suppress("unused")
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var db: AppDatabase
+ private lateinit var dao: WeatherDao
+
+ @Before
+ fun before() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
+ .allowMainThreadQueries()
+ .addTypeConverter(Converter(context))
+ .build()
+ dao = db.getWeatherDao()
+ }
+
+ @After
+ fun after() {
+ db.close()
+ }
+
+ @Test
+ fun whenInitializedThenListIsEmpty() {
+ assertTrue(dao.getAllFullWeatherWithoutCurrent().getOrAwaitValue().isEmpty())
+ }
+
+ @Test
+ fun whenElementAddedThenItemIsInDatabase() {
+ // Arrange
+ val item = createEntity()
+ // Act
+ dao.upsertFullWeather(item)
+ // Assert
+ assertEquals(item, dao.getCurrentFullWeather(item.id).getOrAwaitValue())
+ }
+
+ @Test
+ fun whenCurrentElementAddedThenLiveDataIsEmpty() {
+ // Arrange
+ val item = createEntity()
+ // Act
+ dao.upsertFullWeather(item)
+ // Assert
+ assertEquals(0, dao.getAllFullWeatherWithoutCurrent(CURRENT_LOCATION).getOrAwaitValue().size)
+ }
+
+ @Test
+ fun whenNewElementAddedThenLiveDataIsNot() {
+ // Arrange
+ val elements = createEntityList()
+ val id = elements.first().id
+ // Act
+ dao.upsertListOfFullWeather(elements)
+ // Assert
+ assertEquals(elements.size - 1, dao.getAllFullWeatherWithoutCurrent(id).getOrAwaitValue().size)
+ }
+
+ @Test
+ fun wheElementDeletedThenContainsIsFalse() {
+ // Arrange
+ val elements = createEntityList()
+ val id = elements.first().id
+ // Act
+ dao.upsertListOfFullWeather(elements)
+ dao.deleteEntry(id)
+ // Assert
+ assertEquals(elements.size - 1, dao.getAllFullWeatherWithoutCurrent(id).getOrAwaitValue().size)
+ assertNull(dao.getCurrentFullWeatherSingle(id))
+ }
+
+ private fun createEntity(id: String = CURRENT_LOCATION): EntityItem {
+ val weather = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ FullWeather()
+ } else {
+ mockk()
+ }
+ return EntityItem(id, weather)
+ }
+
+ private fun createEntityList(size: Int = 4): List {
+ return (0.. size).map {
+ val id = UUID.randomUUID().toString()
+ createEntity(id)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt
index 59c6885..e3cb20e 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt
@@ -3,6 +3,9 @@ package com.appttude.h_mal.atlas_weather.utils
enum class Stubs(
val id: String
) {
- Valid("valid_response"),
- Invalid("invalid_response")
+ Metric("valid_response_metric"),
+ Imperial("valid_response_imperial"),
+ WrongLocation("wrong_location_response"),
+ InvalidKey("invalid_api_key_response"),
+ Sydney("valid_response_metric_sydney")
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/TestUtils.kt
new file mode 100644
index 0000000..2305ea8
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/TestUtils.kt
@@ -0,0 +1,32 @@
+package com.appttude.h_mal.atlas_weather.utils
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+fun LiveData.getOrAwaitValue(
+ time: Long = 2,
+ timeUnit: TimeUnit = TimeUnit.SECONDS
+): T {
+ var data: T? = null
+ val latch = CountDownLatch(1)
+ val observer = object : Observer {
+ override fun onChanged(o: T?) {
+ data = o
+ latch.countDown()
+ this@getOrAwaitValue.removeObserver(this)
+ }
+ }
+
+ this.observeForever(observer)
+
+ // Don't wait indefinitely if the LiveData is not set.
+ if (!latch.await(time, timeUnit)) {
+ throw TimeoutException("LiveData value was never set.")
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return data as T
+}
\ No newline at end of file
diff --git a/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
index e4b19de..b863c40 100644
--- a/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
+++ b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
@@ -11,4 +11,8 @@ class HomeScreenRobot : BaseTestRobot() {
fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
fun refresh() = pullToRefresh(R.id.swipe_refresh)
fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4)
+ fun verifyUnableToRetrieve() {
+ matchText(R.id.header_text, R.string.retrieve_warning)
+ matchText(R.id.body_text, R.string.empty_retrieve_warning)
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageNoDataUITest.kt b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageNoDataUITest.kt
new file mode 100644
index 0000000..4ff1669
--- /dev/null
+++ b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageNoDataUITest.kt
@@ -0,0 +1,37 @@
+package com.appttude.h_mal.atlas_weather.tests
+
+import com.appttude.h_mal.atlas_weather.BaseTest
+import com.appttude.h_mal.atlas_weather.ui.MainActivity
+import com.appttude.h_mal.atlas_weather.utils.Stubs
+import com.appttude.h_mal.atlas_weather.robot.homeScreen
+import org.junit.Test
+
+class HomePageNoDataUITest : BaseTest(MainActivity::class.java) {
+
+ override fun beforeLaunch() {
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.InvalidKey, 400)
+ }
+
+ @Test
+ fun loadApp_invalidKeyWeatherResponse_returnsEmptyViewPage() {
+ homeScreen {
+ waitFor(2000)
+ // verify empty
+ verifyUnableToRetrieve()
+ }
+ }
+
+ @Test
+ fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() {
+ homeScreen {
+ waitFor(2000)
+ // verify empty
+ verifyUnableToRetrieve()
+
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
+ refresh()
+ verifyCurrentTemperature(2)
+ verifyCurrentLocation("Mock Location")
+ }
+ }
+}
diff --git a/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
index 789eeaf..5d23f0f 100644
--- a/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
+++ b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
@@ -10,7 +10,7 @@ import org.junit.Test
class HomePageUITest : BaseTest(MainActivity::class.java) {
override fun beforeLaunch() {
- stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
}
@Test
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/AddLocationScreenRobot.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/AddLocationScreenRobot.kt
new file mode 100644
index 0000000..87e79e2
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/AddLocationScreenRobot.kt
@@ -0,0 +1,12 @@
+package com.appttude.h_mal.monoWeather.robot
+
+import com.appttude.h_mal.atlas_weather.BaseTestRobot
+import com.appttude.h_mal.atlas_weather.R
+
+fun addLocation(func: AddLocationScreenRobot.() -> Unit) = AddLocationScreenRobot().apply { func() }
+class AddLocationScreenRobot : BaseTestRobot() {
+ fun setLocation(location: String) =
+ fillEditText(R.id.location_name_tv, location)
+
+ fun submit() = clickButton(R.id.submit)
+}
\ No newline at end of file
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/ContainerRobot.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/ContainerRobot.kt
new file mode 100644
index 0000000..5003a2f
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/ContainerRobot.kt
@@ -0,0 +1,23 @@
+package com.appttude.h_mal.monoWeather.robot
+
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.appttude.h_mal.atlas_weather.BaseTestRobot
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper
+
+fun container(func: ContainerRobot.() -> Unit) = ContainerRobot().apply { func() }
+class ContainerRobot : BaseTestRobot() {
+
+ fun tapTabInBottomBar(tab: Tab) {
+ when (tab) {
+ Tab.WORLD -> EspressoHelper.waitForView(withId(R.id.nav_world))
+ Tab.HOME -> EspressoHelper.waitForView(withId(R.id.nav_home))
+ }.perform(click())
+ }
+
+ enum class Tab {
+ HOME,
+ WORLD
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/HomeScreenRobot.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/WeatherScreen.kt
similarity index 60%
rename from app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/HomeScreenRobot.kt
rename to app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/WeatherScreen.kt
index 834397a..7f4034f 100644
--- a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/HomeScreenRobot.kt
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/WeatherScreen.kt
@@ -3,12 +3,17 @@ package com.appttude.h_mal.monoWeather.robot
import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
-fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
-class HomeScreenRobot : BaseTestRobot() {
+fun weatherScreen(func: WeatherScreen.() -> Unit) = WeatherScreen().apply { func() }
+class WeatherScreen : BaseTestRobot() {
fun verifyCurrentTemperature(temperature: Int) =
matchText(R.id.temp_main_4, temperature.toString())
fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
fun refresh() = pullToRefresh(R.id.swipe_refresh)
fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4)
+
+ fun verifyUnableToRetrieve() {
+ matchText(R.id.header_text, R.string.retrieve_warning)
+ matchText(R.id.body_text, R.string.empty_retrieve_warning)
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/WorldScreenRobot.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/WorldScreenRobot.kt
new file mode 100644
index 0000000..6df917e
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/WorldScreenRobot.kt
@@ -0,0 +1,24 @@
+package com.appttude.h_mal.monoWeather.robot
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.appttude.h_mal.atlas_weather.BaseTestRobot
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper
+
+fun world(func: WorldScreenRobot.() -> Unit) = WorldScreenRobot().apply { func() }
+class WorldScreenRobot : BaseTestRobot() {
+ fun clickFab() = clickButton(R.id.floatingActionButton)
+ fun clickItemInList(location: String) {
+ EspressoHelper.waitForView(withId(R.id.world_recycler))
+ clickViewInRecycler(R.id.world_recycler, location)
+ }
+
+ fun clickItemInListByPosition(position: Int) =
+ clickViewInRecycler(R.id.world_recycler, position)
+
+ fun emptyViewDisplayed() {
+ matchText(R.id.body_text, R.string.retrieve_warning)
+ matchText(R.id.header_text, R.string.empty_retrieve_warning)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageNoDataUITest.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageNoDataUITest.kt
new file mode 100644
index 0000000..58e319b
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageNoDataUITest.kt
@@ -0,0 +1,35 @@
+package com.appttude.h_mal.monoWeather.tests
+
+
+import com.appttude.h_mal.atlas_weather.utils.Stubs
+import com.appttude.h_mal.monoWeather.MonoBaseTest
+import com.appttude.h_mal.monoWeather.robot.weatherScreen
+import org.junit.Test
+
+class HomePageNoDataUITest : MonoBaseTest() {
+
+ override fun beforeLaunch() {
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.InvalidKey, 400)
+ }
+
+ @Test
+ fun loadApp_invalidKeyWeatherResponse_returnsEmptyViewPage() {
+ weatherScreen {
+ // verify empty
+ verifyUnableToRetrieve()
+ }
+ }
+
+ @Test
+ fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() {
+ weatherScreen {
+ // verify empty
+ verifyUnableToRetrieve()
+
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
+ refresh()
+ verifyCurrentTemperature(2)
+ verifyCurrentLocation("Mock Location")
+ }
+ }
+}
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt
index 6f27b73..271e978 100644
--- a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt
@@ -3,18 +3,18 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.monoWeather.MonoBaseTest
-import com.appttude.h_mal.monoWeather.robot.homeScreen
+import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Test
class HomePageUITest : MonoBaseTest() {
override fun beforeLaunch() {
- stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
}
@Test
fun loadApp_validWeatherResponse_returnsValidPage() {
- homeScreen {
+ weatherScreen {
isDisplayed()
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/WorldPageUITest.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/WorldPageUITest.kt
new file mode 100644
index 0000000..18bdcf7
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/WorldPageUITest.kt
@@ -0,0 +1,42 @@
+package com.appttude.h_mal.monoWeather.tests
+
+
+import com.appttude.h_mal.atlas_weather.utils.Stubs
+import com.appttude.h_mal.monoWeather.MonoBaseTest
+import com.appttude.h_mal.monoWeather.robot.ContainerRobot.Tab.WORLD
+import com.appttude.h_mal.monoWeather.robot.addLocation
+import com.appttude.h_mal.monoWeather.robot.container
+import com.appttude.h_mal.monoWeather.robot.weatherScreen
+import com.appttude.h_mal.monoWeather.robot.world
+import org.junit.Test
+
+class WorldPageUITest : MonoBaseTest() {
+
+ override fun beforeLaunch() {
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
+ }
+
+ @Test
+ fun loadApp_addNewLocation_returnsValidPage() {
+ container {
+ tapTabInBottomBar(WORLD)
+ }
+ world {
+ clickFab()
+ }
+ addLocation {
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Sydney)
+ stubLocation("Sydney", -33.89, -151.12)
+ setLocation("Sydney")
+ submit()
+ }
+ world {
+ clickItemInList("Sydney")
+ }
+ weatherScreen {
+ isDisplayed()
+ verifyCurrentTemperature(12)
+ verifyCurrentLocation("Sydney")
+ }
+ }
+}
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/HomeFragment.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/HomeFragment.kt
index d61fd7f..31240c4 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/HomeFragment.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/HomeFragment.kt
@@ -1,6 +1,5 @@
package com.appttude.h_mal.atlas_weather.ui.home
-import android.Manifest
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.os.Bundle
@@ -9,47 +8,32 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
import androidx.navigation.Navigation.findNavController
import androidx.navigation.ui.onNavDestinationSelected
import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
-import com.appttude.h_mal.atlas_weather.ui.dialog.PermissionsDeclarationDialog
+import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.navigateTo
-import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
-import com.appttude.h_mal.monoWeather.ui.BaseFragment
import kotlinx.android.synthetic.main.fragment_home.*
-import org.kodein.di.KodeinAware
-import org.kodein.di.android.x.kodein
-
-import org.kodein.di.generic.instance
/**
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
-class HomeFragment : BaseFragment(R.layout.fragment_home) {
- private val viewModel by getFragmentViewModel()
+class HomeFragment : BaseFragment(R.layout.fragment_home) {
+ private lateinit var recyclerAdapter: WeatherRecyclerAdapter
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
- val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
- navigateToFurtherDetails(it)
- })
-
- forecast_listview.apply {
- layoutManager = LinearLayoutManager(context)
- adapter = recyclerAdapter
- }
-
swipe_refresh.apply {
setOnRefreshListener {
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
@@ -59,22 +43,14 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
}
}
- viewModel.weatherLiveData.observe(viewLifecycleOwner) {
- recyclerAdapter.addCurrent(it)
- }
+ recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
+ navigateToFurtherDetails(it)
+ })
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
- viewModel.operationRefresh.observe(viewLifecycleOwner) { it ->
- it.getContentIfNotHandled()?.let {
- swipe_refresh.isRefreshing = false
- }
+ forecast_listview.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = recyclerAdapter
}
-
- viewModel.operationState.observe(viewLifecycleOwner) {
- swipe_refresh.isRefreshing = false
- }
-
}
@SuppressLint("MissingPermission")
@@ -86,6 +62,19 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
}
}
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ swipe_refresh.isRefreshing = false
+ if (data is WeatherDisplay) {
+ recyclerAdapter.addCurrent(data)
+ }
+ }
+
+ override fun onFailure(error: Any?) {
+ super.onFailure(error)
+ swipe_refresh.isRefreshing = false
+ }
+
@SuppressLint("MissingPermission")
override fun permissionsGranted() {
viewModel.fetchData()
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/EmptyViewHolder.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/EmptyViewHolder.kt
index 01f5388..cb44208 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/EmptyViewHolder.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/EmptyViewHolder.kt
@@ -1,6 +1,23 @@
package com.appttude.h_mal.atlas_weather.ui.home.adapter
import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.DrawableRes
import androidx.recyclerview.widget.RecyclerView
+import com.appttude.h_mal.atlas_weather.R
-class EmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
\ No newline at end of file
+class EmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val icon: ImageView = itemView.findViewById(R.id.icon)
+ var bodyTV: TextView = itemView.findViewById(R.id.body_text)
+ var headerTV: TextView = itemView.findViewById(R.id.header_text)
+
+ fun bindData(
+ @DrawableRes imageRes: Int? = R.drawable.ic_baseline_cloud_off_24,
+ header: String = itemView.resources.getString(R.string.retrieve_warning),
+ body: String = itemView.resources.getString(R.string.empty_retrieve_warning) ){
+ imageRes?.let { icon.setImageResource(it) }
+ headerTV.text = header
+ bodyTV.text = body
+ }
+}
\ No newline at end of file
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/WeatherRecyclerAdapter.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/WeatherRecyclerAdapter.kt
index 9d07194..0844835 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/WeatherRecyclerAdapter.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/WeatherRecyclerAdapter.kt
@@ -1,20 +1,19 @@
package com.appttude.h_mal.atlas_weather.ui.home.adapter
-import android.annotation.SuppressLint
-import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.utils.generateView
+import com.appttude.h_mal.atlas_weather.ui.home.adapter.forecast.ViewHolderForecast
+import com.appttude.h_mal.atlas_weather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
class WeatherRecyclerAdapter(
- val itemClick: (Forecast) -> Unit
+ private val itemClick: (Forecast) -> Unit
) : RecyclerView.Adapter() {
var weather: WeatherDisplay? = null
- @SuppressLint("NotifyDataSetChanged")
fun addCurrent(current: WeatherDisplay) {
weather = current
notifyDataSetChanged()
@@ -23,7 +22,7 @@ class WeatherRecyclerAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (getDataType(viewType)) {
is ViewType.Empty -> {
- val emptyViewHolder = View(parent.context)
+ val emptyViewHolder = parent.generateView(R.layout.empty_state_layout)
EmptyViewHolder(emptyViewHolder)
}
@@ -32,8 +31,8 @@ class WeatherRecyclerAdapter(
ViewHolderCurrent(viewCurrent)
}
- is ViewType.Forecast -> {
- val viewForecast = parent.generateView(R.layout.list_item_forecast)
+ is ViewType.ForecastHourly -> {
+ val viewForecast = parent.generateView(R.layout.hourly_item_forecast)
ViewHolderForecast(viewForecast)
}
@@ -41,13 +40,19 @@ class WeatherRecyclerAdapter(
val viewFurther = parent.generateView(R.layout.list_item_further)
ViewHolderFurtherDetails(viewFurther)
}
+
+ is ViewType.ForecastDaily -> {
+ val viewForecast = parent.generateView(R.layout.list_item_forecast)
+ ViewHolderForecastDaily(viewForecast)
+ }
}
}
sealed class ViewType {
object Empty : ViewType()
object Current : ViewType()
- object Forecast : ViewType()
+ object ForecastHourly : ViewType()
+ object ForecastDaily : ViewType()
object Further : ViewType()
}
@@ -55,19 +60,20 @@ class WeatherRecyclerAdapter(
return when (type) {
0 -> ViewType.Empty
1 -> ViewType.Current
- 2 -> ViewType.Forecast
+ 2 -> ViewType.ForecastHourly
3 -> ViewType.Further
+ 4 -> ViewType.ForecastDaily
else -> ViewType.Empty
}
}
override fun getItemViewType(position: Int): Int {
if (weather == null) return 0
-
return when (position) {
0 -> 1
- in 1 until itemCount - 2 -> 2
- itemCount - 1 -> 3
+ 1 -> 3
+ 2 -> 2
+ in 3 until (itemCount) -> 4
else -> 0
}
}
@@ -75,7 +81,8 @@ class WeatherRecyclerAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getDataType(getItemViewType(position))) {
is ViewType.Empty -> {
- holder as EmptyViewHolder
+ val emptyViewHolder = holder as EmptyViewHolder
+ emptyViewHolder.bindData()
}
is ViewType.Current -> {
@@ -83,28 +90,31 @@ class WeatherRecyclerAdapter(
viewHolderCurrent.bindData(weather)
}
- is ViewType.Forecast -> {
- val viewHolderForecast = holder as ViewHolderForecast
-
- weather?.forecast?.get(position - 1)?.let { i ->
- viewHolderForecast.bindView(i)
- viewHolderForecast.itemView.setOnClickListener {
- itemClick(i)
- }
- }
- }
-
is ViewType.Further -> {
val viewHolderCurrent = holder as ViewHolderFurtherDetails
viewHolderCurrent.bindData(weather)
}
+
+ is ViewType.ForecastHourly -> {
+ val viewHolderForecast = holder as ViewHolderForecast
+ viewHolderForecast.bindView(weather?.hourly)
+ }
+
+ is ViewType.ForecastDaily -> {
+ val viewHolderForecast = holder as ViewHolderForecastDaily
+ weather?.forecast?.getOrNull(position - 3)?.let { f ->
+ viewHolderForecast.bindView(f)
+ viewHolderForecast.itemView.setOnClickListener {
+ itemClick.invoke(f)
+ }
+ }
+ }
}
}
override fun getItemCount(): Int {
- if (weather == null) return 0
- return 2 + (weather?.forecast?.size ?: 0)
+ return if (weather == null) 1 else 3 + (weather?.forecast?.size ?: 0)
}
}
\ No newline at end of file
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/GridCellHolder.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/GridCellHolder.kt
new file mode 100644
index 0000000..f1ce9c5
--- /dev/null
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/GridCellHolder.kt
@@ -0,0 +1,23 @@
+package com.appttude.h_mal.monoWeather.ui.home.adapter.forecast
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.weather.Hour
+import com.appttude.h_mal.atlas_weather.utils.loadImage
+import com.appttude.h_mal.atlas_weather.utils.toTime
+
+class GridCellHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+ var dayTV: TextView = itemView.findViewById(R.id.widget_item_day)
+ var weatherIV: ImageView = itemView.findViewById(R.id.widget_item_image)
+ var mainTempTV: TextView = itemView.findViewById(R.id.widget_item_temp_high)
+
+ fun bindView(hour: Hour?) {
+ dayTV.text = hour?.dt?.toTime()
+ weatherIV.loadImage(hour?.icon)
+ mainTempTV.text = hour?.temp?.toInt()?.toString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/GridForecastAdapter.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/GridForecastAdapter.kt
new file mode 100644
index 0000000..e6197db
--- /dev/null
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/GridForecastAdapter.kt
@@ -0,0 +1,32 @@
+package com.appttude.h_mal.atlas_weather.ui.home.adapter.forecast
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.weather.Hour
+import com.appttude.h_mal.atlas_weather.utils.generateView
+import com.appttude.h_mal.monoWeather.ui.home.adapter.forecast.GridCellHolder
+
+class GridForecastAdapter : RecyclerView.Adapter() {
+ var weather: MutableList = mutableListOf()
+
+ fun addCurrent(current: List?) {
+ weather.clear()
+ current?.let { weather.addAll(it) }
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val viewCurrent = parent.generateView(R.layout.hourly_forecast_grid_item)
+ return GridCellHolder(viewCurrent)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val view = holder as GridCellHolder
+ val forecast = weather[position]
+ view.bindView(forecast)
+ }
+
+ override fun getItemCount(): Int = weather.size
+
+}
\ No newline at end of file
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/ViewHolderForecast.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/ViewHolderForecast.kt
new file mode 100644
index 0000000..d1770e8
--- /dev/null
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecast/ViewHolderForecast.kt
@@ -0,0 +1,20 @@
+package com.appttude.h_mal.atlas_weather.ui.home.adapter.forecast
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.weather.Hour
+import com.appttude.h_mal.atlas_weather.ui.home.adapter.forecast.GridForecastAdapter
+
+class ViewHolderForecast(
+ itemView: View
+) : RecyclerView.ViewHolder(itemView) {
+
+ var recyclerView: RecyclerView = itemView.findViewById(R.id.forecast_recyclerview)
+
+ fun bindView(forecasts: List?) {
+ val adapter = GridForecastAdapter()
+ adapter.addCurrent(forecasts)
+ recyclerView.adapter = adapter
+ }
+}
\ No newline at end of file
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/ViewHolderForecast.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecastDaily/ViewHolderForecastDaily.kt
similarity index 66%
rename from app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/ViewHolderForecast.kt
rename to app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecastDaily/ViewHolderForecastDaily.kt
index a95efd7..d97732f 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/ViewHolderForecast.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/home/adapter/forecastDaily/ViewHolderForecastDaily.kt
@@ -1,4 +1,4 @@
-package com.appttude.h_mal.atlas_weather.ui.home.adapter
+package com.appttude.h_mal.atlas_weather.ui.home.adapter.forecastDaily
import android.view.View
import android.widget.ImageView
@@ -8,21 +8,21 @@ import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import com.appttude.h_mal.atlas_weather.utils.loadImage
-class ViewHolderForecast(itemView: View) : RecyclerView.ViewHolder(itemView) {
+class ViewHolderForecastDaily(itemView: View) : RecyclerView.ViewHolder(itemView) {
var dateTV: TextView = itemView.findViewById(R.id.list_date)
var dayTV: TextView = itemView.findViewById(R.id.list_day)
- var conditionTV: TextView = itemView.findViewById(R.id.list_condition)
var weatherIV: ImageView = itemView.findViewById(R.id.list_icon)
- var mainTempTV: TextView = itemView.findViewById(R.id.list_main_temp)
- var minorTempTV: TextView = itemView.findViewById(R.id.list_minor_temp)
+ var maxTempTV: TextView = itemView.findViewById(R.id.list_main_temp)
+ var minTempTV: TextView = itemView.findViewById(R.id.list_minor_temp)
+ var conditionTV: TextView = itemView.findViewById(R.id.list_condition)
fun bindView(forecast: Forecast?) {
dateTV.text = forecast?.date
dayTV.text = forecast?.day
- conditionTV.text = forecast?.condition
weatherIV.loadImage(forecast?.weatherIcon)
- mainTempTV.text = forecast?.mainTemp
- minorTempTV.text = forecast?.minorTemp
+ maxTempTV.text = forecast?.mainTemp
+ minTempTV.text = forecast?.minorTemp
+ conditionTV.text = forecast?.condition
}
}
\ No newline at end of file
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/AddLocationFragment.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/AddLocationFragment.kt
index e8441d0..2c79c5d 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/AddLocationFragment.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/AddLocationFragment.kt
@@ -1,26 +1,17 @@
package com.appttude.h_mal.atlas_weather.ui.world
import android.os.Bundle
-import android.view.LayoutInflater
import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.observe
import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.goBack
-import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
-import com.appttude.h_mal.monoWeather.ui.BaseFragment
-import kotlinx.android.synthetic.main.activity_add_forecast.*
-import org.kodein.di.KodeinAware
-import org.kodein.di.android.x.kodein
-import org.kodein.di.generic.instance
+import kotlinx.android.synthetic.main.activity_add_forecast.location_name_tv
+import kotlinx.android.synthetic.main.activity_add_forecast.submit
-class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
-
- private val viewModel by getFragmentViewModel()
+class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -31,16 +22,13 @@ class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
submit.error = "Location cannot be blank"
return@setOnClickListener
}
- viewModel.fetchDataForSingleLocation(locationName)
+ viewModel.fetchDataForSingleLocationSearch(locationName)
}
+ }
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
-
- viewModel.operationComplete.observe(viewLifecycleOwner) {
- it?.getContentIfNotHandled()?.let { message ->
- displayToast(message)
- }
+ override fun onSuccess(data: Any?) {
+ if (data is String) {
+ displayToast(data)
goBack()
}
}
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/WorldFragment.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/WorldFragment.kt
index 8b0e787..fd9743d 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/WorldFragment.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/ui/world/WorldFragment.kt
@@ -3,14 +3,13 @@ package com.appttude.h_mal.atlas_weather.ui.world
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
-import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
-import com.appttude.h_mal.monoWeather.ui.BaseFragment
import kotlinx.android.synthetic.atlasWeather.fragment_add_location.floatingActionButton
-import kotlinx.android.synthetic.atlasWeather.fragment_add_location.progressBar2
import kotlinx.android.synthetic.atlasWeather.fragment_add_location.world_recycler
@@ -18,13 +17,13 @@ import kotlinx.android.synthetic.atlasWeather.fragment_add_location.world_recycl
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
-class WorldFragment : BaseFragment(R.layout.fragment_add_location) {
- val viewModel by getFragmentViewModel()
+class WorldFragment : BaseFragment(R.layout.fragment_add_location) {
+ private lateinit var recyclerAdapter: WorldRecyclerAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val recyclerAdapter = WorldRecyclerAdapter {
+ recyclerAdapter = WorldRecyclerAdapter {
val direction =
WorldFragmentDirections.actionWorldFragmentToWorldItemFragment(it)
navigateTo(direction)
@@ -35,22 +34,18 @@ class WorldFragment : BaseFragment(R.layout.fragment_add_location) {
adapter = recyclerAdapter
}
- viewModel.weatherLiveData.observe(viewLifecycleOwner) {
- recyclerAdapter.addCurrent(it)
- }
-
floatingActionButton.setOnClickListener {
navigateTo(R.id.action_worldFragment_to_addLocationFragment)
}
+ }
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar2))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
-
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ if (data is List<*>) recyclerAdapter.addCurrent(data as List)
}
override fun onResume() {
super.onResume()
-
viewModel.fetchAllLocations()
}
diff --git a/app/src/atlasWeather/res/layout/activity_add_forecast.xml b/app/src/atlasWeather/res/layout/activity_add_forecast.xml
index 8152f4b..7d8c3f3 100644
--- a/app/src/atlasWeather/res/layout/activity_add_forecast.xml
+++ b/app/src/atlasWeather/res/layout/activity_add_forecast.xml
@@ -36,20 +36,4 @@
android:textStyle="bold" />
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/atlasWeather/res/layout/activity_main.xml b/app/src/atlasWeather/res/layout/activity_main.xml
index 19566a4..25a8d00 100644
--- a/app/src/atlasWeather/res/layout/activity_main.xml
+++ b/app/src/atlasWeather/res/layout/activity_main.xml
@@ -43,17 +43,5 @@
app:layout_constraintTop_toBottomOf="@id/toolbar"
tools:layout="@layout/fragment_home" />
-
-
diff --git a/app/src/atlasWeather/res/layout/fragment_add_location.xml b/app/src/atlasWeather/res/layout/fragment_add_location.xml
index 7c57ce1..dcc23d7 100644
--- a/app/src/atlasWeather/res/layout/fragment_add_location.xml
+++ b/app/src/atlasWeather/res/layout/fragment_add_location.xml
@@ -31,15 +31,4 @@
app:layout_constraintRight_toRightOf="parent"
app:srcCompat="@android:drawable/ic_input_add" />
-
\ No newline at end of file
diff --git a/app/src/atlasWeather/res/layout/hourly_forecast_grid_item.xml b/app/src/atlasWeather/res/layout/hourly_forecast_grid_item.xml
new file mode 100644
index 0000000..874aba3
--- /dev/null
+++ b/app/src/atlasWeather/res/layout/hourly_forecast_grid_item.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/atlasWeather/res/layout/hourly_item_forecast.xml b/app/src/atlasWeather/res/layout/hourly_item_forecast.xml
new file mode 100644
index 0000000..32e5151
--- /dev/null
+++ b/app/src/atlasWeather/res/layout/hourly_item_forecast.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/BaseActivity.kt
new file mode 100644
index 0000000..b463b90
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/BaseActivity.kt
@@ -0,0 +1,91 @@
+package com.appttude.h_mal.atlas_weather.base
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup.LayoutParams
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.inflate
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.ViewModelLazy
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
+import com.appttude.h_mal.atlas_weather.model.ViewState
+import com.appttude.h_mal.atlas_weather.utils.displayToast
+import com.appttude.h_mal.atlas_weather.utils.hide
+import com.appttude.h_mal.atlas_weather.utils.show
+import com.appttude.h_mal.atlas_weather.utils.triggerAnimation
+import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
+import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.BaseViewModel
+import org.kodein.di.KodeinAware
+import org.kodein.di.android.kodein
+import org.kodein.di.generic.instance
+
+abstract class BaseActivity : AppCompatActivity(), KodeinAware {
+
+ private lateinit var loadingView: View
+
+ override val kodein by kodein()
+
+
+ /**
+ * Creates a loading view which to be shown during async operations
+ *
+ * #setOnClickListener(null) is an ugly work around to prevent under being clicked during
+ * loading
+ */
+ private fun instantiateLoadingView() {
+ loadingView = inflate(this, R.layout.progress_layout, null)
+ loadingView.setOnClickListener(null)
+ addContentView(loadingView, LayoutParams(MATCH_PARENT, MATCH_PARENT))
+ loadingView.hide()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ instantiateLoadingView()
+ }
+
+ fun startActivity(activity: Class) {
+ val intent = Intent(this, activity)
+ startActivity(intent)
+ }
+
+ /**
+ * Called in case of success or some data emitted from the liveData in viewModel
+ */
+ open fun onStarted() {
+ loadingView.fadeIn()
+ }
+
+ /**
+ * Called in case of success or some data emitted from the liveData in viewModel
+ */
+ open fun onSuccess(data: Any?) {
+ loadingView.fadeOut()
+ }
+
+ /**
+ * Called in case of failure or some error emitted from the liveData in viewModel
+ */
+ open fun onFailure(error: Any?) {
+ if (error is String) displayToast(error)
+ loadingView.fadeOut()
+ }
+
+ private fun View.fadeIn() = apply {
+ show()
+ triggerAnimation(R.anim.nav_default_enter_anim) {}
+ }
+
+ private fun View.fadeOut() = apply {
+ hide()
+ triggerAnimation(R.anim.nav_default_exit_anim) {}
+ }
+
+
+ override fun onBackPressed() {
+ loadingView.hide()
+ super.onBackPressed()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt
index 71715b6..ebfa238 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt
@@ -24,33 +24,32 @@ class RepositoryImpl(
}
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {
- db.getSimpleDao().upsertFullWeather(entityItem)
+ db.getWeatherDao().upsertFullWeather(entityItem)
}
override suspend fun saveWeatherListToRoom(
list: List
) {
- db.getSimpleDao().upsertListOfFullWeather(list)
+ db.getWeatherDao().upsertListOfFullWeather(list)
}
- override fun loadRoomWeatherLiveData() = db.getSimpleDao().getAllFullWeatherWithoutCurrent()
+ override fun loadRoomWeatherLiveData() = db.getWeatherDao().getAllFullWeatherWithoutCurrent()
override suspend fun loadWeatherList(): List {
- return db.getSimpleDao()
+ return db.getWeatherDao()
.getWeatherListWithoutCurrent()
.map { it.id }
}
override fun loadCurrentWeatherFromRoom(id: String) =
- db.getSimpleDao().getCurrentFullWeather(id)
+ db.getWeatherDao().getCurrentFullWeather(id)
override suspend fun loadSingleCurrentWeatherFromRoom(id: String) =
- db.getSimpleDao().getCurrentFullWeatherSingle(id)
+ db.getWeatherDao().getCurrentFullWeatherSingle(id)
override fun isSearchValid(locationName: String): Boolean {
val lastSaved = prefs
.getLastSavedAt("$LOCATION_CONST$locationName")
- ?: return true
val difference = System.currentTimeMillis() - lastSaved
return difference > FALLBACK_TIME
@@ -62,7 +61,7 @@ class RepositoryImpl(
override suspend fun deleteSavedWeatherEntry(locationName: String): Boolean {
prefs.deleteLocation(locationName)
- return db.getSimpleDao().deleteEntry(locationName) > 0
+ return db.getWeatherDao().deleteEntry(locationName) > 0
}
override fun getSavedLocations(): List {
@@ -70,7 +69,7 @@ class RepositoryImpl(
}
override suspend fun getSingleWeather(locationName: String): EntityItem {
- return db.getSimpleDao().getCurrentFullWeatherSingle(locationName)
+ return db.getWeatherDao().getCurrentFullWeatherSingle(locationName)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/AppDatabase.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/AppDatabase.kt
index 0a3ea62..78aae7f 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/AppDatabase.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/AppDatabase.kt
@@ -15,7 +15,7 @@ import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
@TypeConverters(Converter::class)
abstract class AppDatabase : RoomDatabase() {
- abstract fun getSimpleDao(): WeatherDao
+ abstract fun getWeatherDao(): WeatherDao
companion object {
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt
index 23840f6..ff91eb5 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt
@@ -11,6 +11,13 @@ object GenericsHelper {
?.kotlin
?: throw IllegalStateException("Can not find class from generic argument")
+// @Suppress("UNCHECKED_CAST")
+// fun Any.getGenericClassInMethod(position: Int): KClass =
+// ((javaClass.methods as? ParameterizedType)
+// ?.actualTypeArguments?.getOrNull(position) as? Class)
+// ?.kotlin
+// ?: throw IllegalStateException("Can not find class from generic argument")
+
// /**
// * Create a view binding out of the the generic [VB]
// *
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/model/ViewState.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/ViewState.kt
new file mode 100644
index 0000000..e258785
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/ViewState.kt
@@ -0,0 +1,7 @@
+package com.appttude.h_mal.atlas_weather.model
+
+sealed class ViewState {
+ object HasStarted : ViewState()
+ class HasData(val data: T) : ViewState()
+ class HasError(val error: T) : ViewState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/BaseFragment.kt
index 25d0c3a..f0cd6c1 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/BaseFragment.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/BaseFragment.kt
@@ -1,7 +1,5 @@
-package com.appttude.h_mal.monoWeather.ui
+package com.appttude.h_mal.atlas_weather.ui
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
@@ -9,16 +7,17 @@ import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
+import androidx.fragment.app.createViewModelLazy
import androidx.lifecycle.Observer
-import androidx.lifecycle.ViewModel
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
+import com.appttude.h_mal.atlas_weather.base.BaseActivity
+import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
+import com.appttude.h_mal.atlas_weather.model.ViewState
import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.utils.displayToast
-import com.appttude.h_mal.atlas_weather.utils.hide
-import com.appttude.h_mal.atlas_weather.utils.show
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
+import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.BaseViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -28,13 +27,18 @@ import org.kodein.di.generic.instance
import kotlin.properties.Delegates
@Suppress("EmptyMethod", "EmptyMethod")
-abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId),
+abstract class BaseFragment(@LayoutRes contentLayoutId: Int) :
+ Fragment(contentLayoutId),
KodeinAware {
override val kodein by kodein()
val factory by instance()
- inline fun getFragmentViewModel(): Lazy = viewModels { factory }
+ val viewModel: V by getFragmentViewModel()
+
+ var mActivity: BaseActivity? = null
+ private fun getFragmentViewModel(): Lazy =
+ createViewModelLazy(getGenericClassAt(0), { viewModelStore }, factoryProducer = { factory })
private var shortAnimationDuration by Delegates.notNull()
@@ -43,25 +47,42 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
}
- // toggle visibility of progress spinner while async operations are taking place
- fun progressBarStateObserver(progressBar: View) = Observer> {
- it.getContentIfNotHandled()?.let { i ->
- if (i)
- progressBar.fadeIn()
- else
- progressBar.fadeOut()
+ @Suppress("UNCHECKED_CAST")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ mActivity = activity as BaseActivity
+ configureObserver()
+ }
+
+ private fun configureObserver() {
+ viewModel.uiState.observe(viewLifecycleOwner) {
+ when (it) {
+ is ViewState.HasStarted -> onStarted()
+ is ViewState.HasData<*> -> onSuccess(it.data)
+ is ViewState.HasError<*> -> onFailure(it.error)
+ }
}
}
- // display a toast when operation fails
- fun errorObserver() = Observer> {
- it.getContentIfNotHandled()?.let { message ->
- displayToast(message)
- }
+ /**
+ * Called in case of starting operation liveData in viewModel
+ */
+ open fun onStarted() {
+ mActivity?.onStarted()
}
- fun refreshObserver(refresher: SwipeRefreshLayout) = Observer> {
- refresher.isRefreshing = false
+ /**
+ * Called in case of success or some data emitted from the liveData in viewModel
+ */
+ open fun onSuccess(data: Any?) {
+ mActivity?.onSuccess(data)
+ }
+
+ /**
+ * Called in case of failure or some error emitted from the liveData in viewModel
+ */
+ open fun onFailure(error: Any?) {
+ mActivity?.onFailure(error)
}
/**
@@ -90,46 +111,6 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
}
}
- private fun View.fadeIn() {
- apply {
- // Set the content view to 0% opacity but visible, so that it is visible
- // (but fully transparent) during the animation.
- alpha = 0f
- hide()
-
- // Animate the content view to 100% opacity, and clear any animation
- // listener set on the view.
- animate()
- .alpha(1f)
- .setDuration(shortAnimationDuration.toLong())
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- show()
- }
- })
- }
- }
-
- private fun View.fadeOut() {
- apply {
- // Set the content view to 0% opacity but visible, so that it is visible
- // (but fully transparent) during the animation.
- alpha = 1f
- show()
-
- // Animate the content view to 100% opacity, and clear any animation
- // listener set on the view.
- animate()
- .alpha(0f)
- .setDuration(shortAnimationDuration.toLong())
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- hide()
- }
- })
- }
- }
-
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(
requestCode: Int,
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt
index c992220..748233d 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt
@@ -9,10 +9,12 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.base.BaseActivity
+import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
-class MainActivity : AppCompatActivity() {
+class MainActivity : BaseActivity() {
lateinit var navHost: NavHostFragment
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/ViewUtils.kt
index b5e2c2a..344aff5 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/ViewUtils.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/ViewUtils.kt
@@ -5,9 +5,12 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.Toast
+import androidx.annotation.AnimRes
import androidx.fragment.app.Fragment
import com.appttude.h_mal.atlas_weather.R
import com.squareup.picasso.Picasso
@@ -42,4 +45,14 @@ fun ImageView.loadImage(url: String?) {
fun Fragment.hideKeyboard() {
val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
+}
+
+fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
+ val animation = AnimationUtils.loadAnimation(context, id)
+ animation.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation)
+ override fun onAnimationStart(a: Animation?) {}
+ override fun onAnimationRepeat(a: Animation?) {}
+ })
+ startAnimation(animation)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt
index b8874db..99cb2cd 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt
@@ -2,13 +2,11 @@ package com.appttude.h_mal.atlas_weather.viewmodel
import android.Manifest
import androidx.annotation.RequiresPermission
-import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
-import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -19,48 +17,39 @@ class MainViewModel(
private val repository: Repository
) : WeatherViewModel() {
- val weatherLiveData = MutableLiveData()
-
- val operationState = MutableLiveData>()
- val operationError = MutableLiveData>()
- val operationRefresh = MutableLiveData>()
-
init {
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
it?.let {
val weather = WeatherDisplay(it)
- weatherLiveData.postValue(weather)
+ onSuccess(weather)
}
}
}
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
fun fetchData() {
- if (!repository.isSearchValid(CURRENT_LOCATION)) {
- operationRefresh.postValue(Event(false))
- return
- }
-
+ onStart()
CoroutineScope(Dispatchers.IO).launch {
- operationState.postValue(Event(true))
try {
- // Get location
- val latLong = locationProvider.getCurrentLatLong()
- // Get weather from api
- val weather = repository
- .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
- val currentLocation =
- locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
- val fullWeather = createFullWeather(weather, currentLocation)
- val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
+ // Has the search been conducted in the last 5 minutes
+ val entityItem = if (repository.isSearchValid(CURRENT_LOCATION)) {
+ // Get location
+ val latLong = locationProvider.getCurrentLatLong()
+ // Get weather from api
+ val weather = repository
+ .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
+ val currentLocation =
+ locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
+ val fullWeather = createFullWeather(weather, currentLocation)
+ EntityItem(CURRENT_LOCATION, fullWeather)
+ } else {
+ repository.getSingleWeather(CURRENT_LOCATION)
+ }
// Save data if not null
repository.saveLastSavedAt(CURRENT_LOCATION)
repository.saveCurrentWeatherToRoom(entityItem)
} catch (e: Exception) {
- operationError.postValue(Event(e.message!!))
- } finally {
- operationState.postValue(Event(false))
- operationRefresh.postValue(Event(false))
+ onError(e.message!!)
}
}
}
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt
index 7fde5d0..30fd381 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt
@@ -1,13 +1,11 @@
package com.appttude.h_mal.atlas_weather.viewmodel
-import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.model.types.LocationType
-import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -20,15 +18,7 @@ class WorldViewModel(
private val locationProvider: LocationProvider,
private val repository: Repository
) : WeatherViewModel() {
-
- val weatherLiveData = MutableLiveData>()
- val singleWeatherLiveData = MutableLiveData()
-
- val operationState = MutableLiveData>()
- val operationError = MutableLiveData>()
- val operationRefresh = MutableLiveData>()
-
- val operationComplete = MutableLiveData>()
+ private var currentLocation: String? = null
private val weatherListLiveData = repository.loadRoomWeatherLiveData()
@@ -37,44 +27,46 @@ class WorldViewModel(
val list = it.map { data ->
WeatherDisplay(data)
}
- weatherLiveData.postValue(list)
+ onSuccess(list)
+ currentLocation?.let { i -> list.first { j -> j.location == i } }
+ ?.let { k -> onSuccess(k) }
}
}
+ fun setLocation(location: String) = run { currentLocation = location }
+
fun getSingleLocation(locationName: String) {
CoroutineScope(Dispatchers.IO).launch {
val entity = repository.getSingleWeather(locationName)
val item = WeatherDisplay(entity)
- singleWeatherLiveData.postValue(item)
+ onSuccess(item)
}
}
fun fetchDataForSingleLocation(locationName: String) {
- if (!repository.isSearchValid(locationName)) {
- operationRefresh.postValue(Event(false))
- return
- }
+ onStart()
CoroutineScope(Dispatchers.IO).launch {
- operationState.postValue(Event(true))
try {
- val weatherEntity = createWeatherEntity(locationName)
+ val weatherEntity = if (repository.isSearchValid(locationName)) {
+ createWeatherEntity(locationName)
+ } else {
+ repository.getSingleWeather(locationName)
+ }
+ onSuccess(Unit)
repository.saveCurrentWeatherToRoom(weatherEntity)
- repository.saveLastSavedAt(locationName)
+ repository.saveLastSavedAt(weatherEntity.id)
} catch (e: IOException) {
- operationError.postValue(Event(e.message!!))
- } finally {
- operationState.postValue(Event(false))
- operationRefresh.postValue(Event(false))
+ onError(e.message!!)
}
}
}
fun fetchDataForSingleLocationSearch(locationName: String) {
CoroutineScope(Dispatchers.IO).launch {
- operationState.postValue(Event(true))
+ onStart()
// Check if location exists
if (repository.getSavedLocations().contains(locationName)) {
- operationError.postValue(Event("$locationName already exists"))
+ onError("$locationName already exists")
return@launch
}
@@ -89,29 +81,26 @@ class WorldViewModel(
LocationType.City
)
if (repository.getSavedLocations().contains(retrievedLocation)) {
- operationError.postValue(Event("$retrievedLocation already exists"))
+ onError("$retrievedLocation already exists")
return@launch
}
// Save data if not null
repository.saveCurrentWeatherToRoom(entityItem)
repository.saveLastSavedAt(retrievedLocation)
- operationComplete.postValue(Event("$retrievedLocation saved"))
-
+ onSuccess("$retrievedLocation saved")
} catch (e: IOException) {
- operationError.postValue(Event(e.message!!))
- } finally {
- operationState.postValue(Event(false))
+ onError(e.message!!)
}
}
}
fun fetchAllLocations() {
+ onStart()
if (!repository.isSearchValid(ALL_LOADED)) {
- operationRefresh.postValue(Event(false))
+ onSuccess(Unit)
return
}
CoroutineScope(Dispatchers.IO).launch {
- operationState.postValue(Event(true))
try {
val list = mutableListOf()
repository.loadWeatherList().forEach { locationName ->
@@ -128,25 +117,25 @@ class WorldViewModel(
repository.saveWeatherListToRoom(list)
repository.saveLastSavedAt(ALL_LOADED)
} catch (e: IOException) {
- operationError.postValue(Event(e.message!!))
+ onError(e.message!!)
} finally {
- operationState.postValue(Event(false))
+ onSuccess(Unit)
}
}
}
fun deleteLocation(locationName: String) {
+ onStart()
CoroutineScope(Dispatchers.IO).launch {
- operationState.postValue(Event(true))
try {
val success = repository.deleteSavedWeatherEntry(locationName)
if (!success) {
- operationError.postValue(Event("Failed to delete"))
+ onError("Failed to delete")
}
} catch (e: IOException) {
- operationError.postValue(Event(e.message!!))
+ onError(e.message!!)
} finally {
- operationState.postValue(Event(false))
+ onSuccess(Unit)
}
}
}
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/BaseViewModel.kt
new file mode 100644
index 0000000..a2ecbc7
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/BaseViewModel.kt
@@ -0,0 +1,25 @@
+package com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.appttude.h_mal.atlas_weather.model.ViewState
+
+open class BaseViewModel: ViewModel() {
+
+ private val _uiState = MutableLiveData()
+ val uiState: LiveData = _uiState
+
+
+ fun onStart() {
+ _uiState.postValue(ViewState.HasStarted)
+ }
+
+ fun onSuccess(result: T) {
+ _uiState.postValue(ViewState.HasData(result))
+ }
+
+ protected fun onError(error: E) {
+ _uiState.postValue(ViewState.HasError(error))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/WeatherViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/WeatherViewModel.kt
index 791f549..519b53b 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/WeatherViewModel.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/baseViewModels/WeatherViewModel.kt
@@ -5,7 +5,7 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
-abstract class WeatherViewModel : ViewModel() {
+abstract class WeatherViewModel : BaseViewModel() {
fun createFullWeather(
weather: WeatherResponse,
diff --git a/app/src/main/res/layout/activity_add_forecast.xml b/app/src/main/res/layout/activity_add_forecast.xml
index 209a3ca..135df78 100644
--- a/app/src/main/res/layout/activity_add_forecast.xml
+++ b/app/src/main/res/layout/activity_add_forecast.xml
@@ -37,20 +37,4 @@
android:textStyle="bold" />
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/db_list_item.xml b/app/src/main/res/layout/db_list_item.xml
index a1e4c0f..4677d45 100644
--- a/app/src/main/res/layout/db_list_item.xml
+++ b/app/src/main/res/layout/db_list_item.xml
@@ -3,10 +3,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginLeft="24dp"
- android:layout_marginTop="6dp"
- android:layout_marginRight="24dp"
- android:layout_marginBottom="6dp"
+ android:paddingLeft="24dp"
+ android:paddingTop="6dp"
+ android:paddingRight="24dp"
+ android:paddingBottom="6dp"
+ android:background="@color/colorPrimaryDark"
android:orientation="horizontal">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ tools:listitem="@layout/db_list_item" />
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/progress_layout.xml b/app/src/main/res/layout/progress_layout.xml
new file mode 100644
index 0000000..53d4a7b
--- /dev/null
+++ b/app/src/main/res/layout/progress_layout.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 93081fc..a00ebe6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,4 +30,5 @@
Loading \nforecast…
Unable to retrieve weather
Make sure you are connected to the internet and have location permissions granted
+ No weather to display
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/EmptyViewHolder.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/EmptyViewHolder.kt
index 3f9d3a8..fa229d4 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/EmptyViewHolder.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/EmptyViewHolder.kt
@@ -12,10 +12,12 @@ class EmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var bodyTV: TextView = itemView.findViewById(R.id.body_text)
var headerTV: TextView = itemView.findViewById(R.id.header_text)
- fun bindData(@DrawableRes imageRes: Int?, header: String, body: String) {
- imageRes?.let { icon.setImageResource(it) }
- headerTV.text = header
- bodyTV.text = body
-
- }
+ fun bindData(
+ @DrawableRes imageRes: Int? = R.drawable.ic_baseline_cloud_off_24,
+ header: String = itemView.resources.getString(R.string.retrieve_warning),
+ body: String = itemView.resources.getString(R.string.empty_retrieve_warning) ){
+ imageRes?.let { icon.setImageResource(it) }
+ headerTV.text = header
+ bodyTV.text = body
+ }
}
\ No newline at end of file
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt
index 1b4e625..4c7e6d7 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt
@@ -3,43 +3,38 @@ package com.appttude.h_mal.monoWeather.ui
import android.os.Bundle
import android.view.View
-import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
import kotlinx.android.synthetic.main.fragment_home.forecast_listview
-import kotlinx.android.synthetic.main.fragment_home.progressBar
import kotlinx.android.synthetic.main.fragment_home.swipe_refresh
-class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
+class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
- private val viewModel by getFragmentViewModel()
private var param1: String? = null
+ private lateinit var recyclerAdapter: WeatherRecyclerAdapter
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
+ param1?.let { viewModel.setLocation(it) }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val recyclerAdapter = WeatherRecyclerAdapter {
+ recyclerAdapter = WeatherRecyclerAdapter {
val directions =
WorldItemFragmentDirections.actionWorldItemFragmentToFurtherDetailsFragment(it)
navigateTo(directions)
}
- param1?.let { viewModel.getSingleLocation(it) }
-
- viewModel.singleWeatherLiveData.observe(viewLifecycleOwner, Observer {
- recyclerAdapter.addCurrent(it)
- swipe_refresh.isRefreshing = false
- })
-
forecast_listview.apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
@@ -54,9 +49,19 @@ class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
}
}
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
- viewModel.operationRefresh.observe(viewLifecycleOwner, refreshObserver(swipe_refresh))
+ param1?.let { viewModel.getSingleLocation(it) }
}
+ override fun onSuccess(data: Any?) {
+ if (data is WeatherDisplay) {
+ recyclerAdapter.addCurrent(data)
+ }
+ super.onSuccess(data)
+ swipe_refresh.isRefreshing = false
+ }
+
+ override fun onFailure(error: Any?) {
+ super.onFailure(error)
+ swipe_refresh.isRefreshing = false
+ }
}
\ No newline at end of file
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/HomeFragment.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/HomeFragment.kt
index 5157d78..6eec52d 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/HomeFragment.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/HomeFragment.kt
@@ -13,10 +13,11 @@ import androidx.navigation.ui.onNavDestinationSelected
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
+import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import com.appttude.h_mal.monoWeather.dialog.PermissionsDeclarationDialog
-import com.appttude.h_mal.monoWeather.ui.BaseFragment
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
import kotlinx.android.synthetic.main.fragment_home.*
@@ -25,22 +26,16 @@ import kotlinx.android.synthetic.main.fragment_home.*
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
-class HomeFragment : BaseFragment(R.layout.fragment_home) {
-
- private val viewModel by getFragmentViewModel()
+class HomeFragment : BaseFragment(R.layout.fragment_home) {
lateinit var dialog: PermissionsDeclarationDialog
+ lateinit var recyclerAdapter: WeatherRecyclerAdapter
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
- val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
- navigateToFurtherDetails(it)
- })
-
- forecast_listview.adapter = recyclerAdapter
dialog = PermissionsDeclarationDialog(requireContext())
swipe_refresh.apply {
@@ -52,13 +47,11 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
}
}
- viewModel.weatherLiveData.observe(viewLifecycleOwner) {
- recyclerAdapter.addCurrent(it)
- }
+ recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
+ navigateToFurtherDetails(it)
+ })
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
- viewModel.operationRefresh.observe(viewLifecycleOwner, refreshObserver(swipe_refresh))
+ forecast_listview.adapter = recyclerAdapter
}
@SuppressLint("MissingPermission")
@@ -76,6 +69,20 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
dialog.dismiss()
}
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ swipe_refresh.isRefreshing = false
+
+ if (data is WeatherDisplay) {
+ recyclerAdapter.addCurrent(data)
+ }
+ }
+
+ override fun onFailure(error: Any?) {
+ super.onFailure(error)
+ swipe_refresh.isRefreshing = false
+ }
+
@SuppressLint("MissingPermission")
override fun permissionsGranted() {
viewModel.fetchData()
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/WeatherRecyclerAdapter.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/WeatherRecyclerAdapter.kt
index 2c2bcb9..e914d98 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/WeatherRecyclerAdapter.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/WeatherRecyclerAdapter.kt
@@ -83,7 +83,8 @@ class WeatherRecyclerAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getDataType(getItemViewType(position))) {
is ViewType.Empty -> {
- holder as EmptyViewHolder
+ val emptyViewHolder = holder as EmptyViewHolder
+ emptyViewHolder.bindData()
}
is ViewType.Current -> {
@@ -115,7 +116,7 @@ class WeatherRecyclerAdapter(
}
override fun getItemCount(): Int {
- return if (weather == null) 0 else 3 + (weather?.forecast?.size ?: 0)
+ return if (weather == null) 1 else 3 + (weather?.forecast?.size ?: 0)
}
}
\ No newline at end of file
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/forecast/ViewHolderForecast.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/forecast/ViewHolderForecast.kt
index 0a8594d..5a8c90a 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/forecast/ViewHolderForecast.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/home/adapter/forecast/ViewHolderForecast.kt
@@ -15,6 +15,5 @@ class ViewHolderForecast(
val adapter = GridForecastAdapter()
adapter.addCurrent(forecasts)
recyclerView.adapter = adapter
-
}
}
\ No newline at end of file
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/AddLocationFragment.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/AddLocationFragment.kt
index 19a57de..a17dcad 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/AddLocationFragment.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/AddLocationFragment.kt
@@ -2,25 +2,21 @@ package com.appttude.h_mal.monoWeather.ui.world
import android.os.Bundle
import android.view.View
-import androidx.lifecycle.observe
import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.goBack
import com.appttude.h_mal.atlas_weather.utils.hideKeyboard
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
-import com.appttude.h_mal.monoWeather.ui.BaseFragment
import kotlinx.android.synthetic.main.activity_add_forecast.location_name_tv
-import kotlinx.android.synthetic.main.activity_add_forecast.progressBar
import kotlinx.android.synthetic.main.activity_add_forecast.submit
-class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
+class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val viewModel by getFragmentViewModel()
-
submit.setOnClickListener {
val locationName = location_name_tv.text?.trim()?.toString()
if (locationName.isNullOrBlank()) {
@@ -30,14 +26,12 @@ class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
viewModel.fetchDataForSingleLocationSearch(locationName)
hideKeyboard()
}
+ }
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
-
- viewModel.operationComplete.observe(viewLifecycleOwner) {
- it?.getContentIfNotHandled()?.let { message ->
- displayToast(message)
- }
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ if (data is String) {
+ displayToast(data)
goBack()
}
}
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/WorldFragment.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/WorldFragment.kt
index a7080ca..691343e 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/WorldFragment.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/world/WorldFragment.kt
@@ -3,36 +3,35 @@ package com.appttude.h_mal.monoWeather.ui.world
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
-import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
+import com.appttude.h_mal.atlas_weather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
-import com.appttude.h_mal.monoWeather.ui.BaseFragment
import com.appttude.h_mal.monoWeather.ui.world.WorldFragmentDirections.actionWorldFragmentToWorldItemFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import kotlinx.android.synthetic.main.fragment__two.floatingActionButton
-import kotlinx.android.synthetic.main.fragment__two.progressBar
-import kotlinx.android.synthetic.main.fragment__two.world_recycler
+import kotlinx.android.synthetic.monoWeather.fragment__two.floatingActionButton
+import kotlinx.android.synthetic.monoWeather.fragment__two.world_recycler
/**
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
-class WorldFragment : BaseFragment(R.layout.fragment__two) {
- private val viewModel by getFragmentViewModel()
+class WorldFragment : BaseFragment(R.layout.fragment__two) {
+
+ lateinit var recyclerAdapter: WorldRecyclerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
viewModel.fetchAllLocations()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val recyclerAdapter = WorldRecyclerAdapter({
+ recyclerAdapter = WorldRecyclerAdapter({
val direction =
actionWorldFragmentToWorldItemFragment(it.location)
navigateTo(direction)
@@ -53,17 +52,19 @@ class WorldFragment : BaseFragment(R.layout.fragment__two) {
adapter = recyclerAdapter
}
- viewModel.weatherLiveData.observe(viewLifecycleOwner) {
- recyclerAdapter.addCurrent(it)
- }
-
floatingActionButton.setOnClickListener {
navigateTo(R.id.action_worldFragment_to_addLocationFragment)
}
- viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
- viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
+ }
+ @Suppress("UNCHECKED_CAST")
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+
+ if (data is List<*>) {
+ recyclerAdapter.addCurrent(data as List)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment__two.xml b/app/src/monoWeather/res/layout/fragment__two.xml
similarity index 72%
rename from app/src/main/res/layout/fragment__two.xml
rename to app/src/monoWeather/res/layout/fragment__two.xml
index b18ceea..2ed58e2 100644
--- a/app/src/main/res/layout/fragment__two.xml
+++ b/app/src/monoWeather/res/layout/fragment__two.xml
@@ -27,18 +27,4 @@
android:contentDescription="@string/image_string"
app:srcCompat="@drawable/ic_baseline_add_24" />
-
-
-
-
diff --git a/app/src/monoWeather/res/layout/mono_item_one.xml b/app/src/monoWeather/res/layout/mono_item_one.xml
index 4f66376..e6cbe7f 100644
--- a/app/src/monoWeather/res/layout/mono_item_one.xml
+++ b/app/src/monoWeather/res/layout/mono_item_one.xml
@@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:background="@color/colorPrimaryDark">
+ android:layout_margin="24dp"
+ android:background="@color/colorPrimaryDark">
()
+
+ //Act
+ //create a successful retrofit response
+ coEvery { api.getFromApi("", "") }.returns(mockResponse)
+
+ // Assert
+ runBlocking {
+ val result = repository.getWeatherFromApi("", "")
+ assertIs(result)
+ }
+ }
+
+ @Test
+ fun getWeatherFromApi_validLatLong_invalidResponse() {
+ //Arrange
+ val mockResponse = createErrorRetrofitMock()
+
+ //Act
+ //create a successful retrofit response
+ coEvery { api.getFromApi(any(), any()) } returns (mockResponse)
+
+ // Assert
+ val ioExceptionReturned = assertFailsWith {
+ runBlocking {
+ repository.getWeatherFromApi("", "")
+ }
+ }
+ assertEquals(ioExceptionReturned.message, "Error Code: 400")
+ }
+
+ @Test
+ fun loadWeatherList_validResponse() {
+ // Arrange
+ val elements = listOf(
+ mockk { every { id } returns any() },
+ mockk { every { id } returns any() },
+ mockk { every { id } returns any() },
+ mockk { every { id } returns any() }
+ )
+
+ //Act
+ coEvery { db.getWeatherDao().getWeatherListWithoutCurrent() } returns elements
+
+ // Assert
+ runBlocking {
+ val result = repository.loadWeatherList()
+ assertIs>(result)
+ }
+ }
+
+
}
\ No newline at end of file
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelperTest.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelperTest.kt
index e1e55d0..adb8cc8 100644
--- a/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelperTest.kt
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelperTest.kt
@@ -7,7 +7,7 @@ import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
-import com.google.gson.Gson
+import com.appttude.h_mal.atlas_weather.utils.BaseTest
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
@@ -18,9 +18,7 @@ import org.junit.Before
import org.junit.Test
import java.io.IOException
-class ServicesHelperTest {
-
- private val gson = Gson()
+class ServicesHelperTest : BaseTest() {
lateinit var helper: ServicesHelper
@@ -40,8 +38,7 @@ class ServicesHelperTest {
MockKAnnotations.init(this)
helper = ServicesHelper(repository, settingsRepository, locationProvider)
- val json = this::class.java.classLoader!!.getResource("weather_sample.json").readText()
- weatherResponse = gson.fromJson(json, WeatherResponse::class.java)
+ weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
}
@Test
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/weather_sample.json b/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/weather_sample.json
deleted file mode 100644
index 8356c51..0000000
--- a/app/src/test/java/com/appttude/h_mal/atlas_weather/helper/weather_sample.json
+++ /dev/null
@@ -1,320 +0,0 @@
-{
- "lat": 51.5,
- "lon": -0.12,
- "timezone": "Europe/London",
- "timezone_offset": 0,
- "current": {
- "dt": 1608391380,
- "sunrise": 1608364972,
- "sunset": 1608393158,
- "temp": 10.53,
- "feels_like": 5.17,
- "pressure": 1006,
- "humidity": 71,
- "dew_point": 5.5,
- "uvi": 0.12,
- "clouds": 39,
- "visibility": 10000,
- "wind_speed": 6.2,
- "wind_deg": 210,
- "weather": [
- {
- "id": 500,
- "main": "Rain",
- "description": "light rain",
- "icon": "10d"
- }
- ],
- "rain": {
- "1h": 0.19
- }
- },
- "daily": [
- {
- "dt": 1608375600,
- "sunrise": 1608364972,
- "sunset": 1608393158,
- "temp": {
- "day": 11.78,
- "min": 9.1,
- "max": 12.31,
- "night": 9.1,
- "eve": 10.27,
- "morn": 10.8
- },
- "feels_like": {
- "day": 6.46,
- "night": 5.08,
- "eve": 5.58,
- "morn": 4.63
- },
- "pressure": 1005,
- "humidity": 69,
- "dew_point": 6.42,
- "wind_speed": 6.37,
- "wind_deg": 217,
- "weather": [
- {
- "id": 500,
- "main": "Rain",
- "description": "light rain",
- "icon": "10d"
- }
- ],
- "clouds": 97,
- "pop": 0.98,
- "rain": 1.69,
- "uvi": 0.53
- },
- {
- "dt": 1608462000,
- "sunrise": 1608451406,
- "sunset": 1608479581,
- "temp": {
- "day": 9.9,
- "min": 7.41,
- "max": 10.52,
- "night": 7.41,
- "eve": 8.83,
- "morn": 8.59
- },
- "feels_like": {
- "day": 4.19,
- "night": 3.99,
- "eve": 4.55,
- "morn": 4.79
- },
- "pressure": 1013,
- "humidity": 64,
- "dew_point": 3.48,
- "wind_speed": 6.12,
- "wind_deg": 226,
- "weather": [
- {
- "id": 500,
- "main": "Rain",
- "description": "light rain",
- "icon": "10d"
- }
- ],
- "clouds": 8,
- "pop": 0.58,
- "rain": 0.65,
- "uvi": 0.45
- },
- {
- "dt": 1608548400,
- "sunrise": 1608537838,
- "sunset": 1608566008,
- "temp": {
- "day": 11.06,
- "min": 7.01,
- "max": 13.57,
- "night": 13.2,
- "eve": 12.68,
- "morn": 8.59
- },
- "feels_like": {
- "day": 6.32,
- "night": 9.99,
- "eve": 9.75,
- "morn": 4.64
- },
- "pressure": 1005,
- "humidity": 91,
- "dew_point": 9.69,
- "wind_speed": 6.7,
- "wind_deg": 185,
- "weather": [
- {
- "id": 501,
- "main": "Rain",
- "description": "moderate rain",
- "icon": "10d"
- }
- ],
- "clouds": 100,
- "pop": 1,
- "rain": 7.85,
- "uvi": 0.21
- },
- {
- "dt": 1608634800,
- "sunrise": 1608624266,
- "sunset": 1608652438,
- "temp": {
- "day": 12.97,
- "min": 11.7,
- "max": 13.21,
- "night": 11.7,
- "eve": 12.37,
- "morn": 12.93
- },
- "feels_like": {
- "day": 11.39,
- "night": 10.49,
- "eve": 10.96,
- "morn": 9.65
- },
- "pressure": 1012,
- "humidity": 83,
- "dew_point": 10.31,
- "wind_speed": 2.38,
- "wind_deg": 214,
- "weather": [
- {
- "id": 500,
- "main": "Rain",
- "description": "light rain",
- "icon": "10d"
- }
- ],
- "clouds": 100,
- "pop": 1,
- "rain": 3.25,
- "uvi": 0.34
- },
- {
- "dt": 1608721200,
- "sunrise": 1608710690,
- "sunset": 1608738871,
- "temp": {
- "day": 12.28,
- "min": 10.12,
- "max": 12.62,
- "night": 10.12,
- "eve": 10.12,
- "morn": 11.73
- },
- "feels_like": {
- "day": 7.48,
- "night": 6.73,
- "eve": 6.6,
- "morn": 8.15
- },
- "pressure": 1006,
- "humidity": 64,
- "dew_point": 5.76,
- "wind_speed": 5.45,
- "wind_deg": 224,
- "weather": [
- {
- "id": 500,
- "main": "Rain",
- "description": "light rain",
- "icon": "10d"
- }
- ],
- "clouds": 97,
- "pop": 0.94,
- "rain": 2.52,
- "uvi": 0.52
- },
- {
- "dt": 1608811200,
- "sunrise": 1608797112,
- "sunset": 1608825307,
- "temp": {
- "day": 7.3,
- "min": 4.66,
- "max": 8.32,
- "night": 4.66,
- "eve": 5.76,
- "morn": 5.85
- },
- "feels_like": {
- "day": 0.36,
- "night": -1.25,
- "eve": -0.31,
- "morn": -2.46
- },
- "pressure": 1020,
- "humidity": 60,
- "dew_point": 0.15,
- "wind_speed": 7.09,
- "wind_deg": 5,
- "weather": [
- {
- "id": 500,
- "main": "Rain",
- "description": "light rain",
- "icon": "10d"
- }
- ],
- "clouds": 85,
- "pop": 0.52,
- "rain": 0.6,
- "uvi": 1
- },
- {
- "dt": 1608897600,
- "sunrise": 1608883530,
- "sunset": 1608911747,
- "temp": {
- "day": 4.12,
- "min": 2.2,
- "max": 4.63,
- "night": 2.97,
- "eve": 3.44,
- "morn": 2.28
- },
- "feels_like": {
- "day": -0.33,
- "night": -0.43,
- "eve": -0.1,
- "morn": -2.82
- },
- "pressure": 1033,
- "humidity": 70,
- "dew_point": -3.09,
- "wind_speed": 3.35,
- "wind_deg": 334,
- "weather": [
- {
- "id": 800,
- "main": "Clear",
- "description": "clear sky",
- "icon": "01d"
- }
- ],
- "clouds": 0,
- "pop": 0,
- "uvi": 1
- },
- {
- "dt": 1608984000,
- "sunrise": 1608969944,
- "sunset": 1608998190,
- "temp": {
- "day": 6.03,
- "min": 2.76,
- "max": 6.92,
- "night": 6.92,
- "eve": 6.45,
- "morn": 3.59
- },
- "feels_like": {
- "day": 0.49,
- "night": -0.96,
- "eve": -0.28,
- "morn": -0.44
- },
- "pressure": 1024,
- "humidity": 69,
- "dew_point": 0.84,
- "wind_speed": 5.25,
- "wind_deg": 251,
- "weather": [
- {
- "id": 804,
- "main": "Clouds",
- "description": "overcast clouds",
- "icon": "04d"
- }
- ],
- "clouds": 100,
- "pop": 0,
- "uvi": 1
- }
- ]
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/utils/BaseTest.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/utils/BaseTest.kt
new file mode 100644
index 0000000..e56274f
--- /dev/null
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/utils/BaseTest.kt
@@ -0,0 +1,37 @@
+package com.appttude.h_mal.atlas_weather.utils
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import io.mockk.every
+import io.mockk.mockk
+import okhttp3.ResponseBody
+import retrofit2.Response
+
+
+open class BaseTest {
+ private val gson by lazy { Gson() }
+
+ fun getTestData(resourceName: String): T {
+ val json = this::class.java.classLoader!!.getResource(resourceName).readText()
+ val typeToken = object : TypeToken() {}.type
+ return gson.fromJson(json, typeToken)
+ }
+
+ fun getTestData(resourceName: String, cls: Class): T {
+ val json = this::class.java.classLoader!!.getResource(resourceName).readText()
+ return gson.fromJson(json, cls)
+ }
+
+ inline fun createSuccessfulRetrofitMock(): Response {
+ val mockResponse = mockk()
+ return Response.success(mockResponse)
+ }
+
+ fun createErrorRetrofitMock(code: Int = 400): Response {
+ val responseBody = mockk(relaxed = true)
+ val rawResponse = mockk().also {
+ every { it.code } returns code
+ }
+ return Response.error(code, responseBody)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/utils/testUtils.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/utils/testUtils.kt
new file mode 100644
index 0000000..7eb9191
--- /dev/null
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/utils/testUtils.kt
@@ -0,0 +1,39 @@
+package com.appttude.h_mal.atlas_weather.utils
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+fun LiveData.getOrAwaitValue(
+ time: Long = 2,
+ timeUnit: TimeUnit = TimeUnit.SECONDS
+): T {
+ var data: T? = null
+ val latch = CountDownLatch(1)
+ val observer = object : Observer {
+ override fun onChanged(o: T?) {
+ data = o
+ latch.countDown()
+ this@getOrAwaitValue.removeObserver(this)
+ }
+ }
+
+ this.observeForever(observer)
+
+ // Don't wait indefinitely if the LiveData is not set.
+ if (!latch.await(time, timeUnit)) {
+ throw TimeoutException("LiveData value was never set.")
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return data as T
+}
+
+fun sleep(millis: Long = 1000) {
+ runBlocking(Dispatchers.Default) { delay(millis) }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt
index a67f220..5b311e2 100644
--- a/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt
@@ -1,26 +1,35 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
+import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
+import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
+import com.appttude.h_mal.atlas_weather.model.ViewState
+import com.appttude.h_mal.atlas_weather.model.types.LocationType
+import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
+import com.appttude.h_mal.atlas_weather.utils.BaseTest
+import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
+import com.appttude.h_mal.atlas_weather.utils.sleep
import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
-import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
+import java.io.IOException
+import kotlin.test.assertIs
+
+
+class WorldViewModelTest : BaseTest() {
-@Suppress("unused")
-class WorldViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
+ @InjectMockKs
lateinit var viewModel: WorldViewModel
@MockK(relaxed = true)
@@ -29,46 +38,102 @@ class WorldViewModelTest {
@MockK
lateinit var locationProvider: LocationProviderImpl
+ private lateinit var weatherResponse: WeatherResponse
@Before
fun setUp() {
MockKAnnotations.init(this)
- viewModel = WorldViewModel(locationProvider, repository)
+ weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
+ }
+
+ @Test
+ fun fetchDataForSingleLocation_validLocation_validReturn() {
+ // Arrange
+ val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
+ temperatureUnit = "°C"
+ locationString = CURRENT_LOCATION
+ })
+
+ // Act
+ every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
+ coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
+ weatherResponse.lat,
+ weatherResponse.lon
+ )
+ coEvery {
+ repository.getWeatherFromApi(
+ weatherResponse.lat.toString(),
+ weatherResponse.lon.toString()
+ )
+ }.returns(weatherResponse)
+ coEvery {
+ locationProvider.getLocationNameFromLatLong(
+ weatherResponse.lat,
+ weatherResponse.lon,
+ LocationType.City
+ )
+ }.returns(CURRENT_LOCATION)
+ every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
+ coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
+
+ viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
+
+ // Assert
+ viewModel.uiState.observeForever {
+ println(it.javaClass.name)
+ }
+
+ sleep(3000)
+ assertIs>(viewModel.uiState.getOrAwaitValue())
}
@Test
fun fetchDataForSingleLocation_invalidLocation_invalidReturn() {
+ // Arrange
val location = CURRENT_LOCATION
+ // Act
+ every { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } throws IOException("Unable to get location")
+ every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
+ coEvery {
+ repository.getWeatherFromApi(
+ weatherResponse.lat.toString(),
+ weatherResponse.lon.toString()
+ )
+ }.returns(weatherResponse)
+
viewModel.fetchDataForSingleLocation(location)
- assertEquals(viewModel.operationRefresh.getOrAwaitValue()?.getContentIfNotHandled(), false)
- }
-}
-
-
-fun LiveData.getOrAwaitValue(
- time: Long = 2,
- timeUnit: TimeUnit = TimeUnit.SECONDS
-): T {
- var data: T? = null
- val latch = CountDownLatch(1)
- val observer = object : Observer {
- override fun onChanged(o: T?) {
- data = o
- latch.countDown()
- this@getOrAwaitValue.removeObserver(this)
- }
+ // Assert
+ sleep(300)
+ assertIs>(viewModel.uiState.getOrAwaitValue())
}
- this.observeForever(observer)
+ @Test
+ fun searchAboveFallbackTime_validLocation_validReturn() {
+ // Arrange
+ val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
+ temperatureUnit = "°C"
+ locationString = CURRENT_LOCATION
+ })
- // Don't wait indefinitely if the LiveData is not set.
- if (!latch.await(time, timeUnit)) {
- throw TimeoutException("LiveData value was never set.")
+ // Act
+ coEvery { repository.getSingleWeather(CURRENT_LOCATION) }.returns(entityItem)
+ every { repository.isSearchValid(CURRENT_LOCATION) }.returns(false)
+ coEvery {
+ repository.getWeatherFromApi(
+ weatherResponse.lat.toString(),
+ weatherResponse.lon.toString()
+ )
+ }.returns(weatherResponse)
+ every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
+ coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
+
+ viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
+
+ // Assert
+ sleep(300)
+ assertIs>(viewModel.uiState.getOrAwaitValue())
}
-
- @Suppress("UNCHECKED_CAST")
- return data as T
}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 8457659..6e4fe3f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -20,3 +20,4 @@ dependencyResolutionManagement {
}
rootProject.name = "Atlas Weather"
include ':app'
+include ':test-resources'