├── .github
├── ci-gradle.properties
└── workflows
│ └── Check.yaml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README-template.md
├── README.md
├── app
├── .gitignore
├── build.gradle
├── debug
│ └── output-metadata.json
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── pp
│ │ └── jetweatherfy
│ │ ├── ForecastScreenTest.kt
│ │ └── fakes
│ │ ├── FakeCityRepository.kt
│ │ └── FakeForecastRepository.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── ic_logo-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── pp
│ │ │ └── jetweatherfy
│ │ │ └── app
│ │ │ └── JetWeatherfyApp.kt
│ └── res
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_logo.xml
│ │ └── ic_logo_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_logo.png
│ │ ├── ic_logo_foreground.png
│ │ └── ic_logo_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_logo.png
│ │ ├── ic_logo_foreground.png
│ │ └── ic_logo_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_logo.png
│ │ ├── ic_logo_foreground.png
│ │ └── ic_logo_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_logo.png
│ │ ├── ic_logo_foreground.png
│ │ └── ic_logo_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_logo.png
│ │ ├── ic_logo_foreground.png
│ │ └── ic_logo_round.png
│ │ └── values
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_logo_background.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── pp
│ └── jetweatherfy
│ └── ExampleUnitTest.kt
├── build.gradle
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── pp
│ └── jetweatherfy
│ └── buildsrc
│ └── Dependencies.kt
├── data
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── pp
│ └── jetweatherfy
│ └── data
│ ├── city
│ ├── CityDao.kt
│ └── FakeCityDao.kt
│ ├── di
│ ├── DaoModule.kt
│ └── DataModule.kt
│ ├── forecast
│ ├── FakeForecastDao.kt
│ └── ForecastDao.kt
│ └── repositories
│ ├── CityRepository.kt
│ └── ForecastRepository.kt
├── debug.keystore
├── domain
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── pp
│ │ └── jetweatherfy
│ │ └── domain
│ │ ├── Constants.kt
│ │ ├── base
│ │ ├── FlowUseCase.kt
│ │ └── Result.kt
│ │ ├── di
│ │ ├── DispatcherQualifiers.kt
│ │ └── DispatchersModule.kt
│ │ ├── model
│ │ └── Forecast.kt
│ │ ├── repositories
│ │ ├── cities
│ │ │ └── ICityRepository.kt
│ │ └── forecast
│ │ │ └── IForecastRepository.kt
│ │ └── usecases
│ │ ├── cities
│ │ ├── AddCity.kt
│ │ └── FetchCities.kt
│ │ └── forecast
│ │ └── FetchForecast.kt
│ └── test
│ └── java
│ └── com
│ └── pp
│ └── jetweatherfy
│ └── domain
│ ├── fakes
│ ├── FakeCityRepository.kt
│ └── FakeForecastRepository.kt
│ └── usecases
│ ├── cities
│ ├── AddCityUseCaseTest.kt
│ └── FetchCitiesUseCaseTest.kt
│ └── forecast
│ └── FetchForecastUseCaseTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── presentation
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── pp
│ │ └── jetweatherfy
│ │ └── presentation
│ │ ├── di
│ │ └── LocationModule.kt
│ │ ├── forecast
│ │ ├── ForecastActivity.kt
│ │ ├── ForecastScreen.kt
│ │ ├── ForecastViewModel.kt
│ │ ├── base
│ │ │ └── LocationActivity.kt
│ │ ├── components
│ │ │ ├── content
│ │ │ │ ├── ForecastContent.kt
│ │ │ │ ├── detailed
│ │ │ │ │ ├── Days.kt
│ │ │ │ │ ├── Details.kt
│ │ │ │ │ └── ForecastDetailedView.kt
│ │ │ │ └── simple
│ │ │ │ │ ├── Days.kt
│ │ │ │ │ ├── Details.kt
│ │ │ │ │ ├── ForecastSimpleView.kt
│ │ │ │ │ └── Hours.kt
│ │ │ ├── surface
│ │ │ │ └── ForecastSurface.kt
│ │ │ ├── topbar
│ │ │ │ ├── ForecastSearchBar.kt
│ │ │ │ └── ForecastTopBar.kt
│ │ │ └── utils
│ │ │ │ ├── WeatherAnimation.kt
│ │ │ │ ├── WeatherTemperature.kt
│ │ │ │ └── WeatherWindAndPrecipitation.kt
│ │ ├── events
│ │ │ ├── ForecastViewEvent.kt
│ │ │ └── LocationViewEvent.kt
│ │ ├── navigation
│ │ │ ├── NavigationCommand.kt
│ │ │ ├── NavigationDirections.kt
│ │ │ └── NavigationManager.kt
│ │ └── state
│ │ │ ├── ForecastViewState.kt
│ │ │ └── LocationViewState.kt
│ │ ├── theme
│ │ ├── Color.kt
│ │ ├── Dimensions.kt
│ │ ├── Shape.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ │ └── utils
│ │ ├── Constants.kt
│ │ ├── ForecastUtils.kt
│ │ ├── GradientColorUtils.kt
│ │ └── Utils.kt
│ └── res
│ ├── drawable-anydpi
│ ├── ic_arrow_down.xml
│ ├── ic_arrow_up.xml
│ ├── ic_check.xml
│ ├── ic_clear.xml
│ ├── ic_list.xml
│ ├── ic_location.xml
│ └── ic_my_location.xml
│ ├── drawable-hdpi
│ ├── ic_arrow_down.png
│ ├── ic_arrow_up.png
│ ├── ic_check.png
│ ├── ic_clear.png
│ ├── ic_list.png
│ ├── ic_location.png
│ └── ic_my_location.png
│ ├── drawable-mdpi
│ ├── ic_arrow_down.png
│ ├── ic_arrow_up.png
│ ├── ic_check.png
│ ├── ic_clear.png
│ ├── ic_list.png
│ ├── ic_location.png
│ └── ic_my_location.png
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable-xhdpi
│ ├── ic_arrow_down.png
│ ├── ic_arrow_up.png
│ ├── ic_check.png
│ ├── ic_clear.png
│ ├── ic_list.png
│ ├── ic_location.png
│ └── ic_my_location.png
│ ├── drawable-xxhdpi
│ ├── ic_arrow_down.png
│ ├── ic_arrow_up.png
│ ├── ic_check.png
│ ├── ic_clear.png
│ ├── ic_list.png
│ ├── ic_location.png
│ └── ic_my_location.png
│ ├── drawable
│ ├── centigrade.png
│ ├── detailed_view.png
│ ├── fahrenheit.png
│ ├── ic_drop.png
│ └── ic_wind.png
│ ├── font
│ ├── comfortaa_bold.ttf
│ ├── comfortaa_light.ttf
│ └── comfortaa_semibold.ttf
│ ├── raw
│ ├── cloudy.json
│ ├── night.json
│ ├── rainy.json
│ ├── sunny.json
│ ├── thunderstorm.json
│ └── windy.json
│ ├── values-es-rES
│ └── strings.xml
│ ├── values-night
│ └── themes.xml
│ ├── values-pt-rPT
│ └── strings.xml
│ └── values
│ ├── colors.xml
│ ├── ic_launcher_background.xml
│ ├── ic_logo_background.xml
│ ├── strings.xml
│ └── themes.xml
├── results
├── screenshot_1.png
├── screenshot_2.png
└── video.mp4
├── settings.gradle
└── spotless
└── copyright.kt
/.github/ci-gradle.properties:
--------------------------------------------------------------------------------
1 |
2 | #
3 | # Copyright 2020 The Android Open Source Project
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | org.gradle.daemon=false
19 | org.gradle.parallel=true
20 | org.gradle.jvmargs=-Xmx5120m
21 | org.gradle.workers.max=2
22 |
23 | kotlin.incremental=false
24 | kotlin.compiler.execution.strategy=in-process
25 |
--------------------------------------------------------------------------------
/.github/workflows/Check.yaml:
--------------------------------------------------------------------------------
1 | name: Check
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | timeout-minutes: 30
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v2
16 |
17 | - name: Copy CI gradle.properties
18 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
19 |
20 | - name: Set up JDK 11
21 | uses: actions/setup-java@v1
22 | with:
23 | java-version: 11
24 |
25 | - name: Build project
26 | run: ./gradlew spotlessCheck assembleDebug lintDebug --stacktrace
27 |
28 | - name: Upload build outputs (APKs)
29 | uses: actions/upload-artifact@v2
30 | with:
31 | name: build-outputs
32 | path: app/build/outputs
33 |
34 | - name: Upload build reports
35 | if: always()
36 | uses: actions/upload-artifact@v2
37 | with:
38 | name: build-reports
39 | path: app/build/reports
40 |
41 | test:
42 | needs: build
43 | runs-on: macOS-latest # enables hardware acceleration in the virtual machine
44 | timeout-minutes: 30
45 | strategy:
46 | matrix:
47 | api-level: [23, 26, 29]
48 |
49 | steps:
50 | - name: Checkout
51 | uses: actions/checkout@v2
52 |
53 | - name: Copy CI gradle.properties
54 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
55 |
56 | - name: Set up JDK 11
57 | uses: actions/setup-java@v1
58 | with:
59 | java-version: 11
60 |
61 | - name: Run instrumentation tests
62 | uses: reactivecircus/android-emulator-runner@v2
63 | with:
64 | api-level: ${{ matrix.api-level }}
65 | arch: x86
66 | disable-animations: true
67 | script: ./gradlew connectedCheck --stacktrace
68 |
69 | - name: Upload test reports
70 | if: always()
71 | uses: actions/upload-artifact@v2
72 | with:
73 | name: test-reports
74 | path: app/build/reports
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Mac files
6 | .DS_Store
7 |
8 | # files for the dex VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # generated files
15 | bin/
16 | gen/
17 |
18 | # Ignore gradle files
19 | .gradle/
20 | build/
21 |
22 | # Local configuration file (sdk path, etc)
23 | local.properties
24 |
25 | # Proguard folder generated by Eclipse
26 | proguard/
27 | proguard-project.txt
28 |
29 | # Eclipse files
30 | .project
31 | .classpath
32 | .settings/
33 |
34 | # Android Studio/IDEA
35 | *.iml
36 | .idea
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
--------------------------------------------------------------------------------
/README-template.md:
--------------------------------------------------------------------------------
1 | # Put title of your app here
2 |
3 |
4 |
5 | 
6 |
7 |
8 | ## :scroll: Description
9 |
10 |
11 |
12 | ## :bulb: Motivation and Context
13 |
14 |
15 |
16 |
17 | ## :camera_flash: Screenshots
18 |
19 |
20 |
21 | ## License
22 | ```
23 | Copyright 2020 The Android Open Source Project
24 |
25 | Licensed under the Apache License, Version 2.0 (the "License");
26 | you may not use this file except in compliance with the License.
27 | You may obtain a copy of the License at
28 |
29 | https://www.apache.org/licenses/LICENSE-2.0
30 |
31 | Unless required by applicable law or agreed to in writing, software
32 | distributed under the License is distributed on an "AS IS" BASIS,
33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34 | See the License for the specific language governing permissions and
35 | limitations under the License.
36 | ```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | JetWeatherfy
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## :scroll: Description
11 |
12 | This is a weather forecast application, using Fake data, developed according to the 4th Week of Android Dev Challenges with Jetpack Compose.
13 | I'm really happy to announce this project was a winner in the Code Quality category!
14 |
15 | Announced winners
16 |
17 |
18 | Download an APK here!
19 |
20 | ## Requirements
21 | - Android Studio Artic Fox | 2020.3.1 Canary 15
22 | - Gradle 7.0
23 |
24 | ## :bulb: Motivation and Context
25 |
26 |
27 | I love Android development, honestly.
28 | Since Jetpack Compose is a totally different way of thinking in terms of UI building, I decided to take these weekly Android Dev Challenges, as a way to enter in its world.
29 |
30 | ## :camera_flash: Screenshots
31 |
32 |
33 |
34 |
35 | ## MAD Score
36 | 
37 | 
38 |
39 |
40 |
41 | ## Author
42 |
43 |
44 |
45 | So... Paulo is from Portugal and he's 23 years old.
46 | He has around 3y and half of work experience as an Android Developer, and has a lot of fun programming in Kotlin!
47 | He's also certified by Google as an Associate Android Developer and a Android Tech Editor at Raywenderlich.
48 | He is a person with good communication skills, easily adaptive to new environments and teams, and a little addicted to learning and self-improvement.
49 |
50 |
51 | ## Find this repository useful? :]
52 |
53 | Feel free to support me and my new content on:
54 |
55 |
56 |
57 |
58 |
59 | ## License
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle
2 | .gradle
3 | build/
4 |
5 | captures
6 |
7 | /local.properties
8 |
9 | # IntelliJ .idea folder
10 | /.idea
11 | *.iml
12 |
13 | # General
14 | .DS_Store
15 | .externalNativeBuild
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | import com.pp.jetweatherfy.buildsrc.Compose
2 | import com.pp.jetweatherfy.buildsrc.Configs
3 | import com.pp.jetweatherfy.buildsrc.Core
4 | import com.pp.jetweatherfy.buildsrc.Libs
5 | import com.pp.jetweatherfy.buildsrc.Tests
6 |
7 | plugins {
8 | id 'com.android.application'
9 | id 'kotlin-android'
10 | id 'kotlin-kapt'
11 | id 'dagger.hilt.android.plugin'
12 | }
13 |
14 | android {
15 | compileSdkVersion Configs.CompileSdkVersion
16 |
17 | defaultConfig {
18 | applicationId "com.pp.jetweatherfy"
19 | minSdkVersion Configs.MinSdkVersion
20 | targetSdkVersion Configs.TargetSdkVersion
21 | versionCode Configs.VersionCode
22 | versionName Configs.VersionName
23 |
24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
25 |
26 | }
27 |
28 | signingConfigs {
29 | // We use a bundled debug keystore, to allow debug builds from CI to be upgradable
30 | debug {
31 | storeFile rootProject.file('debug.keystore')
32 | storePassword 'android'
33 | keyAlias 'androiddebugkey'
34 | keyPassword 'android'
35 | }
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility JavaVersion.VERSION_1_8
40 | targetCompatibility JavaVersion.VERSION_1_8
41 | }
42 |
43 | kotlinOptions {
44 | jvmTarget = "1.8"
45 | }
46 |
47 | buildTypes {
48 | debug {
49 | signingConfig signingConfigs.debug
50 | }
51 | release {
52 | minifyEnabled false
53 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
54 | }
55 | }
56 |
57 | buildFeatures {
58 | compose true
59 |
60 | // Disable unused AGP features
61 | buildConfig false
62 | aidl false
63 | renderScript false
64 | resValues false
65 | shaders false
66 | }
67 |
68 | composeOptions {
69 | kotlinCompilerExtensionVersion Compose.composeVersion
70 | }
71 |
72 | packagingOptions {
73 | // Multiple dependency bring these files in. Exclude them to enable
74 | // our test APK to build (has no effect on our AARs)
75 | excludes += "/META-INF/AL2.0"
76 | excludes += "/META-INF/LGPL2.1"
77 | }
78 | }
79 |
80 | dependencies {
81 | implementation project(':presentation')
82 | implementation project(':domain')
83 | implementation project(':data')
84 |
85 | implementation Core.androidXCore
86 | implementation Core.appCompat
87 | implementation Core.material
88 |
89 | implementation Compose.activityCompose
90 | implementation Compose.ui
91 | implementation Compose.material
92 | implementation Compose.iconsExtended
93 | implementation Compose.tooling
94 | implementation Compose.runtime
95 | implementation Compose.hiltNavigation
96 | implementation Libs.DateTime.jodaTime
97 | implementation Libs.Lottie.lottie
98 | implementation Libs.Accompanist.insets
99 | implementation Libs.GoogleLocation.location
100 |
101 | implementation Libs.Kotlin.stdlib
102 | implementation Libs.Coroutines.android
103 |
104 | implementation Libs.Hilt.library
105 | kapt Libs.Hilt.googleAndroidCompiler
106 | kapt Libs.Hilt.googleCompiler
107 |
108 | androidTestImplementation Tests.junit
109 | androidTestImplementation Tests.junitKotlin
110 | androidTestImplementation Compose.uiTest
111 | androidTestImplementation Libs.Coroutines.test
112 |
113 | }
--------------------------------------------------------------------------------
/app/debug/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.pp.jetweatherfy",
8 | "variantName": "debug",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.0",
16 | "outputFile": "app-debug.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/pp/jetweatherfy/ForecastScreenTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy
17 |
18 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
19 | import androidx.test.ext.junit.runners.AndroidJUnit4
20 | import com.google.accompanist.insets.ProvideWindowInsets
21 | import com.pp.jetweatherfy.domain.usecases.cities.AddCity
22 | import com.pp.jetweatherfy.domain.usecases.cities.FetchCities
23 | import com.pp.jetweatherfy.domain.usecases.forecast.FetchForecast
24 | import com.pp.jetweatherfy.fakes.FakeCityRepository
25 | import com.pp.jetweatherfy.fakes.FakeForecastRepository
26 | import com.pp.jetweatherfy.presentation.forecast.ForecastActivity
27 | import com.pp.jetweatherfy.presentation.forecast.ForecastScreen
28 | import com.pp.jetweatherfy.presentation.forecast.ForecastViewModel
29 | import com.pp.jetweatherfy.presentation.forecast.state.ViewStatus
30 | import com.pp.jetweatherfy.presentation.theme.JetWeatherfyTheme
31 | import kotlinx.coroutines.ExperimentalCoroutinesApi
32 | import kotlinx.coroutines.test.TestCoroutineDispatcher
33 | import kotlinx.coroutines.test.TestCoroutineScope
34 | import kotlinx.coroutines.test.runBlockingTest
35 | import org.junit.Before
36 | import org.junit.Rule
37 | import org.junit.Test
38 | import org.junit.runner.RunWith
39 |
40 | @ExperimentalCoroutinesApi
41 | @RunWith(AndroidJUnit4::class)
42 | class ForecastScreenTest {
43 |
44 | private val testDispatcher = TestCoroutineDispatcher()
45 | private val testScope = TestCoroutineScope(testDispatcher)
46 |
47 | @get:Rule
48 | val composeTestRule = createAndroidComposeRule()
49 |
50 | private lateinit var viewModel: ForecastViewModel
51 |
52 | private val fetchForecastUseCase = FetchForecast(
53 | forecastRepository = FakeForecastRepository(),
54 | dispatcher = testDispatcher
55 | )
56 |
57 | private val fetchCitiesUseCase = FetchCities(
58 | citiesRepository = FakeCityRepository(),
59 | dispatcher = testDispatcher
60 | )
61 |
62 | private val addCityUseCase = AddCity(
63 | citiesRepository = FakeCityRepository(),
64 | dispatcher = testDispatcher
65 | )
66 |
67 | @Before
68 | fun setUp() {
69 | composeTestRule.setContent {
70 | JetWeatherfyTheme {
71 | ProvideWindowInsets {
72 | viewModel = ForecastViewModel(
73 | fetchForecast = fetchForecastUseCase,
74 | fetchCities = fetchCitiesUseCase,
75 | addCity = addCityUseCase
76 | )
77 | ForecastScreen(viewModel = viewModel, onLocationRequested = {})
78 | }
79 | }
80 | }
81 | }
82 |
83 | @Test
84 | fun forecast_screen_first_state_idle() = testScope.runBlockingTest {
85 | // Given
86 | val expectedViewStatus = ViewStatus.Idle
87 |
88 | // When
89 | val currentStatus = viewModel.forecastViewState.value.viewStatus
90 |
91 | // Then
92 | assert(currentStatus == expectedViewStatus)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/pp/jetweatherfy/fakes/FakeCityRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.fakes
17 |
18 | import com.pp.jetweatherfy.domain.repositories.cities.ICityRepository
19 | import java.util.Locale
20 |
21 | class FakeCityRepository : ICityRepository {
22 | private val cities = mutableListOf(
23 | "San Francisco",
24 | "London",
25 | "New York",
26 | "Paris",
27 | "Moscow",
28 | "Tokyo",
29 | "Dubai",
30 | "Toronto"
31 | )
32 |
33 | override suspend fun getCities(query: String): List {
34 | return cities.filter {
35 | it.toLowerCase(Locale.getDefault())
36 | .startsWith(query.toLowerCase(Locale.getDefault()))
37 | }
38 | }
39 |
40 | override suspend fun addCity(city: String) {
41 | if (!cities.contains(city))
42 | cities.add(city)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/pp/jetweatherfy/fakes/FakeForecastRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.fakes
17 |
18 | import com.pp.jetweatherfy.domain.model.DailyForecast
19 | import com.pp.jetweatherfy.domain.model.Forecast
20 | import com.pp.jetweatherfy.domain.repositories.forecast.IForecastRepository
21 |
22 | class FakeForecastRepository : IForecastRepository {
23 |
24 | private val lisbonForecast = Forecast(
25 | city = "Lisbon",
26 | dailyForecasts = listOf()
27 | )
28 |
29 | private val londonForecast = Forecast(
30 | city = "London",
31 | dailyForecasts = listOf(DailyForecast())
32 | )
33 |
34 | override suspend fun getForecast(city: String): Forecast {
35 | return when (city) {
36 | lisbonForecast.city -> lisbonForecast
37 | londonForecast.city -> londonForecast
38 | else -> Forecast()
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
27 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_logo-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/ic_logo-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/pp/jetweatherfy/app/JetWeatherfyApp.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.app
17 |
18 | import android.app.Application
19 | import dagger.hilt.android.HiltAndroidApp
20 |
21 | @HiltAndroidApp
22 | class JetWeatherfyApp : Application()
23 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_logo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_logo_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-hdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_logo_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-hdpi/ic_logo_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-hdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-mdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_logo_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-mdpi/ic_logo_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-mdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xhdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_logo_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xhdpi/ic_logo_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xhdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xxhdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_logo_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xxhdpi/ic_logo_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xxhdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xxxhdpi/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_logo_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xxxhdpi/ic_logo_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_logo_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/app/src/main/res/mipmap-xxxhdpi/ic_logo_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_logo_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 | JetWeatherfy
13 |
--------------------------------------------------------------------------------
/app/src/test/java/com/pp/jetweatherfy/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy
17 |
18 | /**
19 | * Example local unit test, which will execute on the development machine (host).
20 | *
21 | * See [testing documentation](http://d.android.com/tools/testing).
22 | */
23 | class ExampleUnitTest {
24 | // Add unit tests here
25 | }
26 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import com.pp.jetweatherfy.buildsrc.ClassPaths
18 | import com.pp.jetweatherfy.buildsrc.Libs
19 |
20 | buildscript {
21 |
22 | repositories {
23 | google()
24 | mavenCentral()
25 | }
26 |
27 | dependencies {
28 | classpath ClassPaths.gradlePlugin
29 | classpath ClassPaths.kotlinPlugin
30 | classpath Libs.Hilt.gradlePlugin
31 |
32 | }
33 | }
34 |
35 | plugins {
36 | id 'com.diffplug.spotless' version '5.7.0'
37 | }
38 |
39 | subprojects {
40 | repositories {
41 | google()
42 | mavenCentral()
43 | }
44 |
45 | apply plugin: 'com.diffplug.spotless'
46 | spotless {
47 | kotlin {
48 | target '**/*.kt'
49 | targetExclude("$buildDir/**/*.kt")
50 | targetExclude('bin/**/*.kt')
51 |
52 | ktlint("0.41.0")
53 | licenseHeaderFile rootProject.file('spotless/copyright.kt')
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | plugins {
22 | `kotlin-dsl`
23 | }
24 |
--------------------------------------------------------------------------------
/buildSrc/src/main/java/com/pp/jetweatherfy/buildsrc/Dependencies.kt:
--------------------------------------------------------------------------------
1 | package com.pp.jetweatherfy.buildsrc
2 |
3 | object Configs {
4 | const val CompileSdkVersion = 30
5 | const val MinSdkVersion = 23
6 | const val TargetSdkVersion = 30
7 |
8 | const val VersionCode = 1
9 | const val VersionName = "1.0"
10 | }
11 |
12 | object ClassPaths {
13 | const val gradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha15"
14 | const val kotlinPlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
15 | }
16 |
17 | object Compose {
18 | const val composeVersion = "1.0.0-beta06"
19 |
20 | const val animation = "androidx.compose.animation:animation:$composeVersion"
21 | const val iconsExtended = "androidx.compose.material:material-icons-extended:$composeVersion"
22 | const val material = "androidx.compose.material:material:$composeVersion"
23 | const val runtime = "androidx.compose.runtime:runtime:$composeVersion"
24 | const val tooling = "androidx.compose.ui:ui-tooling:$composeVersion"
25 | const val ui = "androidx.compose.ui:ui:$composeVersion"
26 | const val uiUtil = "androidx.compose.ui:ui-util:$composeVersion"
27 | const val uiTest = "androidx.compose.ui:ui-test-junit4:$composeVersion"
28 | const val activityCompose = "androidx.activity:activity-compose:1.3.0-alpha07"
29 | const val navigationCompose = "androidx.navigation:navigation-compose:1.0.0-alpha10"
30 | const val hiltNavigation = "androidx.hilt:hilt-navigation-compose:1.0.0-alpha01"
31 | }
32 |
33 | object Tests {
34 | private const val junitVersion = "4.13.2"
35 | private const val junitKtx = "1.1.2"
36 |
37 | const val junit = "junit:junit:$junitVersion"
38 | const val junitKotlin = "androidx.test.ext:junit-ktx:$junitKtx"
39 | }
40 |
41 | object Core {
42 | const val androidXCore = "androidx.core:core-ktx:1.3.2"
43 | const val appCompat = "androidx.appcompat:appcompat:1.3.0-rc01"
44 | const val material = "com.google.android.material:material:1.3.0"
45 | }
46 |
47 | object Libs {
48 |
49 | object Kotlin {
50 | const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0"
51 | }
52 |
53 | object Coroutines {
54 | private const val version = "1.5.0-RC"
55 | const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
56 | const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
57 | const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"
58 | }
59 |
60 | object DateTime {
61 | const val jodaTime = "net.danlew:android.joda:2.10.9.1"
62 | }
63 |
64 | object Lottie {
65 | const val lottie = "com.airbnb.android:lottie-compose:1.0.0-beta03-1"
66 | }
67 |
68 | object Accompanist {
69 | const val insets = "com.google.accompanist:accompanist-insets:0.9.1"
70 | }
71 |
72 | object GoogleLocation {
73 | const val location = "com.google.android.gms:play-services-location:18.0.0"
74 | }
75 |
76 | object Hilt {
77 | private const val version = "2.35"
78 | const val library = "com.google.dagger:hilt-android:$version"
79 | const val googleAndroidCompiler = "com.google.dagger:hilt-android-compiler:$version"
80 | const val googleCompiler = "com.google.dagger:hilt-compiler:$version"
81 | const val testing = "com.google.dagger:hilt-android-testing:$version"
82 | const val gradlePlugin = "com.google.dagger:hilt-android-gradle-plugin:$version"
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | import com.pp.jetweatherfy.buildsrc.Configs
2 | import com.pp.jetweatherfy.buildsrc.Libs
3 |
4 | plugins {
5 | id 'com.android.library'
6 | id 'kotlin-android'
7 | id 'kotlin-kapt'
8 | id 'dagger.hilt.android.plugin'
9 | }
10 |
11 | android {
12 | compileSdkVersion Configs.CompileSdkVersion
13 |
14 | defaultConfig {
15 | minSdkVersion Configs.MinSdkVersion
16 | targetSdkVersion Configs.TargetSdkVersion
17 | versionCode Configs.VersionCode
18 | versionName Configs.VersionName
19 |
20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
21 | consumerProguardFiles "consumer-rules.pro"
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 |
29 | kotlinOptions {
30 | jvmTarget = '1.8'
31 | }
32 |
33 | packagingOptions {
34 | // Multiple dependency bring these files in. Exclude them to enable
35 | // our test APK to build (has no effect on our AARs)
36 | excludes += "/META-INF/AL2.0"
37 | excludes += "/META-INF/LGPL2.1"
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation project(':domain')
43 |
44 | implementation Libs.Hilt.library
45 | kapt Libs.Hilt.googleAndroidCompiler
46 | kapt Libs.Hilt.googleCompiler
47 |
48 | implementation Libs.DateTime.jodaTime
49 | }
--------------------------------------------------------------------------------
/data/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/data/consumer-rules.pro
--------------------------------------------------------------------------------
/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/city/CityDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.city
17 |
18 | interface CityDao {
19 | suspend fun getCities(): List
20 | suspend fun addCity(city: String)
21 | }
22 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/city/FakeCityDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.city
17 |
18 | class FakeCityDao : CityDao {
19 |
20 | private val cities = mutableListOf(
21 | "London",
22 | "New York",
23 | "Paris",
24 | "Moscow",
25 | "Tokyo",
26 | "Dubai",
27 | "Singapore",
28 | "Barcelona",
29 | "Los Angeles",
30 | "San Francisco",
31 | "Madrid",
32 | "Rome",
33 | "Chicago",
34 | "Toronto",
35 | "Abu Dhabi",
36 | "St. Petersburg",
37 | "Amsterdam",
38 | "Berlin",
39 | "Prague",
40 | "Lisbon",
41 | "Washington",
42 | "Istanbul",
43 | "Las Vegas",
44 | "Seoul",
45 | "Sydney",
46 | "Miami",
47 | "Munich",
48 | "Milan",
49 | "San Diego",
50 | "Bangkok",
51 | "Vienna",
52 | "Dublin",
53 | "Vancouver",
54 | "Boston",
55 | "Zurich",
56 | "Budapest",
57 | "Houston",
58 | "Seattle",
59 | "Montreal",
60 | "Hong Kong",
61 | "Frankfurt",
62 | "São Paulo",
63 | "Copenhagen",
64 | "Atlanta",
65 | "Buenos Aires"
66 | )
67 |
68 | override suspend fun getCities() = cities
69 |
70 | override suspend fun addCity(city: String) {
71 | if (!cities.contains(city)) {
72 | cities.add(city)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/di/DaoModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.di
17 |
18 | import com.pp.jetweatherfy.data.city.CityDao
19 | import com.pp.jetweatherfy.data.city.FakeCityDao
20 | import com.pp.jetweatherfy.data.forecast.FakeForecastDao
21 | import com.pp.jetweatherfy.data.forecast.ForecastDao
22 | import dagger.Module
23 | import dagger.Provides
24 | import dagger.hilt.InstallIn
25 | import dagger.hilt.components.SingletonComponent
26 | import javax.inject.Singleton
27 |
28 | @Module
29 | @InstallIn(SingletonComponent::class)
30 | object DaoModule {
31 |
32 | @Provides
33 | @Singleton
34 | fun provideCityDao(): CityDao = FakeCityDao()
35 |
36 | @Provides
37 | @Singleton
38 | fun provideForecastDao(): ForecastDao = FakeForecastDao()
39 | }
40 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/di/DataModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.di
17 |
18 | import com.pp.jetweatherfy.data.repositories.CityRepository
19 | import com.pp.jetweatherfy.data.repositories.ForecastRepository
20 | import com.pp.jetweatherfy.domain.repositories.cities.ICityRepository
21 | import com.pp.jetweatherfy.domain.repositories.forecast.IForecastRepository
22 | import dagger.Binds
23 | import dagger.Module
24 | import dagger.hilt.InstallIn
25 | import dagger.hilt.components.SingletonComponent
26 | import javax.inject.Singleton
27 |
28 | @Module
29 | @InstallIn(SingletonComponent::class)
30 | interface DataModule {
31 |
32 | @Binds
33 | @Singleton
34 | fun bindForecastRepository(
35 | forecastRepository: ForecastRepository
36 | ): IForecastRepository
37 |
38 | @Binds
39 | @Singleton
40 | fun bindCityRepository(
41 | cityRepository: CityRepository
42 | ): ICityRepository
43 | }
44 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/forecast/FakeForecastDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.forecast
17 |
18 | import com.pp.jetweatherfy.domain.model.DailyForecast
19 | import com.pp.jetweatherfy.domain.model.Forecast
20 | import com.pp.jetweatherfy.domain.model.HourlyForecast
21 | import com.pp.jetweatherfy.domain.model.Weather
22 | import org.joda.time.LocalDateTime
23 | import kotlin.math.roundToInt
24 | import kotlin.random.Random
25 |
26 | class FakeForecastDao : ForecastDao {
27 |
28 | companion object {
29 | const val MaxTemperature = 35
30 | const val MaxWindSpeed = 40
31 | const val MaxPrecipitation = 100
32 | }
33 |
34 | override suspend fun generateForecast() = Forecast(dailyForecasts = generateDailyForecasts())
35 |
36 | private fun generateDailyForecasts(): List {
37 | val today = LocalDateTime.now().withMinuteOfHour(0)
38 |
39 | return (0..6).map { dayNumber ->
40 | val dayStartHour = if (dayNumber == 0) today.hourOfDay else 0
41 | val day = today.plusDays(dayNumber).withHourOfDay(dayStartHour)
42 |
43 | val temperature = Random.nextInt(0, MaxTemperature)
44 | val maxTemperature = (temperature * 1.2f).roundToInt().coerceAtMost(MaxTemperature)
45 | val minTemperature = (temperature / 1.2f).roundToInt().coerceAtLeast(0)
46 | val windSpeed = Random.nextInt(0, MaxWindSpeed)
47 | val precipitation = Random.nextInt(0, MaxPrecipitation)
48 | val weather = generateWeather(temperature, windSpeed, precipitation)
49 |
50 | DailyForecast(
51 | timestamp = day.toString(),
52 | hourlyForecasts = generateHourlyForecasts(
53 | temperature,
54 | maxTemperature,
55 | minTemperature,
56 | day
57 | ),
58 | temperature = temperature,
59 | minTemperature = minTemperature,
60 | maxTemperature = maxTemperature,
61 | precipitationProbability = precipitation,
62 | windSpeed = windSpeed,
63 | weather = weather
64 | )
65 | }
66 | }
67 |
68 | private fun generateHourlyForecasts(
69 | firstTemperature: Int,
70 | maxTemperature: Int,
71 | minTemperature: Int,
72 | day: LocalDateTime
73 | ): List {
74 | val firstHour = day.hourOfDay
75 |
76 | return (firstHour..23).map { hourNumber ->
77 | val hour = day.withHourOfDay(hourNumber)
78 | val temperature = when {
79 | hourNumber == firstHour -> firstTemperature
80 | minTemperature == maxTemperature -> minTemperature
81 | else -> Random.nextInt(
82 | minTemperature,
83 | maxTemperature
84 | )
85 | }
86 | val windSpeed = Random.nextInt(0, MaxWindSpeed)
87 | val precipitation = Random.nextInt(0, MaxPrecipitation)
88 | val weather = generateWeather(temperature, windSpeed, precipitation)
89 |
90 | HourlyForecast(
91 | timestamp = hour.toString(),
92 | temperature = temperature,
93 | weather = weather
94 | )
95 | }
96 | }
97 |
98 | private fun generateWeather(temperature: Int, windSpeed: Int, precipitation: Int) =
99 | when {
100 | precipitation >= 70 && windSpeed >= 10 && temperature > 20 -> Weather.Thunderstorm
101 | precipitation >= 50 || precipitation >= 30 && temperature <= 10 -> Weather.Rainy
102 | precipitation < 15 && temperature > 18 -> Weather.Cloudy
103 | windSpeed > 35 -> Weather.Windy
104 | else -> Weather.Sunny
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/forecast/ForecastDao.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.forecast
17 |
18 | import com.pp.jetweatherfy.domain.model.Forecast
19 |
20 | interface ForecastDao {
21 | suspend fun generateForecast(): Forecast
22 | }
23 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/repositories/CityRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.repositories
17 |
18 | import com.pp.jetweatherfy.data.city.CityDao
19 | import com.pp.jetweatherfy.domain.repositories.cities.ICityRepository
20 | import java.util.Locale
21 | import javax.inject.Inject
22 |
23 | class CityRepository @Inject constructor(private val cityDao: CityDao) : ICityRepository {
24 |
25 | override suspend fun getCities(query: String) = cityDao.getCities().filter {
26 | it.toLowerCase(Locale.getDefault()).startsWith(query.toLowerCase(Locale.getDefault()))
27 | }.sorted()
28 |
29 | override suspend fun addCity(city: String) = cityDao.addCity(formatCity(city))
30 |
31 | private fun formatCity(city: String): String = city.toLowerCase(Locale.getDefault()).split(" ").joinToString(" ") { it.capitalize(Locale.getDefault()) }
32 | }
33 |
--------------------------------------------------------------------------------
/data/src/main/java/com/pp/jetweatherfy/data/repositories/ForecastRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.data.repositories
17 |
18 | import com.pp.jetweatherfy.data.forecast.ForecastDao
19 | import com.pp.jetweatherfy.domain.repositories.forecast.IForecastRepository
20 | import javax.inject.Inject
21 |
22 | class ForecastRepository @Inject constructor(private val forecastDao: ForecastDao) :
23 | IForecastRepository {
24 |
25 | override suspend fun getForecast(city: String) = forecastDao.generateForecast().also { it.city = city }
26 | }
27 |
--------------------------------------------------------------------------------
/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/debug.keystore
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | import com.pp.jetweatherfy.buildsrc.Configs
2 | import com.pp.jetweatherfy.buildsrc.Libs
3 | import com.pp.jetweatherfy.buildsrc.Tests
4 |
5 | plugins {
6 | id 'com.android.library'
7 | id 'kotlin-android'
8 | id 'kotlin-kapt'
9 | id 'dagger.hilt.android.plugin'
10 | }
11 |
12 | android {
13 | compileSdkVersion Configs.CompileSdkVersion
14 |
15 | defaultConfig {
16 | minSdkVersion Configs.MinSdkVersion
17 | targetSdkVersion Configs.TargetSdkVersion
18 | versionCode Configs.VersionCode
19 | versionName Configs.VersionName
20 |
21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
22 | consumerProguardFiles "consumer-rules.pro"
23 | }
24 |
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 |
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 |
34 | packagingOptions {
35 | // Multiple dependency bring these files in. Exclude them to enable
36 | // our test APK to build (has no effect on our AARs)
37 | excludes += "/META-INF/AL2.0"
38 | excludes += "/META-INF/LGPL2.1"
39 | }
40 | }
41 |
42 | dependencies {
43 |
44 | implementation Libs.Hilt.library
45 | kapt Libs.Hilt.googleAndroidCompiler
46 | kapt Libs.Hilt.googleCompiler
47 |
48 | implementation Libs.Kotlin.stdlib
49 | implementation Libs.Coroutines.core
50 |
51 | testImplementation Tests.junit
52 | testImplementation Libs.Coroutines.test
53 | }
--------------------------------------------------------------------------------
/domain/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/domain/consumer-rules.pro
--------------------------------------------------------------------------------
/domain/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/domain/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/Constants.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain
17 |
18 | const val MaxTemperature = 35
19 | const val MaxWindSpeed = 40
20 | const val MaxPrecipitation = 100
21 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/base/FlowUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.base
17 |
18 | import kotlinx.coroutines.CoroutineDispatcher
19 | import kotlinx.coroutines.flow.Flow
20 | import kotlinx.coroutines.flow.catch
21 | import kotlinx.coroutines.flow.flowOn
22 |
23 | abstract class FlowUseCase(private val coroutineDispatcher: CoroutineDispatcher) {
24 |
25 | operator fun invoke(parameters: P): Flow> {
26 | return execute(parameters)
27 | .catch { e ->
28 | emit(Result.Error(Exception(e)))
29 | }.flowOn(coroutineDispatcher)
30 | }
31 |
32 | abstract fun execute(parameters: P): Flow>
33 | }
34 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/base/Result.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.base
17 |
18 | /**
19 | * A generic class that holds a value with its loading status.
20 | * @param
21 | */
22 | sealed class Result {
23 |
24 | data class Success(val data: T) : Result()
25 | data class Error(val error: Throwable) : Result()
26 | object Loading : Result()
27 |
28 | fun isLoading() = this is Loading
29 | fun isSuccessful() = this is Success
30 |
31 | override fun toString(): String {
32 | return when (this) {
33 | is Success<*> -> "Success[data=$data]"
34 | is Error -> "Error[exception=$error]"
35 | Loading -> "Loading"
36 | }
37 | }
38 | }
39 |
40 | fun Result?.successOr(fallback: T): T {
41 | if (this == null) return fallback
42 | return (this as? Result.Success)?.data ?: fallback
43 | }
44 |
45 | val Result.data: T?
46 | get() = (this as? Result.Success)?.data
47 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/di/DispatcherQualifiers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.di
17 |
18 | import javax.inject.Qualifier
19 |
20 | @Retention(AnnotationRetention.BINARY)
21 | @Qualifier
22 | annotation class DefaultDispatcher
23 |
24 | @Retention(AnnotationRetention.BINARY)
25 | @Qualifier
26 | annotation class IoDispatcher
27 |
28 | @Retention(AnnotationRetention.BINARY)
29 | @Qualifier
30 | annotation class MainDispatcher
31 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/di/DispatchersModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.di
17 |
18 | import dagger.Module
19 | import dagger.Provides
20 | import dagger.hilt.InstallIn
21 | import dagger.hilt.components.SingletonComponent
22 | import kotlinx.coroutines.CoroutineDispatcher
23 | import kotlinx.coroutines.Dispatchers
24 |
25 | @Module
26 | @InstallIn(SingletonComponent::class)
27 | object DispatchersModule {
28 |
29 | @DefaultDispatcher
30 | @Provides
31 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
32 |
33 | @IoDispatcher
34 | @Provides
35 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
36 |
37 | @MainDispatcher
38 | @Provides
39 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
40 | }
41 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/model/Forecast.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.model
17 |
18 | data class Forecast(
19 | var city: String = "",
20 | val dailyForecasts: List = listOf()
21 | )
22 |
23 | data class DailyForecast(
24 | val timestamp: String = "",
25 | val hourlyForecasts: List = listOf(),
26 | val temperature: Int = 0,
27 | val minTemperature: Int = 0,
28 | val maxTemperature: Int = 0,
29 | val precipitationProbability: Int = 0,
30 | val windSpeed: Int = 0,
31 | val weather: Weather = Weather.Sunny
32 | )
33 |
34 | data class HourlyForecast(
35 | val timestamp: String,
36 | val temperature: Int,
37 | val weather: Weather
38 | )
39 |
40 | enum class Weather {
41 | Sunny,
42 | Cloudy,
43 | Rainy,
44 | Thunderstorm,
45 | Windy
46 | }
47 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/repositories/cities/ICityRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.repositories.cities
17 |
18 | interface ICityRepository {
19 | suspend fun getCities(query: String): List
20 | suspend fun addCity(city: String)
21 | }
22 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/repositories/forecast/IForecastRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.repositories.forecast
17 |
18 | import com.pp.jetweatherfy.domain.model.Forecast
19 |
20 | interface IForecastRepository {
21 | suspend fun getForecast(city: String): Forecast
22 | }
23 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/usecases/cities/AddCity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.usecases.cities
17 |
18 | import com.pp.jetweatherfy.domain.base.FlowUseCase
19 | import com.pp.jetweatherfy.domain.base.Result
20 | import com.pp.jetweatherfy.domain.di.IoDispatcher
21 | import com.pp.jetweatherfy.domain.repositories.cities.ICityRepository
22 | import kotlinx.coroutines.CoroutineDispatcher
23 | import kotlinx.coroutines.flow.Flow
24 | import kotlinx.coroutines.flow.flow
25 | import javax.inject.Inject
26 |
27 | class AddCity @Inject constructor(
28 | private val citiesRepository: ICityRepository,
29 | @IoDispatcher dispatcher: CoroutineDispatcher
30 | ) : FlowUseCase(dispatcher) {
31 |
32 | override fun execute(parameters: String): Flow> {
33 | return flow {
34 | emit(Result.Loading)
35 | emit(Result.Success(citiesRepository.addCity(parameters)))
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/usecases/cities/FetchCities.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.usecases.cities
17 |
18 | import com.pp.jetweatherfy.domain.base.FlowUseCase
19 | import com.pp.jetweatherfy.domain.base.Result
20 | import com.pp.jetweatherfy.domain.di.IoDispatcher
21 | import com.pp.jetweatherfy.domain.repositories.cities.ICityRepository
22 | import kotlinx.coroutines.CoroutineDispatcher
23 | import kotlinx.coroutines.flow.Flow
24 | import kotlinx.coroutines.flow.flow
25 | import javax.inject.Inject
26 |
27 | class FetchCities @Inject constructor(
28 | private val citiesRepository: ICityRepository,
29 | @IoDispatcher dispatcher: CoroutineDispatcher
30 | ) : FlowUseCase>(dispatcher) {
31 |
32 | override fun execute(parameters: String): Flow>> {
33 | return flow {
34 | emit(Result.Loading)
35 | emit(Result.Success(citiesRepository.getCities(parameters)))
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/pp/jetweatherfy/domain/usecases/forecast/FetchForecast.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.usecases.forecast
17 |
18 | import com.pp.jetweatherfy.domain.base.FlowUseCase
19 | import com.pp.jetweatherfy.domain.base.Result
20 | import com.pp.jetweatherfy.domain.di.IoDispatcher
21 | import com.pp.jetweatherfy.domain.model.Forecast
22 | import com.pp.jetweatherfy.domain.repositories.forecast.IForecastRepository
23 | import kotlinx.coroutines.CoroutineDispatcher
24 | import kotlinx.coroutines.flow.Flow
25 | import kotlinx.coroutines.flow.flow
26 | import javax.inject.Inject
27 |
28 | class FetchForecast @Inject constructor(
29 | private val forecastRepository: IForecastRepository,
30 | @IoDispatcher dispatcher: CoroutineDispatcher
31 | ) : FlowUseCase(dispatcher) {
32 |
33 | override fun execute(parameters: String): Flow> {
34 | return flow {
35 | emit(Result.Loading)
36 | emit(Result.Success(forecastRepository.getForecast(parameters)))
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/domain/src/test/java/com/pp/jetweatherfy/domain/fakes/FakeCityRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.fakes
17 |
18 | import com.pp.jetweatherfy.domain.repositories.cities.ICityRepository
19 | import java.util.Locale
20 |
21 | class FakeCityRepository : ICityRepository {
22 | private val cities = mutableListOf(
23 | "San Francisco",
24 | "London",
25 | "New York",
26 | "Paris",
27 | "Moscow",
28 | "Tokyo",
29 | "Dubai",
30 | "Toronto"
31 | )
32 |
33 | override suspend fun getCities(query: String): List {
34 | return cities.filter {
35 | it.toLowerCase(Locale.getDefault())
36 | .startsWith(query.toLowerCase(Locale.getDefault()))
37 | }
38 | }
39 |
40 | override suspend fun addCity(city: String) {
41 | if (!cities.contains(city))
42 | cities.add(city)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/domain/src/test/java/com/pp/jetweatherfy/domain/fakes/FakeForecastRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.fakes
17 |
18 | import com.pp.jetweatherfy.domain.model.DailyForecast
19 | import com.pp.jetweatherfy.domain.model.Forecast
20 | import com.pp.jetweatherfy.domain.repositories.forecast.IForecastRepository
21 |
22 | class FakeForecastRepository : IForecastRepository {
23 |
24 | private val lisbonForecast = Forecast(
25 | city = "Lisbon",
26 | dailyForecasts = listOf()
27 | )
28 |
29 | private val londonForecast = Forecast(
30 | city = "London",
31 | dailyForecasts = listOf(DailyForecast())
32 | )
33 |
34 | override suspend fun getForecast(city: String): Forecast {
35 | return when (city) {
36 | lisbonForecast.city -> lisbonForecast
37 | londonForecast.city -> londonForecast
38 | else -> Forecast()
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/domain/src/test/java/com/pp/jetweatherfy/domain/usecases/cities/AddCityUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.usecases.cities
17 |
18 | import com.pp.jetweatherfy.domain.fakes.FakeCityRepository
19 | import kotlinx.coroutines.ExperimentalCoroutinesApi
20 | import kotlinx.coroutines.test.TestCoroutineDispatcher
21 | import kotlinx.coroutines.test.TestCoroutineScope
22 | import kotlinx.coroutines.test.runBlockingTest
23 | import org.junit.Test
24 |
25 | @ExperimentalCoroutinesApi
26 | class AddCityUseCaseTest {
27 |
28 | private val coroutineDispatcher = TestCoroutineDispatcher()
29 | private val coroutineScope = TestCoroutineScope(coroutineDispatcher)
30 |
31 | private val cityRepository = FakeCityRepository()
32 | private val addCityUseCase = AddCity(
33 | citiesRepository = cityRepository,
34 | dispatcher = coroutineDispatcher
35 | )
36 |
37 | @Test
38 | fun add_city_returns_new_cities() = coroutineScope.runBlockingTest {
39 | // Given
40 | val cityToAdd = "San Francisco"
41 | val expectedNewCities = listOf(
42 | cityToAdd
43 | )
44 |
45 | // When
46 | addCityUseCase(cityToAdd)
47 | val newCities = cityRepository.getCities(cityToAdd)
48 |
49 | // Then
50 | assert(newCities.size == 1)
51 | assert(newCities == expectedNewCities)
52 | }
53 |
54 | @Test
55 | fun add_city_twice_returns_same_cities() = coroutineScope.runBlockingTest {
56 | // Given
57 | val cityToAdd = "San Francisco"
58 | val expectedNewCities = listOf(
59 | cityToAdd
60 | )
61 |
62 | // When
63 | addCityUseCase(cityToAdd)
64 | val newCities = cityRepository.getCities(cityToAdd)
65 |
66 | // Then
67 | assert(newCities.size == 1)
68 | assert(newCities == expectedNewCities)
69 |
70 | addCityUseCase(cityToAdd)
71 | val citiesAfterSecondAdd = cityRepository.getCities(cityToAdd)
72 |
73 | // Then
74 | assert(citiesAfterSecondAdd.size == 1)
75 | assert(citiesAfterSecondAdd == expectedNewCities)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/domain/src/test/java/com/pp/jetweatherfy/domain/usecases/cities/FetchCitiesUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.usecases.cities
17 |
18 | import com.pp.jetweatherfy.domain.base.Result
19 | import com.pp.jetweatherfy.domain.base.data
20 | import com.pp.jetweatherfy.domain.fakes.FakeCityRepository
21 | import kotlinx.coroutines.ExperimentalCoroutinesApi
22 | import kotlinx.coroutines.flow.toList
23 | import kotlinx.coroutines.test.TestCoroutineDispatcher
24 | import kotlinx.coroutines.test.TestCoroutineScope
25 | import kotlinx.coroutines.test.runBlockingTest
26 | import org.junit.Test
27 |
28 | @ExperimentalCoroutinesApi
29 | class FetchCitiesUseCaseTest {
30 |
31 | private val coroutineDispatcher = TestCoroutineDispatcher()
32 | private val coroutineScope = TestCoroutineScope(coroutineDispatcher)
33 |
34 | private val fetchCitiesUseCase = FetchCities(
35 | citiesRepository = FakeCityRepository(),
36 | dispatcher = coroutineDispatcher
37 | )
38 |
39 | @Test
40 | fun fetch_cities_returns_all_cities() = coroutineScope.runBlockingTest {
41 | // Given
42 | val query = ""
43 | val expectedCities = listOf(
44 | "San Francisco",
45 | "London",
46 | "New York",
47 | "Paris",
48 | "Moscow",
49 | "Tokyo",
50 | "Dubai",
51 | "Toronto"
52 | )
53 |
54 | // When
55 | val result = fetchCitiesUseCase(query).toList()
56 | assert(result.size == 2)
57 | assert(result[0] == Result.Loading)
58 | val cities = result[1].data ?: listOf()
59 |
60 | // Then
61 | assert(cities == expectedCities)
62 | }
63 |
64 | @Test
65 | fun fetch_cities_returns_starts_with() = coroutineScope.runBlockingTest {
66 | // Given
67 | val query = "To"
68 | val expectedCities = listOf(
69 | "Tokyo",
70 | "Toronto"
71 | )
72 |
73 | // When
74 | val result = fetchCitiesUseCase(query).toList()
75 | assert(result.size == 2)
76 | assert(result[0] == Result.Loading)
77 | val cities = result[1].data ?: listOf()
78 |
79 | // Then
80 | assert(cities == expectedCities)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/domain/src/test/java/com/pp/jetweatherfy/domain/usecases/forecast/FetchForecastUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.domain.usecases.forecast
17 |
18 | import com.pp.jetweatherfy.domain.base.Result
19 | import com.pp.jetweatherfy.domain.base.data
20 | import com.pp.jetweatherfy.domain.fakes.FakeForecastRepository
21 | import com.pp.jetweatherfy.domain.model.Forecast
22 | import kotlinx.coroutines.ExperimentalCoroutinesApi
23 | import kotlinx.coroutines.flow.toList
24 | import kotlinx.coroutines.test.TestCoroutineDispatcher
25 | import kotlinx.coroutines.test.TestCoroutineScope
26 | import kotlinx.coroutines.test.runBlockingTest
27 | import org.junit.Test
28 |
29 | @ExperimentalCoroutinesApi
30 | class FetchForecastUseCaseTest {
31 |
32 | private val coroutineDispatcher = TestCoroutineDispatcher()
33 | private val coroutineScope = TestCoroutineScope(coroutineDispatcher)
34 |
35 | private val fetchForecastUseCase = FetchForecast(
36 | forecastRepository = FakeForecastRepository(),
37 | dispatcher = coroutineDispatcher
38 | )
39 |
40 | @Test
41 | fun fetch_forecast_returns_lisbon() = coroutineScope.runBlockingTest {
42 | // Given
43 | val expectedCity = "Lisbon"
44 |
45 | // When
46 | val results = fetchForecastUseCase(expectedCity).toList()
47 | assert(results.size == 2)
48 | assert(results[0] == Result.Loading)
49 | val forecast = results[1].data ?: Forecast()
50 |
51 | // Then
52 | assert(forecast.city == expectedCity)
53 | }
54 |
55 | @Test
56 | fun fetch_forecast_returns_london() = coroutineScope.runBlockingTest {
57 | // Given
58 | val expectedCity = "London"
59 |
60 | // When
61 | val results = fetchForecastUseCase(expectedCity).toList()
62 | assert(results.size == 2)
63 | assert(results[0] == Result.Loading)
64 | val forecast = results[1].data ?: Forecast()
65 |
66 | // Then
67 | assert(forecast.city == expectedCity)
68 | }
69 |
70 | @Test
71 | fun fetch_forecast_returns_default() = coroutineScope.runBlockingTest {
72 | // Given
73 | val expectedForecast = Forecast()
74 |
75 | // When
76 | val results = fetchForecastUseCase("").toList()
77 | assert(results.size == 2)
78 | assert(results[0] == Result.Loading)
79 | val forecast = results[1].data ?: Forecast()
80 |
81 | // Then
82 | assert(forecast == expectedForecast)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri May 14 00:10:54 BST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/presentation/build.gradle:
--------------------------------------------------------------------------------
1 | import com.pp.jetweatherfy.buildsrc.Compose
2 | import com.pp.jetweatherfy.buildsrc.Core
3 | import com.pp.jetweatherfy.buildsrc.Configs
4 | import com.pp.jetweatherfy.buildsrc.Libs
5 |
6 | plugins {
7 | id 'com.android.library'
8 | id 'kotlin-android'
9 | id 'kotlin-kapt'
10 | id 'dagger.hilt.android.plugin'
11 | }
12 |
13 | android {
14 | compileSdkVersion Configs.CompileSdkVersion
15 |
16 | defaultConfig {
17 | minSdkVersion Configs.MinSdkVersion
18 | targetSdkVersion Configs.TargetSdkVersion
19 | versionCode Configs.VersionCode
20 | versionName Configs.VersionName
21 |
22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23 | consumerProguardFiles "consumer-rules.pro"
24 | }
25 |
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 |
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 |
35 | buildFeatures {
36 | compose true
37 |
38 | // Disable unused AGP features
39 | buildConfig false
40 | aidl false
41 | renderScript false
42 | resValues false
43 | shaders false
44 | }
45 |
46 | composeOptions {
47 | kotlinCompilerExtensionVersion Compose.composeVersion
48 | }
49 |
50 | packagingOptions {
51 | // Multiple dependency bring these files in. Exclude them to enable
52 | // our test APK to build (has no effect on our AARs)
53 | excludes += "/META-INF/AL2.0"
54 | excludes += "/META-INF/LGPL2.1"
55 | }
56 |
57 | }
58 |
59 | dependencies {
60 | implementation project(':domain')
61 |
62 | implementation Core.androidXCore
63 | implementation Core.appCompat
64 | implementation Core.material
65 | implementation Libs.Kotlin.stdlib
66 | implementation Libs.Coroutines.core
67 |
68 | implementation Compose.activityCompose
69 | implementation Compose.ui
70 | implementation Compose.material
71 | implementation Compose.iconsExtended
72 | implementation Compose.tooling
73 | implementation Compose.runtime
74 | implementation Compose.hiltNavigation
75 | implementation Libs.DateTime.jodaTime
76 | implementation Libs.Lottie.lottie
77 | implementation Libs.Accompanist.insets
78 | implementation Libs.GoogleLocation.location
79 |
80 | implementation Libs.Hilt.library
81 | kapt Libs.Hilt.googleAndroidCompiler
82 | kapt Libs.Hilt.googleCompiler
83 | }
--------------------------------------------------------------------------------
/presentation/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/consumer-rules.pro
--------------------------------------------------------------------------------
/presentation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/di/LocationModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.di
17 |
18 | import android.content.Context
19 | import android.location.Geocoder
20 | import com.google.android.gms.location.FusedLocationProviderClient
21 | import com.google.android.gms.location.LocationServices
22 | import dagger.Module
23 | import dagger.Provides
24 | import dagger.hilt.InstallIn
25 | import dagger.hilt.android.components.ActivityComponent
26 | import dagger.hilt.android.qualifiers.ActivityContext
27 |
28 | @Module
29 | @InstallIn(ActivityComponent::class)
30 | object LocationModule {
31 |
32 | @Provides
33 | fun provideLocationProvider(
34 | @ActivityContext context: Context
35 | ): FusedLocationProviderClient {
36 | return LocationServices.getFusedLocationProviderClient(context)
37 | }
38 |
39 | @Provides
40 | fun provideGeoCoder(
41 | @ActivityContext context: Context
42 | ): Geocoder {
43 | return Geocoder(context)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/ForecastActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast
17 |
18 | import android.os.Bundle
19 | import androidx.activity.compose.setContent
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.collectAsState
22 | import androidx.compose.ui.tooling.preview.Preview
23 | import androidx.hilt.navigation.compose.hiltNavGraphViewModel
24 | import androidx.navigation.compose.NavHost
25 | import androidx.navigation.compose.composable
26 | import androidx.navigation.compose.navigate
27 | import androidx.navigation.compose.rememberNavController
28 | import com.google.accompanist.insets.ProvideWindowInsets
29 | import com.pp.jetweatherfy.presentation.forecast.base.LocationActivity
30 | import com.pp.jetweatherfy.presentation.forecast.events.LocationViewEvent
31 | import com.pp.jetweatherfy.presentation.forecast.navigation.NavigationDirections
32 | import com.pp.jetweatherfy.presentation.forecast.navigation.NavigationManager
33 | import com.pp.jetweatherfy.presentation.theme.JetWeatherfyTheme
34 | import dagger.hilt.android.AndroidEntryPoint
35 |
36 | @AndroidEntryPoint
37 | class ForecastActivity : LocationActivity() {
38 |
39 | private var forecastViewModel: ForecastViewModel? = null
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | setContent {
44 | JetWeatherfyTheme {
45 | ProvideWindowInsets {
46 | App(
47 | onLocationRequested = { viewModel ->
48 | if (forecastViewModel == null)
49 | forecastViewModel = viewModel
50 |
51 | getLocation()
52 | }
53 | )
54 | }
55 | }
56 | }
57 | }
58 |
59 | override fun onLocationSuccess(cityName: String) {
60 | forecastViewModel?.onLocationEvent(LocationViewEvent.SetLocation(cityName))
61 | }
62 |
63 | override fun onLocationFailure() {
64 | forecastViewModel?.onLocationEvent(LocationViewEvent.LocationError)
65 | }
66 |
67 | override fun onLocationRequestCanceled() {
68 | forecastViewModel?.onLocationEvent(LocationViewEvent.PermissionsError)
69 | }
70 | }
71 |
72 | @Composable
73 | fun App(onLocationRequested: (ForecastViewModel) -> Unit = {}) {
74 | val navController = rememberNavController()
75 |
76 | NavigationManager.command.collectAsState().value.also { command ->
77 | if (command.destination.isNotEmpty()) {
78 | navController.navigate(command.destination)
79 | }
80 | }
81 |
82 | NavHost(
83 | navController,
84 | startDestination = NavigationDirections.Forecast.destination
85 | ) {
86 | composable(NavigationDirections.Forecast.destination) { backStackEntry ->
87 | val viewModel = hiltNavGraphViewModel(backStackEntry = backStackEntry)
88 | ForecastScreen(viewModel, onLocationRequested = { onLocationRequested(viewModel) })
89 | }
90 | }
91 | }
92 |
93 | @Preview
94 | @Composable
95 | fun AppLightPreview() {
96 | JetWeatherfyTheme {
97 | ProvideWindowInsets {
98 | App()
99 | }
100 | }
101 | }
102 |
103 | @Preview
104 | @Composable
105 | fun AppDarkPreview() {
106 | JetWeatherfyTheme(darkTheme = true) {
107 | ProvideWindowInsets {
108 | App()
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/ForecastScreen.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast
17 |
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.collectAsState
20 | import androidx.compose.runtime.getValue
21 | import com.pp.jetweatherfy.presentation.forecast.components.content.ForecastContent
22 | import com.pp.jetweatherfy.presentation.forecast.components.surface.ForecastSurface
23 | import com.pp.jetweatherfy.presentation.forecast.components.topbar.ForecastTopBar
24 | import com.pp.jetweatherfy.presentation.forecast.events.ForecastViewEvent.SetSelectedDailyForecast
25 | import com.pp.jetweatherfy.presentation.forecast.events.ForecastViewEvent.SetViewType
26 | import com.pp.jetweatherfy.presentation.forecast.events.ForecastViewEvent.SetWeatherUnit
27 | import com.pp.jetweatherfy.presentation.forecast.events.LocationViewEvent.SearchCities
28 | import com.pp.jetweatherfy.presentation.forecast.events.LocationViewEvent.SetLocation
29 | import com.pp.jetweatherfy.presentation.forecast.state.ForecastViewState
30 | import com.pp.jetweatherfy.presentation.forecast.state.LocationViewState
31 | import com.pp.jetweatherfy.presentation.forecast.state.ViewType.Detailed
32 |
33 | @Composable
34 | fun ForecastScreen(viewModel: ForecastViewModel, onLocationRequested: () -> Unit) {
35 | val forecastViewState by viewModel.forecastViewState.collectAsState(ForecastViewState())
36 | val locationViewState by viewModel.locationViewState.collectAsState(LocationViewState())
37 |
38 | ForecastSurface(forecastViewState.selectedDailyForecast) {
39 | ForecastTopBar(
40 | forecastState = forecastViewState,
41 | locationState = locationViewState,
42 | onWeatherUnitToggled = { weatherUnit ->
43 | viewModel.onForecastEvent(SetWeatherUnit(weatherUnit))
44 | },
45 | onViewTypeToggled = { viewType ->
46 | viewModel.onForecastEvent(SetViewType(viewType))
47 | },
48 | onSetLocationClick = {
49 | onLocationRequested()
50 | },
51 | onQueryTyping = { query ->
52 | viewModel.onLocationEvent(SearchCities(query))
53 | },
54 | onCitySelected = { city ->
55 | viewModel.onLocationEvent(SetLocation(city))
56 | }
57 | )
58 | ForecastContent(
59 | forecastState = forecastViewState,
60 | onDailyForecastSelected = { dailyForecast ->
61 | viewModel.onForecastEvent(SetSelectedDailyForecast(dailyForecast))
62 | },
63 | onSeeMoreClick = {
64 | viewModel.onForecastEvent(SetViewType(Detailed))
65 | }
66 | )
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/base/LocationActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.base
17 |
18 | import android.Manifest
19 | import android.annotation.SuppressLint
20 | import android.content.pm.PackageManager
21 | import android.location.Geocoder
22 | import android.os.Bundle
23 | import androidx.appcompat.app.AppCompatActivity
24 | import com.google.android.gms.location.FusedLocationProviderClient
25 | import com.google.android.gms.location.LocationCallback
26 | import com.google.android.gms.location.LocationRequest
27 | import com.google.android.gms.location.LocationResult
28 | import com.pp.jetweatherfy.presentation.utils.askPermissions
29 | import com.pp.jetweatherfy.presentation.utils.hasPermissions
30 | import dagger.hilt.android.AndroidEntryPoint
31 | import javax.inject.Inject
32 |
33 | @AndroidEntryPoint
34 | abstract class LocationActivity : AppCompatActivity() {
35 |
36 | @Inject
37 | lateinit var locationProvider: FusedLocationProviderClient
38 |
39 | @Inject
40 | lateinit var geoCoder: Geocoder
41 |
42 | private val locationRequest: LocationRequest by lazy {
43 | LocationRequest.create().apply {
44 | priority = LocationRequest.PRIORITY_HIGH_ACCURACY
45 | interval = 5 * 1000
46 | }
47 | }
48 |
49 | private val locationCallback: LocationCallback by lazy {
50 | object : LocationCallback() {
51 | override fun onLocationResult(result: LocationResult) {
52 | locationProvider.removeLocationUpdates(locationCallback)
53 | }
54 | }
55 | }
56 |
57 | override fun onCreate(savedInstanceState: Bundle?) {
58 | super.onCreate(savedInstanceState)
59 | if (!hasPermissions(
60 | Manifest.permission.ACCESS_FINE_LOCATION,
61 | Manifest.permission.ACCESS_COARSE_LOCATION
62 | )
63 | ) {
64 | askPermissions(
65 | 100,
66 | Manifest.permission.ACCESS_FINE_LOCATION,
67 | Manifest.permission.ACCESS_COARSE_LOCATION
68 | )
69 | }
70 | }
71 |
72 | @SuppressLint("MissingPermission")
73 | protected fun getLocation() {
74 | locationProvider.lastLocation
75 | .addOnSuccessListener { location ->
76 | location?.let {
77 | val cityName =
78 | geoCoder.getFromLocation(location.latitude, location.longitude, 1)
79 | .firstOrNull()?.locality
80 | if (cityName != null)
81 | onLocationSuccess(cityName)
82 | else
83 | onLocationFailure()
84 | } ?: run {
85 | locationProvider.requestLocationUpdates(
86 | locationRequest,
87 | locationCallback,
88 | mainLooper
89 | )
90 | onLocationFailure()
91 | }
92 | }
93 | .addOnFailureListener {
94 | onLocationFailure()
95 | }
96 | }
97 |
98 | abstract fun onLocationSuccess(cityName: String)
99 | abstract fun onLocationFailure()
100 | abstract fun onLocationRequestCanceled()
101 |
102 | override fun onRequestPermissionsResult(
103 | requestCode: Int,
104 | permissions: Array,
105 | grantResults: IntArray
106 | ) {
107 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
108 | if (grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED) {
109 | getLocation()
110 | } else {
111 | onLocationRequestCanceled()
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/content/detailed/Days.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.content.detailed
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.clickable
20 | import androidx.compose.foundation.layout.Arrangement
21 | import androidx.compose.foundation.layout.Box
22 | import androidx.compose.foundation.layout.Column
23 | import androidx.compose.foundation.layout.Row
24 | import androidx.compose.foundation.layout.fillMaxWidth
25 | import androidx.compose.foundation.layout.padding
26 | import androidx.compose.foundation.lazy.LazyColumn
27 | import androidx.compose.foundation.lazy.LazyListState
28 | import androidx.compose.foundation.lazy.itemsIndexed
29 | import androidx.compose.material.MaterialTheme
30 | import androidx.compose.material.Text
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.ui.Alignment
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.draw.clip
35 | import androidx.compose.ui.graphics.Color
36 | import androidx.compose.ui.res.painterResource
37 | import androidx.compose.ui.res.stringResource
38 | import com.pp.jetweatherfy.domain.model.DailyForecast
39 | import com.pp.jetweatherfy.presentation.R
40 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherAnimation
41 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherTemperature
42 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherWindAndPrecipitation
43 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
44 | import com.pp.jetweatherfy.presentation.theme.BigDimension
45 | import com.pp.jetweatherfy.presentation.theme.MediumDimension
46 | import com.pp.jetweatherfy.presentation.theme.SmallDimension
47 | import com.pp.jetweatherfy.presentation.utils.UnselectedAlpha
48 | import com.pp.jetweatherfy.presentation.utils.generateColorBasedOnForecast
49 | import com.pp.jetweatherfy.presentation.utils.getFormattedTime
50 |
51 | @Composable
52 | internal fun Days(
53 | modifier: Modifier = Modifier,
54 | dailyForecastsScrollState: LazyListState,
55 | selectedDailyForecast: DailyForecast,
56 | dailyForecasts: List,
57 | weatherUnit: WeatherUnit,
58 | onDailyForecastSelected: (Int, DailyForecast) -> Unit
59 | ) {
60 | val backgroundColor =
61 | selectedDailyForecast.generateColorBasedOnForecast().copy(alpha = UnselectedAlpha)
62 |
63 | Box(
64 | modifier = modifier
65 | .clip(MaterialTheme.shapes.medium)
66 | .background(backgroundColor)
67 | .fillMaxWidth()
68 | .padding(MediumDimension),
69 | contentAlignment = Alignment.Center
70 | ) {
71 | LazyColumn(
72 | modifier = Modifier.fillMaxWidth(),
73 | verticalArrangement = Arrangement.spacedBy(MediumDimension),
74 | state = dailyForecastsScrollState
75 | ) {
76 | itemsIndexed(dailyForecasts) { index, dailyForecast ->
77 | Day(
78 | isSelected = dailyForecast == selectedDailyForecast,
79 | dailyForecast = dailyForecast,
80 | backgroundColor = backgroundColor,
81 | weatherUnit = weatherUnit,
82 | onDailyForecastSelected = { onDailyForecastSelected(index, dailyForecast) }
83 | )
84 | }
85 | }
86 | }
87 | }
88 |
89 | @Composable
90 | private fun Day(
91 | isSelected: Boolean,
92 | dailyForecast: DailyForecast,
93 | backgroundColor: Color,
94 | weatherUnit: WeatherUnit,
95 | onDailyForecastSelected: () -> Unit
96 | ) {
97 | Row(
98 | modifier = Modifier
99 | .fillMaxWidth()
100 | .clip(MaterialTheme.shapes.medium)
101 | .background(backgroundColor.copy(alpha = if (isSelected) UnselectedAlpha else 0f))
102 | .clickable { onDailyForecastSelected() }
103 | .padding(SmallDimension),
104 | horizontalArrangement = Arrangement.SpaceBetween
105 | ) {
106 | Description(dailyForecast = dailyForecast)
107 | ExtraInformation(dailyForecast = dailyForecast)
108 | WeatherTemperature(
109 | temperature = dailyForecast.temperature,
110 | minTemperature = dailyForecast.minTemperature,
111 | maxTemperature = dailyForecast.maxTemperature,
112 | temperatureStyle = MaterialTheme.typography.subtitle2,
113 | maxAndMinStyle = MaterialTheme.typography.body2,
114 | alignment = Alignment.Top,
115 | weatherUnit = weatherUnit
116 | )
117 | }
118 | }
119 |
120 | @Composable
121 | private fun Description(dailyForecast: DailyForecast) {
122 | Row(horizontalArrangement = Arrangement.spacedBy(SmallDimension)) {
123 | WeatherAnimation(weather = dailyForecast.weather, animationSize = BigDimension)
124 | Text(
125 | text = dailyForecast.getFormattedTime(),
126 | style = MaterialTheme.typography.subtitle2
127 | )
128 | }
129 | }
130 |
131 | @Composable
132 | private fun ExtraInformation(dailyForecast: DailyForecast) {
133 | Column(
134 | horizontalAlignment = Alignment.Start,
135 | verticalArrangement = Arrangement.spacedBy(SmallDimension)
136 | ) {
137 | WeatherWindAndPrecipitation(
138 | text = "${dailyForecast.windSpeed} km/h",
139 | icon = painterResource(id = R.drawable.ic_wind),
140 | contentDescription = stringResource(R.string.wind)
141 | )
142 | WeatherWindAndPrecipitation(
143 | text = "${dailyForecast.precipitationProbability} %",
144 | icon = painterResource(id = R.drawable.ic_drop),
145 | contentDescription = stringResource(R.string.precipitation_probability)
146 | )
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/content/detailed/ForecastDetailedView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.content.detailed
17 |
18 | import androidx.compose.animation.core.animateFloat
19 | import androidx.compose.animation.core.tween
20 | import androidx.compose.animation.core.updateTransition
21 | import androidx.compose.foundation.layout.Arrangement
22 | import androidx.compose.foundation.layout.Column
23 | import androidx.compose.foundation.layout.fillMaxSize
24 | import androidx.compose.foundation.layout.offset
25 | import androidx.compose.foundation.lazy.rememberLazyListState
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.runtime.getValue
28 | import androidx.compose.runtime.rememberCoroutineScope
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.draw.alpha
32 | import com.pp.jetweatherfy.domain.model.DailyForecast
33 | import com.pp.jetweatherfy.domain.model.Forecast
34 | import com.pp.jetweatherfy.presentation.forecast.components.content.contentOffsetTransition
35 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
36 | import com.pp.jetweatherfy.presentation.theme.MediumDimension
37 | import com.pp.jetweatherfy.presentation.utils.AnimationDuration
38 | import com.pp.jetweatherfy.presentation.utils.scrollToBegin
39 | import kotlinx.coroutines.launch
40 |
41 | @Composable
42 | fun ForecastDetailedView(
43 | modifier: Modifier = Modifier,
44 | isActive: Boolean,
45 | forecast: Forecast,
46 | selectedDailyForecast: DailyForecast,
47 | weatherUnit: WeatherUnit,
48 | onDailyForecastSelected: (DailyForecast) -> Unit
49 | ) {
50 | val coroutineScope = rememberCoroutineScope()
51 | val transition = updateTransition(targetState = isActive, label = "")
52 | val dailyForecastsScrollState = rememberLazyListState()
53 | val hourlyForecastsScrollState = rememberLazyListState()
54 |
55 | val firstTileValue by contentOffsetTransition(transition = transition, inverseStart = true)
56 | val secondTileValue by contentOffsetTransition(
57 | transition = transition,
58 | delay = 100,
59 | inverseStart = true
60 | )
61 | val alphaValue by transition.animateFloat(
62 | transitionSpec = { tween(AnimationDuration) },
63 | label = ""
64 | ) { active -> if (active) 1f else 0f }
65 |
66 | if (!isActive) {
67 | dailyForecastsScrollState.scrollToBegin(coroutineScope)
68 | hourlyForecastsScrollState.scrollToBegin(coroutineScope)
69 | }
70 |
71 | Column(
72 | modifier = Modifier.fillMaxSize(),
73 | horizontalAlignment = Alignment.CenterHorizontally,
74 | verticalArrangement = Arrangement.spacedBy(MediumDimension)
75 | ) {
76 | if (isActive || alphaValue > .15f) {
77 | Details(
78 | modifier = modifier
79 | .offset(x = firstTileValue)
80 | .alpha(alphaValue),
81 | selectedDailyForecast = selectedDailyForecast,
82 | hourlyForecastsScrollState = hourlyForecastsScrollState,
83 | weatherUnit = weatherUnit
84 | )
85 | Days(
86 | modifier = Modifier
87 | .offset(x = secondTileValue)
88 | .alpha(alphaValue),
89 | dailyForecasts = forecast.dailyForecasts,
90 | selectedDailyForecast = selectedDailyForecast,
91 | onDailyForecastSelected = { index, newSelectedDailyForecast ->
92 | onDailyForecastSelected(newSelectedDailyForecast)
93 | coroutineScope.launch {
94 | dailyForecastsScrollState.animateScrollToItem(index)
95 | hourlyForecastsScrollState.animateScrollToItem(0)
96 | }
97 | },
98 | weatherUnit = weatherUnit,
99 | dailyForecastsScrollState = dailyForecastsScrollState
100 | )
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/content/simple/Days.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.content.simple
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.clickable
20 | import androidx.compose.foundation.layout.Arrangement
21 | import androidx.compose.foundation.layout.Box
22 | import androidx.compose.foundation.layout.fillMaxWidth
23 | import androidx.compose.foundation.layout.padding
24 | import androidx.compose.foundation.lazy.LazyListState
25 | import androidx.compose.foundation.lazy.LazyRow
26 | import androidx.compose.foundation.lazy.itemsIndexed
27 | import androidx.compose.material.MaterialTheme
28 | import androidx.compose.material.Text
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.ui.Alignment
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.draw.clip
33 | import androidx.compose.ui.graphics.Color
34 | import androidx.compose.ui.res.stringResource
35 | import com.pp.jetweatherfy.domain.model.DailyForecast
36 | import com.pp.jetweatherfy.presentation.R
37 | import com.pp.jetweatherfy.presentation.theme.BigDimension
38 | import com.pp.jetweatherfy.presentation.theme.SmallDimension
39 | import com.pp.jetweatherfy.presentation.utils.SelectedAlpha
40 | import com.pp.jetweatherfy.presentation.utils.UnselectedAlpha
41 | import com.pp.jetweatherfy.presentation.utils.generateColorBasedOnForecast
42 | import com.pp.jetweatherfy.presentation.utils.getFormattedTime
43 |
44 | @Composable
45 | internal fun Days(
46 | modifier: Modifier = Modifier,
47 | scrollState: LazyListState,
48 | selectedDailyForecast: DailyForecast,
49 | dailyForecasts: List,
50 | onMoreClick: () -> Unit,
51 | onDailyForecastSelected: (Int, DailyForecast) -> Unit,
52 | ) {
53 | val backgroundColor =
54 | selectedDailyForecast.generateColorBasedOnForecast().copy(alpha = UnselectedAlpha)
55 |
56 | LazyRow(
57 | modifier = modifier.fillMaxWidth(),
58 | horizontalArrangement = Arrangement.spacedBy(BigDimension),
59 | state = scrollState
60 | ) {
61 | itemsIndexed(dailyForecasts) { index, dailyForecast ->
62 | val isSelectedAlpha =
63 | if (dailyForecast == selectedDailyForecast) SelectedAlpha else UnselectedAlpha
64 | Day(
65 | surfaceColor = backgroundColor.copy(alpha = isSelectedAlpha),
66 | text = dailyForecast.getFormattedTime(),
67 | onClick = { onDailyForecastSelected(index, dailyForecast) }
68 | )
69 | }
70 | item {
71 | Day(
72 | surfaceColor = backgroundColor,
73 | text = stringResource(R.string.more),
74 | onClick = { onMoreClick() }
75 | )
76 | }
77 | }
78 | }
79 |
80 | @Composable
81 | private fun Day(
82 | surfaceColor: Color,
83 | text: String,
84 | onClick: () -> Unit
85 | ) {
86 | Box(
87 | modifier = Modifier
88 | .clip(MaterialTheme.shapes.medium)
89 | .background(surfaceColor)
90 | .clickable { onClick() }
91 | .padding(SmallDimension),
92 | contentAlignment = Alignment.Center
93 | ) {
94 | Text(text = text, style = MaterialTheme.typography.subtitle1)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/content/simple/Details.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.content.simple
17 |
18 | import androidx.compose.foundation.layout.Arrangement
19 | import androidx.compose.foundation.layout.Column
20 | import androidx.compose.foundation.layout.Row
21 | import androidx.compose.foundation.layout.fillMaxWidth
22 | import androidx.compose.foundation.layout.wrapContentHeight
23 | import androidx.compose.material.MaterialTheme
24 | import androidx.compose.material.Text
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.res.painterResource
29 | import androidx.compose.ui.res.stringResource
30 | import com.pp.jetweatherfy.domain.model.DailyForecast
31 | import com.pp.jetweatherfy.presentation.R
32 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherAnimation
33 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherTemperature
34 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherWindAndPrecipitation
35 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
36 | import com.pp.jetweatherfy.presentation.theme.MediumDimension
37 | import com.pp.jetweatherfy.presentation.utils.getWeatherResources
38 |
39 | @Composable
40 | internal fun Details(
41 | modifier: Modifier = Modifier,
42 | selectedDailyForecast: DailyForecast,
43 | weatherUnit: WeatherUnit
44 | ) {
45 | Column(
46 | modifier = modifier
47 | .fillMaxWidth()
48 | .wrapContentHeight(),
49 | horizontalAlignment = Alignment.CenterHorizontally
50 | ) {
51 | Description(dailyForecast = selectedDailyForecast)
52 | WeatherTemperature(
53 | temperature = selectedDailyForecast.temperature,
54 | minTemperature = selectedDailyForecast.minTemperature,
55 | maxTemperature = selectedDailyForecast.maxTemperature,
56 | weatherUnit = weatherUnit
57 | )
58 | ExtraInformation(dailyForecast = selectedDailyForecast)
59 | }
60 | }
61 |
62 | @Composable
63 | private fun Description(dailyForecast: DailyForecast) {
64 | WeatherAnimation(weather = dailyForecast.weather)
65 |
66 | Text(
67 | text = stringResource(id = dailyForecast.weather.getWeatherResources().description),
68 | style = MaterialTheme.typography.subtitle1
69 | )
70 | }
71 |
72 | @Composable
73 | private fun ExtraInformation(dailyForecast: DailyForecast) {
74 | Row(
75 | horizontalArrangement = Arrangement.spacedBy(MediumDimension),
76 | verticalAlignment = Alignment.CenterVertically
77 | ) {
78 | WeatherWindAndPrecipitation(
79 | text = "${dailyForecast.windSpeed} km/h",
80 | icon = painterResource(id = R.drawable.ic_wind),
81 | contentDescription = stringResource(R.string.wind)
82 | )
83 | WeatherWindAndPrecipitation(
84 | text = "${dailyForecast.precipitationProbability} %",
85 | icon = painterResource(id = R.drawable.ic_drop),
86 | contentDescription = stringResource(R.string.precipitation_probability)
87 | )
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/content/simple/ForecastSimpleView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.content.simple
17 |
18 | import androidx.compose.animation.core.animateFloat
19 | import androidx.compose.animation.core.tween
20 | import androidx.compose.animation.core.updateTransition
21 | import androidx.compose.foundation.layout.Arrangement
22 | import androidx.compose.foundation.layout.Column
23 | import androidx.compose.foundation.layout.fillMaxSize
24 | import androidx.compose.foundation.layout.offset
25 | import androidx.compose.foundation.layout.padding
26 | import androidx.compose.foundation.lazy.rememberLazyListState
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.getValue
29 | import androidx.compose.runtime.rememberCoroutineScope
30 | import androidx.compose.ui.Alignment
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.draw.alpha
33 | import com.pp.jetweatherfy.domain.model.DailyForecast
34 | import com.pp.jetweatherfy.domain.model.Forecast
35 | import com.pp.jetweatherfy.presentation.forecast.components.content.contentOffsetTransition
36 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
37 | import com.pp.jetweatherfy.presentation.theme.BigDimension
38 | import com.pp.jetweatherfy.presentation.theme.MediumDimension
39 | import com.pp.jetweatherfy.presentation.utils.AnimationDuration
40 | import com.pp.jetweatherfy.presentation.utils.generateColorBasedOnForecast
41 | import com.pp.jetweatherfy.presentation.utils.scrollToBegin
42 | import kotlinx.coroutines.launch
43 |
44 | @Composable
45 | fun ForecastSimpleView(
46 | modifier: Modifier = Modifier,
47 | isActive: Boolean,
48 | forecast: Forecast,
49 | selectedDailyForecast: DailyForecast,
50 | weatherUnit: WeatherUnit,
51 | onSeeMoreClick: () -> Unit,
52 | onDailyForecastSelected: (DailyForecast) -> Unit
53 | ) {
54 | val coroutineScope = rememberCoroutineScope()
55 | val dailyForecastsScrollState = rememberLazyListState()
56 | val hourlyForecastsScrollState = rememberLazyListState()
57 | val transition =
58 | updateTransition(targetState = isActive, label = "JetWeatherfySimpleContentTransition")
59 |
60 | if (!isActive) {
61 | dailyForecastsScrollState.scrollToBegin(coroutineScope)
62 | hourlyForecastsScrollState.scrollToBegin(coroutineScope)
63 | }
64 |
65 | val firstTileValue by contentOffsetTransition(transition = transition)
66 | val secondTileValue by contentOffsetTransition(transition = transition, delay = 100)
67 | val thirdTileValue by contentOffsetTransition(transition = transition, delay = 200)
68 | val alphaValue by transition.animateFloat(
69 | transitionSpec = { tween(AnimationDuration) },
70 | label = ""
71 | ) { active -> if (active) 1f else 0f }
72 |
73 | Column(
74 | modifier = Modifier.fillMaxSize(),
75 | horizontalAlignment = Alignment.CenterHorizontally,
76 | verticalArrangement = Arrangement.spacedBy(MediumDimension)
77 | ) {
78 | if (isActive || alphaValue > .15f) {
79 | Details(
80 | modifier = modifier
81 | .offset(x = firstTileValue)
82 | .alpha(alphaValue),
83 | selectedDailyForecast = selectedDailyForecast,
84 | weatherUnit = weatherUnit
85 | )
86 | Days(
87 | modifier = Modifier
88 | .padding(top = BigDimension)
89 | .offset(x = secondTileValue)
90 | .alpha(alphaValue),
91 | scrollState = dailyForecastsScrollState,
92 | selectedDailyForecast = selectedDailyForecast,
93 | dailyForecasts = forecast.dailyForecasts.take(2),
94 | onMoreClick = { onSeeMoreClick() },
95 | onDailyForecastSelected = { index, newSelectedDailyForecast ->
96 | onDailyForecastSelected(newSelectedDailyForecast)
97 | coroutineScope.launch {
98 | dailyForecastsScrollState.animateScrollToItem(index)
99 | hourlyForecastsScrollState.animateScrollToItem(0)
100 | }
101 | }
102 | )
103 | Hours(
104 | modifier = Modifier
105 | .offset(x = thirdTileValue)
106 | .alpha(alphaValue),
107 | scrollState = hourlyForecastsScrollState,
108 | hourlyForecasts = selectedDailyForecast.hourlyForecasts,
109 | surfaceColor = selectedDailyForecast.generateColorBasedOnForecast(),
110 | weatherUnit = weatherUnit
111 | )
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/content/simple/Hours.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.content.simple
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.layout.Arrangement
20 | import androidx.compose.foundation.layout.Column
21 | import androidx.compose.foundation.layout.fillMaxWidth
22 | import androidx.compose.foundation.layout.padding
23 | import androidx.compose.foundation.lazy.LazyListState
24 | import androidx.compose.foundation.lazy.LazyRow
25 | import androidx.compose.foundation.lazy.items
26 | import androidx.compose.material.MaterialTheme
27 | import androidx.compose.material.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.draw.clip
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.unit.dp
34 | import com.pp.jetweatherfy.domain.model.HourlyForecast
35 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherAnimation
36 | import com.pp.jetweatherfy.presentation.forecast.components.utils.WeatherTemperature
37 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
38 | import com.pp.jetweatherfy.presentation.theme.MediumDimension
39 | import com.pp.jetweatherfy.presentation.utils.UnselectedAlpha
40 | import com.pp.jetweatherfy.presentation.utils.getFormattedTime
41 |
42 | @Composable
43 | internal fun Hours(
44 | modifier: Modifier = Modifier,
45 | scrollState: LazyListState,
46 | hourlyForecasts: List,
47 | surfaceColor: Color?,
48 | weatherUnit: WeatherUnit
49 | ) {
50 | val backgroundColor =
51 | (surfaceColor ?: MaterialTheme.colors.primary).copy(alpha = UnselectedAlpha)
52 |
53 | LazyRow(
54 | modifier = modifier.fillMaxWidth(),
55 | horizontalArrangement = Arrangement.spacedBy(MediumDimension),
56 | state = scrollState
57 | ) {
58 | items(hourlyForecasts) { hourlyForecast ->
59 | Hour(
60 | surfaceColor = backgroundColor,
61 | hourlyForecast = hourlyForecast,
62 | weatherUnit = weatherUnit
63 | )
64 | }
65 | }
66 | }
67 |
68 | @Composable
69 | private fun Hour(
70 | surfaceColor: Color,
71 | hourlyForecast: HourlyForecast,
72 | weatherUnit: WeatherUnit
73 | ) {
74 | Column(
75 | modifier = Modifier
76 | .clip(MaterialTheme.shapes.medium)
77 | .background(surfaceColor)
78 | .padding(MediumDimension),
79 | horizontalAlignment = Alignment.CenterHorizontally,
80 | verticalArrangement = Arrangement.spacedBy(8.dp)
81 | ) {
82 | Text(text = hourlyForecast.getFormattedTime(), style = MaterialTheme.typography.subtitle2)
83 | WeatherAnimation(weather = hourlyForecast.weather, animationSize = 30.dp)
84 | WeatherTemperature(
85 | temperature = hourlyForecast.temperature,
86 | temperatureStyle = MaterialTheme.typography.subtitle1,
87 | weatherUnit = weatherUnit
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/surface/ForecastSurface.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.surface
17 |
18 | import androidx.compose.animation.animateColorAsState
19 | import androidx.compose.animation.core.tween
20 | import androidx.compose.foundation.background
21 | import androidx.compose.foundation.layout.Column
22 | import androidx.compose.foundation.layout.fillMaxSize
23 | import androidx.compose.material.Surface
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.getValue
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.platform.testTag
28 | import com.pp.jetweatherfy.domain.model.DailyForecast
29 | import com.pp.jetweatherfy.presentation.utils.BackgroundAnimationDuration
30 | import com.pp.jetweatherfy.presentation.utils.generateColorBasedOnForecast
31 | import com.pp.jetweatherfy.presentation.utils.generateGradientFeel
32 |
33 | @Composable
34 | fun ForecastSurface(selectedDailyForecast: DailyForecast, content: @Composable () -> Unit) {
35 | val backgroundColorFeel by animateColorAsState(
36 | targetValue = selectedDailyForecast.generateColorBasedOnForecast(),
37 | animationSpec = tween(BackgroundAnimationDuration)
38 | )
39 |
40 | Surface(
41 | modifier = Modifier
42 | .testTag("ForecastSurface")
43 | .fillMaxSize()
44 | .background(generateGradientFeel(backgroundColorFeel)),
45 | ) {
46 | Column(modifier = Modifier.fillMaxSize()) {
47 | content()
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/utils/WeatherAnimation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.utils
17 |
18 | import androidx.compose.foundation.layout.requiredSize
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.unit.Dp
22 | import androidx.compose.ui.unit.dp
23 | import com.airbnb.lottie.compose.LottieAnimation
24 | import com.airbnb.lottie.compose.LottieAnimationSpec
25 | import com.airbnb.lottie.compose.rememberLottieAnimationState
26 | import com.pp.jetweatherfy.domain.model.Weather
27 | import com.pp.jetweatherfy.presentation.utils.getWeatherResources
28 |
29 | @Composable
30 | fun WeatherAnimation(weather: Weather, animationSize: Dp? = null) {
31 | val animationSpec = LottieAnimationSpec.RawRes(weather.getWeatherResources().icon)
32 | val animationState =
33 | rememberLottieAnimationState(autoPlay = true, repeatCount = Integer.MAX_VALUE)
34 |
35 | LottieAnimation(
36 | animationSpec,
37 | modifier = Modifier.requiredSize(animationSize ?: 150.dp),
38 | animationState
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/utils/WeatherTemperature.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.utils
17 |
18 | import androidx.compose.foundation.layout.Column
19 | import androidx.compose.foundation.layout.Row
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.material.Icon
22 | import androidx.compose.material.MaterialTheme
23 | import androidx.compose.material.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.painter.Painter
28 | import androidx.compose.ui.res.painterResource
29 | import androidx.compose.ui.res.stringResource
30 | import androidx.compose.ui.text.TextStyle
31 | import androidx.compose.ui.unit.dp
32 | import com.pp.jetweatherfy.presentation.R
33 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
34 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit.Imperial
35 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit.Metric
36 | import kotlin.math.roundToInt
37 |
38 | @Composable
39 | fun WeatherTemperature(
40 | temperature: Int,
41 | maxTemperature: Int? = null,
42 | minTemperature: Int? = null,
43 | weatherUnit: WeatherUnit,
44 | alignment: Alignment.Vertical = Alignment.CenterVertically,
45 | temperatureStyle: TextStyle = MaterialTheme.typography.h1,
46 | maxAndMinStyle: TextStyle = MaterialTheme.typography.subtitle2
47 | ) {
48 | val finalTemperature = when (weatherUnit) {
49 | Metric -> temperature
50 | Imperial -> ((temperature * (9 / 5f)) + 32).roundToInt()
51 | }
52 | val finalMinTemperature = minTemperature?.let {
53 | when (weatherUnit) {
54 | Metric -> minTemperature
55 | Imperial -> ((minTemperature * (9 / 5f)) + 32).roundToInt()
56 | }
57 | }
58 | val finalMaxTemperature = maxTemperature?.let {
59 | when (weatherUnit) {
60 | Metric -> maxTemperature
61 | Imperial -> ((maxTemperature * (9 / 5f)) + 32).roundToInt()
62 | }
63 | }
64 |
65 | Row(verticalAlignment = alignment) {
66 | AverageTemperature(
67 | temperature = finalTemperature,
68 | weatherUnit = weatherUnit,
69 | style = temperatureStyle
70 | )
71 | MinAndMaxTemperature(
72 | maxTemperature = finalMaxTemperature,
73 | minTemperature = finalMinTemperature,
74 | weatherUnit = weatherUnit,
75 | style = maxAndMinStyle
76 | )
77 | }
78 | }
79 |
80 | @Composable
81 | private fun AverageTemperature(
82 | temperature: Int,
83 | weatherUnit: WeatherUnit,
84 | style: TextStyle
85 | ) {
86 | Text(
87 | text = "$temperature${weatherUnit.indication}",
88 | style = style
89 | )
90 | }
91 |
92 | @Composable
93 | private fun MinAndMaxTemperature(
94 | maxTemperature: Int?,
95 | minTemperature: Int?,
96 | weatherUnit: WeatherUnit,
97 | style: TextStyle
98 | ) {
99 | Column(modifier = Modifier.padding(top = 1.dp)) {
100 | if (maxTemperature != null && minTemperature != null) {
101 | MaxAndMaxTemperatureItem(
102 | temperature = maxTemperature,
103 | weatherUnit = weatherUnit,
104 | style = style,
105 | icon = painterResource(id = R.drawable.ic_arrow_up),
106 | contentDescription = stringResource(R.string.max_temperature)
107 | )
108 | MaxAndMaxTemperatureItem(
109 | temperature = minTemperature,
110 | weatherUnit = weatherUnit,
111 | style = style,
112 | icon = painterResource(id = R.drawable.ic_arrow_down),
113 | contentDescription = stringResource(R.string.min_temperature)
114 | )
115 | }
116 | }
117 | }
118 |
119 | @Composable
120 | private fun MaxAndMaxTemperatureItem(
121 | temperature: Int,
122 | weatherUnit: WeatherUnit,
123 | style: TextStyle,
124 | icon: Painter,
125 | contentDescription: String?
126 | ) {
127 | Row {
128 | Icon(
129 | painter = icon,
130 | contentDescription = contentDescription
131 | )
132 | Text(
133 | text = "$temperature${weatherUnit.indication}",
134 | style = style
135 | )
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/components/utils/WeatherWindAndPrecipitation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.components.utils
17 |
18 | import androidx.compose.foundation.layout.Arrangement
19 | import androidx.compose.foundation.layout.Row
20 | import androidx.compose.foundation.layout.requiredSize
21 | import androidx.compose.material.Icon
22 | import androidx.compose.material.MaterialTheme
23 | import androidx.compose.material.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.painter.Painter
28 | import com.pp.jetweatherfy.presentation.theme.BigDimension
29 | import com.pp.jetweatherfy.presentation.theme.SmallDimension
30 |
31 | @Composable
32 | fun WeatherWindAndPrecipitation(text: String, icon: Painter, contentDescription: String?) {
33 | Row(
34 | horizontalArrangement = Arrangement.spacedBy(SmallDimension),
35 | verticalAlignment = Alignment.CenterVertically
36 | ) {
37 | Icon(
38 | painter = icon,
39 | contentDescription = contentDescription,
40 | modifier = Modifier.requiredSize(BigDimension)
41 | )
42 | Text(
43 | text = text,
44 | style = MaterialTheme.typography.subtitle2
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/events/ForecastViewEvent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.events
17 |
18 | import com.pp.jetweatherfy.domain.model.DailyForecast
19 | import com.pp.jetweatherfy.presentation.forecast.state.ViewStatus
20 | import com.pp.jetweatherfy.presentation.forecast.state.ViewType
21 | import com.pp.jetweatherfy.presentation.forecast.state.WeatherUnit
22 |
23 | sealed class ForecastViewEvent {
24 | data class GetForecast(val city: String) : ForecastViewEvent()
25 | data class SetSelectedDailyForecast(val selectedDailyForecast: DailyForecast) : ForecastViewEvent()
26 | data class SetViewStatus(val viewStatus: ViewStatus) : ForecastViewEvent()
27 | data class SetViewType(val viewType: ViewType) : ForecastViewEvent()
28 | data class SetWeatherUnit(val weatherUnit: WeatherUnit) : ForecastViewEvent()
29 | }
30 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/events/LocationViewEvent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.events
17 |
18 | sealed class LocationViewEvent {
19 | data class SearchCities(val query: String) : LocationViewEvent()
20 | data class SetLocation(val location: String) : LocationViewEvent()
21 | object LocationError : LocationViewEvent()
22 | object PermissionsError : LocationViewEvent()
23 | }
24 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/navigation/NavigationCommand.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.navigation
17 |
18 | import androidx.navigation.compose.NamedNavArgument
19 |
20 | interface NavigationCommand {
21 | val arguments: List
22 | val destination: String
23 |
24 | companion object {
25 | val Default = object : NavigationCommand {
26 | override val arguments = emptyList()
27 | override val destination = ""
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/navigation/NavigationDirections.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.navigation
17 |
18 | import androidx.navigation.compose.NamedNavArgument
19 |
20 | object NavigationDirections {
21 |
22 | val Forecast = object : NavigationCommand {
23 | override val arguments = emptyList()
24 |
25 | override val destination: String = "forecast"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/navigation/NavigationManager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.navigation
17 |
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 |
20 | object NavigationManager {
21 |
22 | val command = MutableStateFlow(NavigationCommand.Default)
23 |
24 | fun navigate(directions: NavigationCommand) {
25 | command.value = directions
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/state/ForecastViewState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.state
17 |
18 | import com.pp.jetweatherfy.domain.model.DailyForecast
19 | import com.pp.jetweatherfy.domain.model.Forecast
20 |
21 | data class ForecastViewState(
22 | val forecast: Forecast = Forecast(),
23 | val selectedDailyForecast: DailyForecast = DailyForecast(),
24 | val viewType: ViewType = ViewType.Simple,
25 | val viewStatus: ViewStatus = ViewStatus.Idle,
26 | val weatherUnit: WeatherUnit = WeatherUnit.Metric
27 | )
28 |
29 | enum class ViewStatus {
30 | Idle,
31 | Loading,
32 | Running,
33 | HandlingErrors
34 | }
35 |
36 | enum class ViewType {
37 | Simple,
38 | Detailed
39 | }
40 |
41 | enum class WeatherUnit(val indication: String) {
42 | Metric("ºC"),
43 | Imperial("ºF")
44 | }
45 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/forecast/state/LocationViewState.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.forecast.state
17 |
18 | data class LocationViewState(
19 | val query: String = "",
20 | val cities: List = emptyList(),
21 | val errorGettingLocation: Boolean = false,
22 | val errorGettingPermissions: Boolean = false
23 | )
24 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/theme/Color.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.theme
17 |
18 | import androidx.compose.ui.graphics.Color
19 |
20 | val grayish_blue_900 = Color(0xFF282b39)
21 | val grayish_blue_300 = Color(0xFF979cb1)
22 | val white_50 = Color(0xFFFAFAFA)
23 | val white_50_65a = Color(0xA6FAFAFA)
24 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/theme/Dimensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.theme
17 |
18 | import androidx.compose.ui.unit.dp
19 |
20 | val SmallDimension = 8.dp
21 | val MediumDimension = 16.dp
22 | val BigDimension = 24.dp
23 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.theme
17 |
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material.Shapes
20 | import androidx.compose.ui.unit.dp
21 |
22 | val shapes = Shapes(
23 | small = RoundedCornerShape(4.dp),
24 | medium = RoundedCornerShape(16.dp),
25 | large = RoundedCornerShape(16.dp)
26 | )
27 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.theme
17 |
18 | import androidx.compose.foundation.isSystemInDarkTheme
19 | import androidx.compose.material.MaterialTheme
20 | import androidx.compose.material.darkColors
21 | import androidx.compose.material.lightColors
22 | import androidx.compose.runtime.Composable
23 |
24 | private val DarkColorPalette = darkColors(
25 | primary = grayish_blue_900,
26 | secondary = grayish_blue_300,
27 | background = grayish_blue_900,
28 | surface = white_50_65a,
29 | onPrimary = white_50,
30 | onSecondary = white_50,
31 | onBackground = white_50,
32 | onSurface = grayish_blue_900
33 | )
34 |
35 | private val LightColorPalette = lightColors(
36 | primary = grayish_blue_900,
37 | secondary = grayish_blue_300,
38 | background = grayish_blue_900,
39 | surface = white_50_65a,
40 | onPrimary = white_50,
41 | onSecondary = white_50,
42 | onBackground = white_50,
43 | onSurface = grayish_blue_900
44 | )
45 |
46 | @Composable
47 | fun JetWeatherfyTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
48 | val colors = if (darkTheme) {
49 | DarkColorPalette
50 | } else {
51 | LightColorPalette
52 | }
53 |
54 | MaterialTheme(
55 | colors = colors,
56 | typography = typography,
57 | shapes = shapes,
58 | content = content
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/theme/Type.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.theme
17 |
18 | import androidx.compose.material.Typography
19 | import androidx.compose.ui.text.font.Font
20 | import androidx.compose.ui.text.font.FontFamily
21 | import androidx.compose.ui.text.font.FontStyle
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.unit.sp
24 | import com.pp.jetweatherfy.presentation.R
25 |
26 | private val AppFontFamily = FontFamily(
27 | fonts = listOf(
28 | Font(
29 | resId = R.font.comfortaa_light,
30 | weight = FontWeight.Light,
31 | style = FontStyle.Normal
32 | ),
33 | Font(
34 | resId = R.font.comfortaa_bold,
35 | weight = FontWeight.Bold,
36 | style = FontStyle.Normal
37 | ),
38 | Font(
39 | resId = R.font.comfortaa_semibold,
40 | weight = FontWeight.SemiBold,
41 | style = FontStyle.Normal
42 | )
43 | )
44 | )
45 |
46 | private val DefaultTypography = Typography()
47 | val typography = Typography(
48 | h1 = DefaultTypography.h1.copy(
49 | fontFamily = AppFontFamily,
50 | fontWeight = FontWeight.Bold,
51 | fontSize = 72.sp,
52 | letterSpacing = (0.1).sp
53 | ),
54 | h2 = DefaultTypography.h2.copy(
55 | fontFamily = AppFontFamily,
56 | fontWeight = FontWeight.Bold,
57 | fontSize = 28.sp,
58 | letterSpacing = (0.1).sp
59 | ),
60 | subtitle1 = DefaultTypography.subtitle1.copy(
61 | fontFamily = AppFontFamily,
62 | fontWeight = FontWeight.SemiBold,
63 | fontSize = 21.sp,
64 | letterSpacing = 0.sp
65 | ),
66 | subtitle2 = DefaultTypography.subtitle2.copy(
67 | fontFamily = AppFontFamily,
68 | fontWeight = FontWeight.SemiBold,
69 | fontSize = 14.sp,
70 | letterSpacing = 0.sp
71 | ),
72 | body1 = DefaultTypography.body1.copy(
73 | fontFamily = AppFontFamily,
74 | fontWeight = FontWeight.SemiBold,
75 | fontSize = 18.sp,
76 | letterSpacing = 0.sp
77 | ),
78 | body2 = DefaultTypography.body2.copy(
79 | fontFamily = AppFontFamily, fontWeight = FontWeight.Light,
80 | fontSize = 12.sp,
81 | letterSpacing = 0.sp
82 | ),
83 | button = DefaultTypography.button.copy(
84 | fontFamily = AppFontFamily,
85 | fontWeight = FontWeight.SemiBold,
86 | fontSize = 14.sp,
87 | letterSpacing = 1.sp
88 | ),
89 | caption = DefaultTypography.caption.copy(
90 | fontFamily = AppFontFamily,
91 | fontWeight = FontWeight.SemiBold,
92 | fontSize = 12.sp,
93 | letterSpacing = 0.sp
94 | )
95 | )
96 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.utils
17 |
18 | import androidx.compose.ui.unit.dp
19 |
20 | const val DailyTimestampFormat = "E, d MMM"
21 | const val HourlyTimestampFormat = "K:mm a"
22 | const val AnimationDuration = 1000
23 | const val BackgroundAnimationDuration = 400
24 | const val SelectedAlpha = 0.25f
25 | const val UnselectedAlpha = 0.1f
26 | val AnimationStartOffset = 400.dp
27 | val AnimationEndOffset = 0.dp
28 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/utils/ForecastUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.utils
17 |
18 | import androidx.annotation.RawRes
19 | import androidx.annotation.StringRes
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.res.stringResource
23 | import com.pp.jetweatherfy.domain.MaxPrecipitation
24 | import com.pp.jetweatherfy.domain.MaxTemperature
25 | import com.pp.jetweatherfy.domain.MaxWindSpeed
26 | import com.pp.jetweatherfy.domain.model.DailyForecast
27 | import com.pp.jetweatherfy.domain.model.HourlyForecast
28 | import com.pp.jetweatherfy.domain.model.Weather
29 | import com.pp.jetweatherfy.presentation.R
30 | import org.joda.time.LocalDateTime
31 | import java.util.Locale
32 |
33 | @Composable
34 | fun DailyForecast.getFormattedTime(): String {
35 | val timestampTime = LocalDateTime.parse(timestamp)
36 | val today = LocalDateTime.now()
37 | return when {
38 | timestampTime.dayOfYear == today.dayOfYear && timestampTime.year == today.year -> stringResource(
39 | R.string.today
40 | )
41 | timestampTime.dayOfYear == today.plusDays(1).dayOfYear && timestampTime.year == today.plusDays(
42 | 1
43 | ).year -> stringResource(
44 | R.string.tomorrow
45 | )
46 | else -> timestampTime.toString(DailyTimestampFormat)
47 | }
48 | }
49 |
50 | fun HourlyForecast.getFormattedTime() =
51 | LocalDateTime.parse(timestamp).toString(HourlyTimestampFormat)
52 | .toUpperCase(Locale.getDefault())
53 |
54 | fun DailyForecast.generateColorBasedOnForecast(): Color {
55 | return Color(
56 | red = (temperature * 255 / MaxTemperature.toFloat()) / 255f,
57 | green = (windSpeed * 255 / MaxWindSpeed.toFloat()) / 255f,
58 | blue = (precipitationProbability * 255 / MaxPrecipitation.toFloat()) / 255f
59 | )
60 | }
61 |
62 | data class WeatherResources(
63 | @StringRes val description: Int,
64 | @RawRes val icon: Int
65 | )
66 |
67 | fun Weather.getWeatherResources(): WeatherResources {
68 | return when (this) {
69 | Weather.Sunny -> WeatherResources(
70 | description = R.string.sunny,
71 | icon = R.raw.sunny
72 | )
73 | Weather.Cloudy -> WeatherResources(
74 | description = R.string.cloudy,
75 | icon = R.raw.cloudy
76 | )
77 | Weather.Rainy -> WeatherResources(
78 | description = R.string.rainy,
79 | icon = R.raw.rainy
80 | )
81 | Weather.Thunderstorm -> WeatherResources(
82 | description = R.string.thunderstorm,
83 | icon = R.raw.thunderstorm
84 | )
85 | Weather.Windy -> WeatherResources(
86 | description = R.string.windy,
87 | icon = R.raw.windy
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/utils/GradientColorUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.utils
17 |
18 | import androidx.annotation.Size
19 | import androidx.compose.ui.graphics.Brush
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.core.graphics.ColorUtils
22 | import kotlin.math.roundToInt
23 |
24 | @Size(3)
25 | private fun Color.toHSL(@Size(3) hsl: FloatArray = FloatArray(3)): FloatArray {
26 | val max = red.coerceAtLeast(green.coerceAtLeast(blue))
27 | val min = red.coerceAtMost(green.coerceAtMost(blue))
28 | hsl[2] = (max + min) / 2
29 |
30 | if (max == min) {
31 | hsl[1] = 0f
32 | hsl[0] = hsl[1]
33 | } else {
34 | val d = max - min
35 |
36 | hsl[1] = if (hsl[2] > 0.5f) d / (2f - max - min) else d / (max + min)
37 | when (max) {
38 | red -> hsl[0] = (green - blue) / d + (if (green < blue) 6 else 0)
39 | green -> hsl[0] = (blue - red) / d + 2
40 | blue -> hsl[0] = (red - green) / d + 4
41 | }
42 | hsl[0] /= 6f
43 | }
44 | return hsl
45 | }
46 |
47 | private fun hslToColor(@Size(3) hsl: FloatArray): Color {
48 | val r: Float
49 | val g: Float
50 | val b: Float
51 |
52 | val h = hsl[0]
53 | val s = hsl[1]
54 | val l = hsl[2]
55 |
56 | if (s == 0f) {
57 | b = l
58 | g = b
59 | r = g
60 | } else {
61 | val q = if (l < 0.5f) l * (1 + s) else l + s - l * s
62 | val p = 2 * l - q
63 | r = hue2rgb(p, q, h + 1f / 3)
64 | g = hue2rgb(p, q, h)
65 | b = hue2rgb(p, q, h - 1f / 3)
66 | }
67 |
68 | return Color(r, g, b)
69 | }
70 |
71 | private fun hue2rgb(p: Float, q: Float, t: Float): Float {
72 | var valueT = t
73 | if (valueT < 0) valueT += 1f
74 | if (valueT > 1) valueT -= 1f
75 | if (valueT < 1f / 6) return p + (q - p) * 6f * valueT
76 | if (valueT < 1f / 2) return q
77 | return if (valueT < 2f / 3) p + (q - p) * (2f / 3 - valueT) * 6f else p
78 | }
79 |
80 | fun Color.lightenColor(value: Float): Color {
81 | val hsl = toHSL()
82 | hsl[2] += value
83 | hsl[2] = 0f.coerceAtLeast(hsl[2].coerceAtMost(1f))
84 | return hslToColor(hsl)
85 | }
86 |
87 | fun Color.darkenColor(value: Float): Color {
88 | val hsl = toHSL()
89 | hsl[2] -= value
90 | hsl[2] = 0f.coerceAtLeast(hsl[2].coerceAtMost(1f))
91 | return hslToColor(hsl)
92 | }
93 |
94 | fun Color.isLightColor(hsl: FloatArray = FloatArray(3)): Boolean {
95 | ColorUtils.RGBToHSL((red * 255f).roundToInt(), (green * 255f).roundToInt(), (blue * 255f).roundToInt(), hsl)
96 | return hsl[2] < .5f
97 | }
98 |
99 | fun Color.isDarkColor(hsl: FloatArray = FloatArray(3)): Boolean {
100 | ColorUtils.RGBToHSL((red * 255f).roundToInt(), (green * 255f).roundToInt(), (blue * 255f).roundToInt(), hsl)
101 | return hsl[2] < .5f
102 | }
103 |
104 | fun generateGradientFeel(baseColor: Color, lightenFactor: Float = 0.3f): Brush {
105 | val lightFactor = lightenFactor.coerceIn(0f, 1f)
106 |
107 | return Brush.verticalGradient(
108 | colors = listOf(
109 | baseColor,
110 | baseColor.lightenColor(lightFactor)
111 | )
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/pp/jetweatherfy/presentation/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.pp.jetweatherfy.presentation.utils
17 |
18 | import android.content.Context
19 | import android.content.pm.PackageManager
20 | import android.view.View
21 | import android.view.inputmethod.InputMethodManager
22 | import androidx.compose.foundation.lazy.LazyListState
23 | import androidx.core.app.ActivityCompat
24 | import androidx.fragment.app.FragmentActivity
25 | import kotlinx.coroutines.CoroutineScope
26 | import kotlinx.coroutines.launch
27 |
28 | fun LazyListState.scrollToBegin(coroutineScope: CoroutineScope, animated: Boolean = true) {
29 | coroutineScope.launch {
30 | if (layoutInfo.totalItemsCount > 0) {
31 | if (animated)
32 | animateScrollToItem(0)
33 | else
34 | scrollToItem(0)
35 | }
36 | }
37 | }
38 |
39 | fun FragmentActivity.hasPermissions(vararg permissions: String) = permissions.all { permission ->
40 | ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
41 | }
42 |
43 | fun FragmentActivity.askPermissions(requestCode: Int, vararg permissions: String) {
44 | ActivityCompat.requestPermissions(
45 | this,
46 | permissions,
47 | requestCode
48 | )
49 | }
50 |
51 | fun View.hideKeyboard() {
52 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
53 | imm?.hideSoftInputFromWindow(windowToken, 0)
54 | }
55 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_arrow_up.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_check.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_clear.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_list.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_location.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-anydpi/ic_my_location.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_arrow_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_arrow_down.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_arrow_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_arrow_up.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_check.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_clear.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_list.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-hdpi/ic_my_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-hdpi/ic_my_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_arrow_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_arrow_down.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_arrow_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_arrow_up.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_check.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_clear.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_list.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-mdpi/ic_my_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-mdpi/ic_my_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
11 |
17 |
18 |
19 |
25 |
28 |
31 |
32 |
33 |
34 |
40 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_arrow_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_arrow_down.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_arrow_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_arrow_up.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_check.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_clear.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_list.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xhdpi/ic_my_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xhdpi/ic_my_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_arrow_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_arrow_down.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_arrow_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_arrow_up.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_check.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_clear.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_list.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-xxhdpi/ic_my_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable-xxhdpi/ic_my_location.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/centigrade.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable/centigrade.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/detailed_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable/detailed_view.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/fahrenheit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable/fahrenheit.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_drop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable/ic_drop.png
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_wind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/drawable/ic_wind.png
--------------------------------------------------------------------------------
/presentation/src/main/res/font/comfortaa_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/font/comfortaa_bold.ttf
--------------------------------------------------------------------------------
/presentation/src/main/res/font/comfortaa_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/font/comfortaa_light.ttf
--------------------------------------------------------------------------------
/presentation/src/main/res/font/comfortaa_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/presentation/src/main/res/font/comfortaa_semibold.ttf
--------------------------------------------------------------------------------
/presentation/src/main/res/raw/sunny.json:
--------------------------------------------------------------------------------
1 | {"v":"5.5.9","fr":30,"ip":0,"op":600,"w":200,"h":200,"nm":"天气-多云","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"“太阳”轮廓 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.005,99.986,0],"ix":2},"a":{"a":0,"k":[37.356,37.368,0],"ix":1},"s":{"a":0,"k":[135,135,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-20.489,-0.009],[-0.009,20.49],[0,0],[20.49,0.003],[0.003,-20.49]],"o":[[-0.009,20.49],[20.49,0.009],[0,0],[0.003,-20.49],[-20.49,-0.004],[0,0]],"v":[[-37.097,-0.008],[-0.013,37.109],[37.103,0.025],[37.103,-0.008],[0.009,-37.113],[-37.097,-0.02]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929000016755,0.663000009574,0.081999999402,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[37.356,37.368],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":601,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"“光芒”轮廓 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":600,"s":[360]}],"ix":10},"p":{"a":0,"k":[100.018,100.016,0],"ix":2},"a":{"a":0,"k":[58.9,61.35,0],"ix":1},"s":{"a":0,"k":[135,135,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.3,1],[-0.901,1.3],[0,0],[-1.299,-0.9],[0.9,-1.3],[0,0]],"o":[[-1.301,-1],[0,0],[1,-1.3],[1.3,1],[0,0],[-1,1.3]],"v":[[27.951,-38.5],[27.251,-42.7],[31.65,-48.7],[35.85,-49.4],[36.551,-45.2],[32.15,-39.2]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.5,1.6],[-1.5,0.5],[0,0],[-0.5,-1.5],[1.5,-0.5],[0,0]],"o":[[-0.5,-1.6],[0,0],[1.599,-0.5],[0.5,1.6],[0,0],[-1.6,0.5]],"v":[[45.251,-14.7],[47.15,-18.5],[54.251,-20.8],[58.051,-18.9],[56.15,-15.1],[49.051,-12.8]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[-0.5,1.6],[-1.6,-0.6],[0,0],[0.599,-1.6],[1.599,0.6],[0,0]],"o":[[0.5,-1.6],[0,0],[1.601,0.5],[-0.5,1.6],[0,0],[-1.599,-0.5]],"v":[[45.251,14.7],[49.051,12.8],[56.15,15.1],[58.051,18.9],[54.251,20.8],[47.15,18.5]],"c":true},"ix":2},"nm":"路径 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-1.4,1],[-0.899,-1.4],[0,0],[1.401,-0.9],[0.901,1.4],[0,0]],"o":[[1.3,-1],[0,0],[1,1.3],[-1.299,1],[0,0],[-1,-1.3]],"v":[[27.951,38.5],[32.15,39.2],[36.551,45.2],[35.85,49.4],[31.65,48.7],[27.251,42.7]],"c":true},"ix":2},"nm":"路径 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[-1.7,0],[0,-1.6],[0,0],[1.6,0],[0,1.6],[0,0]],"o":[[1.699,0],[0,0],[0,1.7],[-1.7,0],[0,0],[0,-1.7]],"v":[[-0.049,47.6],[2.951,50.6],[2.951,58.1],[-0.049,61.1],[-3.049,58.1],[-3.049,50.6]],"c":true},"ix":2},"nm":"路径 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[-1.3,-1],[0.901,-1.3],[0,0],[1.299,0.9],[-0.899,1.3],[0,0]],"o":[[1.3,1],[0,0],[-1,1.3],[-1.3,-1],[0,0],[1,-1.4]],"v":[[-28.049,38.5],[-27.35,42.7],[-31.749,48.7],[-35.949,49.4],[-36.65,45.2],[-32.249,39.2]],"c":true},"ix":2},"nm":"路径 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[-0.5,-1.6],[1.5,-0.5],[0,0],[0.5,1.5],[-1.5,0.5],[0,0]],"o":[[0.5,1.6],[0,0],[-1.6,0.5],[-0.5,-1.6],[0,0],[1.601,-0.6]],"v":[[-45.349,14.7],[-47.249,18.5],[-54.349,20.8],[-58.15,18.9],[-56.249,15.1],[-49.15,12.8]],"c":true},"ix":2},"nm":"路径 7","mn":"ADBE Vector Shape - Group","hd":false},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[0.641,-1.597],[1.512,0.696],[0,0],[-0.695,1.512],[-1.512,-0.695],[0,0]],"o":[[-0.558,1.542],[0,0],[-1.543,-0.558],[0.558,-1.542],[0,0],[1.543,0.557]],"v":[[-44.187,-14.327],[-48.104,-12.657],[-55.041,-15.347],[-56.712,-19.264],[-52.795,-20.934],[-45.858,-18.244]],"c":true},"ix":2},"nm":"路径 8","mn":"ADBE Vector Shape - Group","hd":false},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[1.399,-1],[0.9,1.4],[0,0],[-1.4,0.9],[-0.901,-1.4],[0,0]],"o":[[-1.3,1],[0,0],[-1,-1.3],[1.299,-1],[0,0],[1,1.3]],"v":[[-28.049,-38.5],[-32.249,-39.2],[-36.65,-45.2],[-35.949,-49.4],[-31.749,-48.7],[-27.35,-42.7]],"c":true},"ix":2},"nm":"路径 9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[1.699,0],[0,1.6],[0,0],[-1.601,0],[0,-1.6],[0,0]],"o":[[-1.7,0],[0,0],[0,-1.7],[1.699,0],[0,0],[0,1.6]],"v":[[-0.049,-47.6],[-3.049,-50.6],[-3.049,-58.1],[-0.049,-61.1],[2.951,-58.1],[2.951,-50.6]],"c":true},"ix":2},"nm":"路径 10","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929000016755,0.663000009574,0.081999999402,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[58.9,61.35],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":12,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":601,"st":0,"bm":0}],"markers":[]}
--------------------------------------------------------------------------------
/presentation/src/main/res/values-es-rES/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | JetWeatherfy
4 | Soleado
5 | Nublado
6 | Lluvioso
7 | Tormenta
8 | Ventoso
9 | Elige tu ciudad
10 | Localización
11 | Seleccionado
12 | Limpiar
13 | Velocidad del Viento
14 | Probabilidad de Precipitación
15 | “La conversación sobre el clima es el último refugio de los sin imaginación“.\n\n- Oscar Wilde
16 | Ver más
17 | Hoy
18 | Mañana
19 | Detectando tu localización…
20 | Obtener mi localización
21 | Alternar tipo de vista
22 | Temperature Máxima
23 | Temperatura Mínima
24 | Error al obtener tu localización. Asegúrese de tener la localización activada y vuelva a intentar en unos segundos.
25 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values-pt-rPT/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | JetWeatherfy
4 | Nublado
5 | Chuvoso
6 | Sol
7 | Trovoada
8 | Ventoso
9 | Escolhe a cidade
10 | Localização
11 | Selecionado
12 | Limpar
13 | Velocidade do Vento
14 | Probabilidade de Precipitação
15 | “A conversa sobre o tempo é o último refúgio dos sem imaginação.”\n\n- Oscar Wilde
16 | Ver mais
17 | Hoje
18 | Amanhã
19 | A detetar a tua localização…
20 | Obter a minha localização
21 | Trocar modo de vista
22 | Temperatura Máxima
23 | Temperatura Mínima
24 | Erro ao obter a sua localização. Certifique-se de que a localização está ativada e tente novamente em alguns segundos.
25 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 | #FFBB86FC
14 | #FF6200EE
15 | #FF3700B3
16 | #FF03DAC5
17 | #FF018786
18 | #FF000000
19 | #FFFFFFFF
20 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/ic_logo_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 | JetWeatherfy
13 | Sunny
14 | Cloudy
15 | Rainy
16 | Thunderstorm
17 | Windy
18 | Pick your city
19 | Location
20 | Selected
21 | Clear
22 | Wind Speed
23 | Precipitation Probability
24 | “Conversation about the weather is the last refuge of the unimaginative.”\n\n– Oscar Wilde
25 | See more
26 | Today
27 | Tomorrow
28 | Detecting your location…
29 | Get my location
30 | Toggle view type
31 | Max Temperature
32 | Min Temperature
33 | Error getting your location. Please be sure you have the location turned on and try again in some seconds.
34 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
26 |
27 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/results/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/results/screenshot_1.png
--------------------------------------------------------------------------------
/results/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/results/screenshot_2.png
--------------------------------------------------------------------------------
/results/video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauloaapereira/AndroidDevChallenge_Week4_JetWeatherfy/fcf55a107586de46a081d8e692c0f3722f4bc3d8/results/video.mp4
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "JetWeatherfy"
2 | include ':app'
3 | include ':data'
4 | include ':domain'
5 | include ':presentation'
6 |
--------------------------------------------------------------------------------
/spotless/copyright.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright $YEAR Paulo Pereira
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
--------------------------------------------------------------------------------