├── .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 | ![Workflow result](https://github.com///workflows/Check/badge.svg) 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 | License 5 | Build Status 6 | Medium 7 | Profile 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 | ![image](https://user-images.githubusercontent.com/27347361/114248588-ef5aa400-998f-11eb-9fae-e46a90d745c6.png) 37 | ![image](https://user-images.githubusercontent.com/27347361/114248605-fa153900-998f-11eb-8ff9-30d3e77ec851.png) 38 | 39 | 40 | 41 | ## Author 42 | 43 | Medium 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 | BuyMeACoffee 56 | 57 | Paypal 58 | 59 | ## License 60 | 61 | License 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 |